diff --git a/external/lwsf b/external/lwsf index bd58c50..db85219 160000 --- a/external/lwsf +++ b/external/lwsf @@ -1 +1 @@ -Subproject commit bd58c505e0e58a7747d62d8f9323bc7df39cf342 +Subproject commit db85219a97a38da80fa639e82cbf3b0a87b5c966 diff --git a/src/components/table.cpp b/src/components/table.cpp index fb37e07..1dd3b11 100644 --- a/src/components/table.cpp +++ b/src/components/table.cpp @@ -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 #include #include #include @@ -196,8 +197,6 @@ namespace lwcli { namespace component row.Decorate(ftxui::focus); } } - else - selected_ = -1; if (selected_ != highlighted_ && min_row() <= highlighted_ && highlighted_ < rows_) { diff --git a/src/main.cpp b/src/main.cpp index 969977b..5f2e9fb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 #include #include #include @@ -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) { - 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) { state.screen.ExitLoopClosure()(); diff --git a/src/views/accounts.cpp b/src/views/accounts.cpp index 18f8d12..2799d1f 100644 --- a/src/views/accounts.cpp +++ b/src/views/accounts.cpp @@ -29,6 +29,7 @@ #include "wallet.h" +#include #include #include #include diff --git a/src/views/history.cpp b/src/views/history.cpp index 12f1b1c..9c2e2fe 100644 --- a/src/views/history.cpp +++ b/src/views/history.cpp @@ -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 #include #include #include @@ -60,37 +61,72 @@ namespace lwcli { namespace view Monero::TransactionHistory* const history_; Monero::TransactionInfo const* info_; std::string note_; + std::string hash_; + std::vector 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 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"}; - - 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"}; + on_refresh(info); + hash_ = info->hash(); - 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(¬e_, std::move(options)); - ok_ = ftxui::Button(_("OK"), [this] () { - history_->setTxNote(info_->hash(), note_); - throw event::close{}; - }, ftxui::ButtonOption::Ascii()); - - container_ = ftxui::Container::Vertical({ - ftxui::Container::Horizontal({cancel_, ok_}), - note_input_ + 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({buttons_, note_input_}); + Add(container_); } }; @@ -196,10 +206,9 @@ namespace lwcli { namespace view const std::shared_ptr 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 row_map_; + std::vector 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, 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&& 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 (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)); return true; @@ -280,45 +321,28 @@ namespace lwcli { namespace view std::vector> transaction_list() { - Monero::TransactionHistory* tx_history = wallet_->history(); - if (!tx_history) - throw std::runtime_error{"unexpected history nullptr"}; - - std::map, std::pair, 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> 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 (sizeof(date) - 1 != std::strftime(date, sizeof(date), "%Y/%m/%d ", std::addressof(expanded))) - throw std::runtime_error{"strftime failed"}; + 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 */ + auto table = ftxui::vbox({ + get_title(), + ftxui::text(_("Balance: ") + lwsf::displayAmount(wallet_->balance(account_))), + table_->Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center | ftxui::flex + }); if (!overlay_) - { - auto table = table_->Render(); // compute balance first in callback - 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())); + return table; + return ftxui::dbox(std::move(table), decorate::overlay(overlay_->Render())); } }; } // anonymous diff --git a/src/views/wallet.cpp b/src/views/wallet.cpp index ec1214a..1000abe 100644 --- a/src/views/wallet.cpp +++ b/src/views/wallet.cpp @@ -83,7 +83,7 @@ namespace lwcli { namespace view ftxui::Component ActiveChild() override final { if (state_.overlay) - return state_.overlay; + return state_.overlay; return ui_; } @@ -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)