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