Merge remote-tracking branch 'upstream/main'

# Conflicts:
#	external/lwsf
This commit is contained in:
jwinterm
2026-03-20 21:41:56 -04:00
6 changed files with 141 additions and 122 deletions
+1 -1
+1 -2
View File
@@ -26,6 +26,7 @@
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <algorithm>
#include <ctime>
#include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
@@ -196,8 +197,6 @@ namespace lwcli { namespace component
row.Decorate(ftxui::focus);
}
}
else
selected_ = -1;
if (selected_ != highlighted_ && min_row() <= highlighted_ && highlighted_ < rows_)
{
+2
View File
@@ -26,6 +26,7 @@
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
@@ -289,6 +290,7 @@ int main(int, const char* argv[])
auto window = ftxui::CatchEvent(lwcli::view::manager(std::move(wm), std::move(prog.file)), [&] (ftxui::Event event)
{
if (event != lwcli::event::refresh_wallet)
state.last_event = std::chrono::steady_clock::now().time_since_epoch().count();
if (event == ftxui::Event::CtrlC)
{
+1
View File
@@ -29,6 +29,7 @@
#include "wallet.h"
#include <algorithm>
#include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
#include <ftxui/dom/table.hpp>
+127 -110
View File
@@ -26,6 +26,7 @@
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <cstring>
#include <ctime>
#include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
@@ -60,37 +61,72 @@ namespace lwcli { namespace view
Monero::TransactionHistory* const history_;
Monero::TransactionInfo const* info_;
std::string note_;
std::string hash_;
std::vector<Monero::TransactionInfo::Transfer> transfers_;
ftxui::Component note_input_;
const ftxui::Component cancel_;
ftxui::Component ok_;
ftxui::Component buttons_;
ftxui::Component container_;
ftxui::Element title_;
ftxui::Elements timestamp_;
ftxui::Elements payment_id_;
ftxui::Elements amount_;
ftxui::Elements fee_;
ftxui::Elements minors_;
ftxui::Elements coinbase_;
bool Focusable() const override final { return true; }
ftxui::Component ActiveChild() override final { return container_; }
bool OnEvent(ftxui::Event event) override final
{
if (event == ftxui::Event::CtrlQ)
if (event == event::refresh_wallet)
return on_refresh(history_->transaction(hash_));
else if (event == ftxui::Event::CtrlQ)
throw event::close{};
return container_->OnEvent(std::move(event));
}
virtual ftxui::Element OnRender() override final
{
auto buttons = ftxui::hcenter(buttons_->Render());
if (!info_)
{
return ftxui::window(
ftxui::text(_("Tx ") + hash_),
ftxui::vbox(std::move(buttons), ftxui::separator(), ftxui::text("No longer available"))
);
}
ftxui::Elements minors;
{
std::string minors_text;
for (const std::uint32_t minor : info_->subaddrIndex())
{
if (!minors.empty())
minors_text.append(", ");
minors_text.append(std::to_string(minor));
}
minors = {ftxui::text(_("Subaddress Minor: ")), ftxui::text(std::move(minors_text))};
}
ftxui::Elements timestamp;
{
std::tm expanded{};
const std::time_t raw = info_->timestamp();
if (gmtime_r(std::addressof(raw), std::addressof(expanded)))
{
char buf[100] = {0};
if (sizeof(buf) <= std::strftime(buf, sizeof(buf), "%B %d %Y %I:%M:%S UTC", std::addressof(expanded)))
throw std::runtime_error{"strftime failed"};
timestamp = {ftxui::text(_("Timestamp: ")), ftxui::text(buf)};
}
else
timestamp = {ftxui::text(_("Timestamp: ")), ftxui::text("gmtime failure")};
}
std::vector<ftxui::Elements> grid{
{ftxui::text(_("Description: ")), note_input_->Render()},
timestamp_,
payment_id_,
std::move(timestamp),
{ftxui::text(_("Payment ID: ")), ftxui::text(info_->paymentId())},
{ftxui::text(_("Confirmations: ")), ftxui::text(std::to_string(info_->confirmations()))},
amount_,
fee_,
{ftxui::text(_("Amount: ")), ftxui::text(print_amount(*info_))},
{ftxui::text(_("Fee: ")), ftxui::text(lwsf::displayAmount(info_->fee()))},
{ftxui::text(_("Block Height: ")), ftxui::text(std::to_string(info_->blockHeight()))},
minors_,
coinbase_
std::move(minors),
{ftxui::text(_("Coinbase: ")), ftxui::text(info_->isCoinbase() ? _("Yes"): _("No"))},
};
const std::size_t base_size = grid.size();
@@ -110,7 +146,7 @@ namespace lwcli { namespace view
ftxui::Elements vertical;
vertical.reserve(4);
vertical.push_back(ftxui::hcenter(ftxui::hbox(cancel_->Render(), ok_->Render())));
vertical.push_back(std::move(buttons));
if (info_->isFailed())
vertical.push_back(ftxui::inverted(ftxui::hcenter(ftxui::text(_("FAILED")))));
else if (info_->isPending())
@@ -120,74 +156,48 @@ namespace lwcli { namespace view
vertical.push_back(ftxui::gridbox(std::move(grid)));
return ftxui::window(title_, ftxui::vbox(std::move(vertical)));
return ftxui::window(ftxui::text(_("Tx ") + hash_), ftxui::vbox(std::move(vertical)));
}
bool on_refresh(const Monero::TransactionInfo* info)
{
info_ = info;
return true;
}
public:
explicit tx_details(Monero::TransactionHistory* history, std::size_t index)
explicit tx_details(Monero::TransactionHistory* history, const Monero::TransactionInfo* info)
: history_(history),
info_(nullptr),
note_(),
hash_(),
transfers_(),
note_input_(nullptr),
cancel_(ftxui::Button(_("Cancel"), [] () { throw event::close{}; }, ftxui::ButtonOption::Ascii())),
ok_(nullptr),
container_(),
title_(nullptr),
timestamp_(),
payment_id_(),
amount_(),
fee_(),
minors_(),
coinbase_()
buttons_(),
container_()
{
if (!history_)
throw std::runtime_error{"unexpected nullptr"};
info_ = history->transaction(index);
if (!info_)
throw std::runtime_error{"unexpected nullptr"};
on_refresh(info);
hash_ = info->hash();
title_ = ftxui::text(_("Tx ") + info_->hash());
{
std::tm expanded{};
const std::time_t timestamp = info_->timestamp();
if (!gmtime_r(std::addressof(timestamp), std::addressof(expanded)))
throw std::runtime_error{"gmtime failure"};
char buf[100] = {0};
if (sizeof(buf) <= std::strftime(buf, sizeof(buf), "%B %d %Y %I:%M:%S UTC", std::addressof(expanded)))
throw std::runtime_error{"strftime failed"};
timestamp_ = {ftxui::text(_("Timestamp: ")), ftxui::text(buf)};
}
payment_id_ = {ftxui::text(_("Payment ID: ")), ftxui::text(info_->paymentId())};
amount_ = {ftxui::text(_("Amount: ")), ftxui::text(print_amount(*info_))};
fee_ = {ftxui::text(_("Fee: ")), ftxui::text(lwsf::displayAmount(info_->fee()))};
{
std::string minors;
for (const std::uint32_t minor : info_->subaddrIndex())
{
if (!minors.empty())
minors.append(", ");
minors.append(std::to_string(minor));
}
minors_ = {ftxui::text(_("Subaddress Minor: ")), ftxui::text(std::move(minors))};
}
coinbase_ = {ftxui::text(_("Coinbase: ")), ftxui::text(info_->isCoinbase() ? _("Yes"): _("No"))};
note_ = info_->description();
note_ = info->description();
auto options = ftxui::InputOption::Default();
options.cursor_position = note_.size();
options.multiline = false;
note_input_ = ftxui::Input(&note_, std::move(options));
ok_ = ftxui::Button(_("OK"), [this] () {
history_->setTxNote(info_->hash(), note_);
buttons_ = ftxui::Container::Horizontal({
ftxui::Button(_("Cancel"), [] () { throw event::close{}; }, ftxui::ButtonOption::Ascii()),
ftxui::Button(_("OK"), [this] () {
history_->setTxNote(hash_, note_);
throw event::close{};
}, ftxui::ButtonOption::Ascii());
container_ = ftxui::Container::Vertical({
ftxui::Container::Horizontal({cancel_, ok_}),
note_input_
}, ftxui::ButtonOption::Ascii())
});
container_ = ftxui::Container::Vertical({buttons_, note_input_});
Add(container_);
}
};
@@ -196,10 +206,9 @@ namespace lwcli { namespace view
const std::shared_ptr<Monero::Wallet> wallet_;
ftxui::Component table_;
ftxui::Component overlay_;
ftxui::Element table_cached_; //!< Does not redraw when child is enabled
const std::string title1_;
const std::string title2_;
std::unordered_map<std::size_t, std::size_t> row_map_;
std::vector<const Monero::TransactionInfo*> row_map_;
const std::uint32_t account_;
bool Focusable() const override final { return true; }
@@ -216,13 +225,36 @@ namespace lwcli { namespace view
return ftxui::text(title1_ + wallet_->getSubaddressLabel(account_, 0) + title2_);
}
void load_history()
{
Monero::TransactionHistory* tx_history = wallet_->history();
if (!tx_history)
throw std::runtime_error{"unexpeted history nullptr"};
tx_history->refresh();
std::map<std::pair<std::uint64_t, std::string>, const Monero::TransactionInfo*, std::greater<>> ordered;
const auto history = tx_history->getAll();
for (const Monero::TransactionInfo* tx : history)
{
if (!tx)
throw std::runtime_error{"unexpected tx_info nullptr"};
if (tx->subaddrAccount() == account_)
ordered.try_emplace({tx->blockHeight(), tx->hash()}, tx);
}
row_map_.clear();
row_map_.reserve(ordered.size());
for (const auto& entry : ordered)
row_map_.push_back(entry.second);
}
public:
explicit history_(std::shared_ptr<Monero::Wallet>&& wallet, std::uint32_t account)
: ftxui::ComponentBase(),
wallet_(std::move(wallet)),
table_(),
overlay_(nullptr),
table_cached_(nullptr),
title1_(_("Account #") + std::to_string(account) + " / "),
title2_(" / " + wallet_->address(account, 0).substr(0, 20) + "..."),
row_map_(),
@@ -231,6 +263,8 @@ namespace lwcli { namespace view
if (!wallet_)
throw std::invalid_argument{"lwcli::view::history given nullptr"};
load_history();
// perform transalation lookup once
table_ = component::table(
{_("Date"), _("Amount"), _("Payment ID"), _("Label"), _("Desription"), _("Block"), _("Fee"), _("Hash")},
@@ -256,7 +290,14 @@ namespace lwcli { namespace view
{
try
{
if (event == event::refresh_wallet)
{
load_history();
if (overlay_)
overlay_->OnEvent(std::move(event));
return true;
}
else if (overlay_)
{
overlay_->OnEvent(std::move(event));
return true;
@@ -280,45 +321,28 @@ namespace lwcli { namespace view
std::vector<std::vector<std::string>> transaction_list()
{
Monero::TransactionHistory* tx_history = wallet_->history();
if (!tx_history)
throw std::runtime_error{"unexpected history nullptr"};
std::map<std::pair<std::uint64_t, std::string>, std::pair<const Monero::TransactionInfo*, std::size_t>, std::greater<>> ordered;
tx_history->refresh();
const auto history = tx_history->getAll();
for (std::size_t i = 0; i < history.size(); ++i)
{
const Monero::TransactionInfo* tx = history[i];
if (!tx)
throw std::runtime_error{"unexpected tx_info nullptr"};
if (tx->subaddrAccount() == account_)
ordered.try_emplace({tx->blockHeight(), tx->hash()}, tx, i);
}
std::vector<std::vector<std::string>> rows;
rows.reserve(ordered.size() + 2);
rows.reserve(row_map_.size() + 2);
std::size_t i = 0;
row_map_.clear();
for (const auto& entry : ordered)
for (const Monero::TransactionInfo* tx : row_map_)
{
const Monero::TransactionInfo* tx = entry.second.first;
row_map_.emplace(i, entry.second.second);
const std::uint64_t amount = tx->amount();
const int direction = tx->direction();
const std::uint64_t fee = tx->fee();
char date[12] = {0};
std::tm expanded{};
const std::time_t timestamp = tx->timestamp();
if (!gmtime_r(std::addressof(timestamp), std::addressof(expanded)))
throw std::runtime_error{"gmtime failure"};
char date[12] = {0};
if (gmtime_r(std::addressof(timestamp), std::addressof(expanded)))
{
if (sizeof(date) - 1 != std::strftime(date, sizeof(date), "%Y/%m/%d ", std::addressof(expanded)))
throw std::runtime_error{"strftime failed"};
}
else
{
std::strncpy(date, "gmtime fail", sizeof(date));
date[sizeof(date) - 1] = 0;
}
std::string payment_id = tx->paymentId();
if (payment_id.size() == 16 && payment_id.find_first_not_of('0') == std::string::npos)
@@ -346,8 +370,6 @@ namespace lwcli { namespace view
lwsf::displayAmount(tx->fee()),
tx->hash().substr(0, 16) + "..."
});
++i;
}
return rows;
@@ -355,19 +377,14 @@ namespace lwcli { namespace view
ftxui::Element OnRender() override final
{
/* Do not redraw table when showing tx. the
history()->refresh() call can invalidate pointers */
if (!overlay_)
{
auto table = table_->Render(); // compute balance first in callback
table_cached_ = ftxui::vbox({
auto table = ftxui::vbox({
get_title(),
ftxui::text(_("Balance: ") + lwsf::displayAmount(wallet_->balance(account_))),
std::move(table) | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center | ftxui::flex
table_->Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center | ftxui::flex
});
return table_cached_;
}
return ftxui::dbox(table_cached_, decorate::overlay(overlay_->Render()));
if (!overlay_)
return table;
return ftxui::dbox(std::move(table), decorate::overlay(overlay_->Render()));
}
};
} // anonymous
+1 -1
View File
@@ -152,7 +152,7 @@ namespace lwcli { namespace view
const bool has_overlay = bool(state_.overlay);
if (event == event::refresh_wallet)
return true;
return history_->OnEvent(std::move(event));
else if (state_.overlay)
state_.overlay->OnEvent(std::move(event));
else if (event == ftxui::Event::CtrlQ)