From 50ff86c56e7616395319574f75998f3750021275 Mon Sep 17 00:00:00 2001 From: Lee Clagett Date: Sun, 20 Apr 2025 21:29:51 -0400 Subject: [PATCH] Update lwsf pin, and added limited account support --- external/lwsf | 2 +- src/CMakeLists.txt | 1 + src/components/CMakeLists.txt | 34 ++++ src/components/table.cpp | 141 +++++++++++++++ src/components/table.h | 46 +++++ src/lwcli_config.h | 2 + src/views/CMakeLists.txt | 6 +- src/views/accounts.cpp | 324 ++++++++++++++++++++++++++++++++++ src/views/accounts.h | 39 ++++ src/views/history.cpp | 243 ++++++++++++------------- src/views/settings.cpp | 6 +- src/views/wallet.cpp | 14 +- 12 files changed, 715 insertions(+), 143 deletions(-) create mode 100644 src/components/CMakeLists.txt create mode 100644 src/components/table.cpp create mode 100644 src/components/table.h create mode 100644 src/views/accounts.cpp create mode 100644 src/views/accounts.h diff --git a/external/lwsf b/external/lwsf index d7fb796..38f4477 160000 --- a/external/lwsf +++ b/external/lwsf @@ -1 +1 @@ -Subproject commit d7fb7961f8d3142969db22051189782d66b09f69 +Subproject commit 38f44770b83ce36b528d79b497ca4c02f9c03ad2 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4a361bd..6131588 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. include_directories(.) +add_subdirectory(components) add_subdirectory(decorate) add_subdirectory(views) diff --git a/src/components/CMakeLists.txt b/src/components/CMakeLists.txt new file mode 100644 index 0000000..8d43240 --- /dev/null +++ b/src/components/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (c) 2025, Cifro Codes LLC +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# 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. + +set(lwcli-components_sources table.cpp) +set(lwscli-components_headers table.h) + +add_library(lwcli-components ${lwcli-components_sources} ${lwcli-components_headers}) +target_link_libraries(lwcli-components PRIVATE component dom) + diff --git a/src/components/table.cpp b/src/components/table.cpp new file mode 100644 index 0000000..0156a09 --- /dev/null +++ b/src/components/table.cpp @@ -0,0 +1,141 @@ +// Copyright (c) 2025, Cifro Codes LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// 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 + +#include "decorate/overlay.h" +#include "table.h" + +namespace lwcli { namespace component +{ + namespace + { + class table_ final : public ftxui::ComponentBase + { + const table_generator generator_; + const table_enter enter_; + const std::vector> title_; + std::ptrdiff_t rows_; + std::ptrdiff_t selected_; + + static constexpr std::ptrdiff_t min_row() noexcept { return 0; }; + bool Focusable() const override final { return true; } + + static std::vector> get_title_bar(std::vector&& title) + { + std::vector> out; + out.reserve(2); + + const std::size_t count = title.size(); + out.push_back(std::move(title)); + out.push_back(std::vector(count)); + + return out; + } + + public: + explicit table_(std::vector&& title, table_generator&& generator, table_enter&& enter) + : ftxui::ComponentBase(), + generator_(std::move(generator)), + enter_(std::move(enter)), + title_(get_title_bar(std::move(title))), + rows_(0), + selected_(-1) + { + set_size(generator_().size()); + } + + void set_size(const std::size_t rows) + { + if (std::numeric_limits::max() < rows) + throw std::runtime_error{"lwcli::component::table exceeds max size"}; + rows_ = rows; + } + + bool OnEvent(ftxui::Event event) override final + { + if (event == ftxui::Event::ArrowDown && selected_ + min_row() < rows_) + ++selected_; + else if (event == ftxui::Event::ArrowUp && min_row() <= selected_) + --selected_; + else if (event == ftxui::Event::PageDown) + selected_ = std::min(rows_ + min_row(), selected_ + 15); + else if (event == ftxui::Event::PageUp) + selected_ = std::max(min_row(), selected_ - 15); + else if (event == ftxui::Event::Return && min_row() <= selected_ && selected_ < rows_) + return enter_(std::size_t(selected_)); + + return min_row() <= selected_ && selected_ - min_row() < rows_; + } + + ftxui::Element OnRender() override final + { + auto rows = generator_(); + set_size(rows.size()); + + rows.reserve(rows.size() + title_.size()); + rows.insert(rows.begin(), title_.begin(), title_.end()); + + ftxui::Table table{std::move(rows)}; + + { + auto title = table.SelectRow(0); + title.Decorate(ftxui::bold); + title.SeparatorVertical(ftxui::LIGHT); + //title.Border(ftxui::LIGHT); + } + + if (Focused()) + { + if (selected_ < min_row()) + selected_ = min_row(); + else if (rows_ <= selected_ - min_row()) + selected_ = rows_ - 1 + min_row(); + + if (min_row() <= selected_ && selected_ < rows_) + { + auto row = table.SelectRow(std::size_t(selected_) + title_.size()); + row.Decorate(ftxui::inverted); + row.Decorate(ftxui::focus); + } + } + return table.Render(); + } + }; + } // anonymous + + ftxui::Component table(std::vector title, table_generator generator, table_enter enter) + { + if (!generator || !enter) + throw std::invalid_argument{"lwcli::components::table was given nullptr"}; + return std::make_shared(std::move(title), std::move(generator), std::move(enter)); + } +}} // lwcli // view diff --git a/src/components/table.h b/src/components/table.h new file mode 100644 index 0000000..a00b0a6 --- /dev/null +++ b/src/components/table.h @@ -0,0 +1,46 @@ +// Copyright (c) 2025, Cifro Codes LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// 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. + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace lwcli { namespace component +{ + //! Shows Transaction History + using table_generator = std::function>()>; + using table_enter = std::function; + ftxui::Component table(std::vector title, table_generator generator, table_enter enter); + +}} // lwscli // view + diff --git a/src/lwcli_config.h b/src/lwcli_config.h index 0b5dde7..f5f4c61 100644 --- a/src/lwcli_config.h +++ b/src/lwcli_config.h @@ -50,6 +50,8 @@ namespace lwcli { namespace config { return ( ... && verify_sso(strs)); } constexpr const std::string_view default_language{"English"}; + constexpr const std::string_view default_account_name{"Untitled Account"}; + constexpr const std::string_view default_subaddress_name{"Untitled Address"}; //! Timeout interval for inactivity with open wallet constexpr const std::chrono::minutes wallet_timeout{2}; diff --git a/src/views/CMakeLists.txt b/src/views/CMakeLists.txt index 3a13787..3f05fec 100644 --- a/src/views/CMakeLists.txt +++ b/src/views/CMakeLists.txt @@ -26,9 +26,9 @@ # 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. -set(lwcli-views_sources history.cpp keys.cpp manager.cpp settings.cpp wallet.cpp) -set(lwscli-views_headers history.h keys.h manager.h settings.h wallet.h) +set(lwcli-views_sources accounts.cpp history.cpp keys.cpp manager.cpp settings.cpp wallet.cpp) +set(lwscli-views_headers accounts.h history.h keys.h manager.h settings.h wallet.h) add_library(lwcli-views ${lwcli-views_sources} ${lwcli-views_headers}) -target_link_libraries(lwcli-views PRIVATE component dom lwcli-decorate lwsf-api) +target_link_libraries(lwcli-views PRIVATE component dom lwcli-components lwcli-decorate lwsf-api) diff --git a/src/views/accounts.cpp b/src/views/accounts.cpp new file mode 100644 index 0000000..7be3393 --- /dev/null +++ b/src/views/accounts.cpp @@ -0,0 +1,324 @@ +// Copyright (c) 2025, Cifro Codes LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// 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 "wallet.h" + +#include +#include +#include +#include + +#include "components/table.h" +#include "decorate/overlay.h" +#include "events.h" +#include "lwcli_config.h" +#include "translate.h" +#include "util.h" + +namespace lwcli { namespace view +{ + namespace + { + ftxui::Component last_input(std::string* str) + { + auto opt = ftxui::InputOption::Default(); + opt.cursor_position = str->size(); + opt.multiline = false; + return ftxui::Input(str, std::move(opt)); + } + + ftxui::ButtonOption ascii() { return ftxui::ButtonOption::Ascii(); } + + class account_detail final : public ftxui::ComponentBase + { + std::string account_name_; + const std::shared_ptr wal_; + Monero::Subaddress* const acct_; + const ftxui::Element title_; + const ftxui::Elements address_; + const ftxui::Element desc_; + ftxui::Component details_; + ftxui::Component table_; + ftxui::Component buttons_; + const ftxui::Component name_; + ftxui::Component ui_; + std::unordered_map row_map_; + const std::uint32_t id_; + + bool Focusable() const override final { return true; } + ftxui::Component ActiveChild() override final + { + if (details_) + return details_; + return ui_; + } + + public: + explicit account_detail(std::shared_ptr wal, Monero::Subaddress* acct, std::size_t id) + : ftxui::ComponentBase(), + account_name_(wal->getSubaddressLabel(id, 0)), + wal_(std::move(wal)), + acct_(acct), + title_(ftxui::text(_("Account #") + std::to_string(id))), + address_({ftxui::text("Address: "), ftxui::text(wal_->address(id, 0))}), + desc_(ftxui::text(_("Name: "))), + details_(), + table_(), + buttons_(), + name_(last_input(&account_name_)), + ui_(), + id_(id) + { + if (!acct_) + throw std::runtime_error{"lwcli::account_detail given nullptr"}; + if (std::numeric_limits::max() < id_) + throw std::runtime_error{"lwcli::account_detail given invalid id"}; + + buttons_ = ftxui::Container::Horizontal({ + ftxui::Button(_("Cancel"), [] () { throw event::close{}; }, ascii()), + ftxui::Button(_("Save"), [this] () { + wal_->setSubaddressLabel(id_, 0, account_name_); + throw event::close{}; + }, ascii()), + ftxui::Button(_("Add Subaddress"), [this] () { + acct_->addRow(id_, std::string{config::default_subaddress_name}); + }, ascii()) + }); + + table_ = component::table( + {_("#"), _("Label"), _("Address")}, + [this] () { return subaddress_list(); }, + [this] (std::size_t i) { return display_details(i); } + ); + + ui_ = ftxui::Container::Vertical({buttons_, name_, table_}); + Add(ui_); + } + + bool display_details(const std::size_t index) + { + if (row_map_.find(index) == row_map_.end()) + throw std::logic_error{"DEBUG"}; + return true; + } + + bool OnEvent(ftxui::Event event) override final + { + try + { + if (details_) + return details_->OnEvent(std::move(event)); + else if (event == ftxui::Event::CtrlQ) + throw event::close{}; + ui_->OnEvent(std::move(event)); + } + catch (const event::close&) + { + if (!details_) + throw; + details_->Detach(); + details_.reset(); + } + return true; + } + + std::vector> subaddress_list() + { + std::vector> rows{}; + + acct_->refresh(id_); + auto all = acct_->getAll(); + + // reverse order sort + std::sort(all.begin(), all.end(), [] (const auto x, const auto y) { + return y->getRowId() < x->getRowId(); + }); + + std::size_t i = 0; + rows.reserve(all.size()); + for (Monero::SubaddressRow const* const detail : all) + { + const std::size_t id = detail->getRowId(); + row_map_.try_emplace(i++).first->second = id; + rows.push_back({ + std::to_string(id), + detail->getLabel(), + detail->getAddress().substr(0, 20) + "..." + }); + } + + return rows; + } + + ftxui::Element OnRender() override final + { + return ftxui::window(title_, ftxui::vbox({ + buttons_->Render() | ftxui::hcenter, + ftxui::separator(), + ftxui::gridbox({address_, {desc_, name_->Render()}}), + ftxui::separator(), + table_->Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center + })); + } + }; + + + class accounts_ final : public ftxui::ComponentBase + { + const std::shared_ptr wal_; + Monero::SubaddressAccount* const wal_accounts_; + std::uint32_t* const account_; + const ftxui::Element title_; + ftxui::Component details_; + ftxui::Component table_; + ftxui::Element table_cached_; + ftxui::Component buttons_; + ftxui::Component ui_; + std::unordered_map row_map_; + + bool Focusable() const override final { return true; } + ftxui::Component ActiveChild() override final + { + if (details_) + return details_; + return ui_; + } + + public: + explicit accounts_(std::shared_ptr&& wal, std::uint32_t* account) + : ftxui::ComponentBase(), + wal_(std::move(wal)), + wal_accounts_(wal_->subaddressAccount()), + account_(account), + title_(ftxui::text(_("Accounts"))), + details_(), + table_(), + table_cached_(), + buttons_(), + ui_(), + row_map_() + { + buttons_ = ftxui::Container::Horizontal({ + ftxui::Button(_("Close"), [] () { throw event::close{}; }, ascii()), + ftxui::Button(_("Add Account"), [this] () { + wal_->addSubaddressAccount(std::string{config::default_account_name}); + }, ascii()), + }); + + table_ = component::table( + {_("#"), _("Balance"), _("Label"), _("Address")}, + [this] () { return accounts_list(); }, + [this] (std::size_t i) { return display_details(i); } + ); + + ui_ = ftxui::Container::Vertical({buttons_, table_}); + Add(ui_); + } + + bool display_details(const std::size_t index) + { + if (details_) + details_->Detach(); + details_ = std::make_shared(wal_, wal_->subaddress(), row_map_.at(index)); + return true; + } + + bool OnEvent(ftxui::Event event) override final + { + try + { + if (details_) + return details_->OnEvent(std::move(event)); + else if (event == ftxui::Event::CtrlQ) + throw event::close{}; + ui_->OnEvent(std::move(event)); + } + catch (const event::close&) + { + if (!details_) + throw; + details_->Detach(); + details_.reset(); + } + return true; + } + + std::vector> accounts_list() + { + std::vector> rows{}; + + wal_accounts_->refresh(); + auto all = wal_accounts_->getAll(); + + // reverse order sort + std::sort(all.begin(), all.end(), [] (const auto x, const auto y) { + return y->getRowId() < x->getRowId(); + }); + + std::size_t i = 0; + rows.reserve(all.size()); + for (Monero::SubaddressAccountRow const* const detail : all) + { + const std::size_t id = detail->getRowId(); + char const* const active = id == *account_ ? "*" : ""; + row_map_.try_emplace(i++).first->second = id; + rows.push_back({ + active + std::to_string(id), + detail->getBalance() + " XMR", + detail->getLabel(), + detail->getAddress().substr(0, 12) + "..." + }); + } + + return rows; + } + + ftxui::Element OnRender() override final + { + if (!details_) + { + table_cached_ = ftxui::window(title_, ftxui::vbox({ + buttons_->Render() | ftxui::hcenter, + ftxui::separator(), + table_->Render() //| ftxui::vscroll_indicator | ftxui::yframe | ftxui::center + })); + return table_cached_; + } + return ftxui::dbox(table_cached_, decorate::overlay(details_->Render())); + } + }; + } + + ftxui::Component accounts(std::shared_ptr wal, std::uint32_t* account) + { + if (!wal || !account) + throw std::invalid_argument{"views::accounts cannot be given nullptr"}; + return std::make_shared(std::move(wal), account); + } + +}} // lwcli // diff --git a/src/views/accounts.h b/src/views/accounts.h new file mode 100644 index 0000000..56680c7 --- /dev/null +++ b/src/views/accounts.h @@ -0,0 +1,39 @@ +// copyright (c) 2025, cifro codes llc +// +// all rights reserved. +// +// redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// this software is provided by the copyright holders and contributors "as is" and any +// express or implied warranties, including, but not limited to, the implied warranties of +// merchantability and fitness for a particular purpose are disclaimed. in no event shall +// the copyright holder or contributors be liable for any direct, indirect, incidental, +// special, exemplary, or consequential damages (including, but not limited to, +// procurement of substitute goods or services; loss of use, data, or profits; or business +// interruption) however caused and on any theory of liability, whether in contract, +// 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. +#pragma once + +#include +#include +#include + +namespace Monero { class Wallet; } +namespace lwcli { namespace view +{ + ftxui::Component accounts(std::shared_ptr wallet, std::uint32_t* account); +}} // lwscli // view + diff --git a/src/views/history.cpp b/src/views/history.cpp index fb72411..1ee96df 100644 --- a/src/views/history.cpp +++ b/src/views/history.cpp @@ -34,6 +34,7 @@ #include #include +#include "components/table.h" #include "decorate/overlay.h" #include "events.h" #include "history.h" @@ -244,27 +245,30 @@ namespace lwcli { namespace view class history_ final : public ftxui::ComponentBase { const std::shared_ptr wallet_; + ftxui::Component table_; ftxui::Component overlay_; ftxui::Element title_; - ftxui::Element table_; //!< Does not redraw when child is enabled - std::vector> base_rows; - std::ptrdiff_t highlighted_; - std::unordered_map row_map_; + ftxui::Element table_cached_; //!< Does not redraw when child is enabled + std::unordered_map row_map_; + std::uint64_t balance_; const std::uint32_t account_; - static constexpr std::size_t min_row() noexcept { return 2; }; bool Focusable() const override final { return true; } - ftxui::Component ActiveChild() override final { return overlay_; } + ftxui::Component ActiveChild() override final + { + if (overlay_) + return overlay_; + return table_; + } public: explicit history_(std::shared_ptr&& wallet, std::uint32_t account) : ftxui::ComponentBase(), wallet_(std::move(wallet)), + table_(), overlay_(nullptr), title_(nullptr), - table_(nullptr), - base_rows(), - highlighted_(min_row() - 1), + table_cached_(nullptr), row_map_(), account_(account) { @@ -281,10 +285,22 @@ namespace lwcli { namespace view title_ = ftxui::text(std::move(address)); // perform transalation lookup once - base_rows = { + table_ = component::table( {_("Date"), _("Amount"), _("Payment ID"), _("Desription"), _("Block"), _("Fee"), _("Hash")}, - { "", "", "", "", "", "", "" } - }; + [this] () { return transaction_list(); }, + [this] (std::size_t i) { return add_overlay(i); } + ); + + Add(table_); + } + + bool add_overlay(const std::size_t i) + { + if (overlay_) + overlay_->Detach(); + overlay_ = std::make_shared(wallet_->history(), row_map_.at(i)); + Add(overlay_); + return true; } bool OnEvent(ftxui::Event event) override final @@ -300,19 +316,8 @@ namespace lwcli { namespace view throw event::close{}; else if (event.is_character()) return false; - else if (event == ftxui::Event::ArrowDown && highlighted_ < row_map_.size() + min_row() - 1) - ++highlighted_; - else if (event == ftxui::Event::ArrowUp && min_row() <= highlighted_) - --highlighted_; - else if (event == ftxui::Event::PageDown) - highlighted_ = std::min(std::ptrdiff_t(row_map_.size() + min_row()), highlighted_ + 10); - else if (event == ftxui::Event::PageUp) - highlighted_ = std::max(std::ptrdiff_t(min_row()), highlighted_ - 10); - else if (event == ftxui::Event::Return) - { - overlay_ = ftxui::Make(wallet_->history(), row_map_.at(highlighted_ - min_row())); - Add(overlay_); - } + else + return table_->OnEvent(std::move(event)); } catch (const event::close&) { @@ -321,121 +326,101 @@ namespace lwcli { namespace view overlay_->Detach(); overlay_.reset(); } - return min_row() <= highlighted_ && highlighted_ - min_row() < row_map_.size(); + return true; + } + + 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); + } + + balance_ = 0; + std::vector> rows; + rows.reserve(ordered.size() + 2); + + std::size_t i = 0; + 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 int direction = tx->direction(); + const std::uint64_t fee = tx->fee(); + + + if (!tx->isFailed()) + { + if (direction == Monero::TransactionInfo::Direction_Out) + { + balance_ -= amount; + balance_ -= fee; + } + else + balance_ += amount; + } + + 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"}; + + std::string payment_id = tx->paymentId(); + if (payment_id.size() == 16 && payment_id.find_first_not_of('0') == std::string::npos) + payment_id.clear(); + + char const* const extended_payment_id = 16 < payment_id.size() ? + "..." : ""; + rows.push_back({ + std::string{date}, + print_amount(amount, direction), + payment_id.substr(0, 16) + extended_payment_id, + tx->description(), + std::to_string(tx->blockHeight()), + print_money(tx->fee()), + tx->hash().substr(0, 16) + "..." + }); + + ++i; + } + + return rows; } ftxui::Element OnRender() override final { - if (Focused()) - { - if (highlighted_ < min_row()) - highlighted_ = min_row(); - else if (row_map_.size() <= highlighted_ - min_row()) - highlighted_ = row_map_.size() - 1 + min_row(); - } - /* Do not redraw table when showing tx. the history()->refresh() call can invalidate pointers */ if (!overlay_) { - 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::uint64_t balance = 0; - std::vector> rows; - rows.reserve(ordered.size() + 2); - rows = base_rows; - - std::size_t i = 0; - 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 int direction = tx->direction(); - const std::uint64_t fee = tx->fee(); - - - if (!tx->isFailed()) - { - if (direction == Monero::TransactionInfo::Direction_Out) - { - balance -= amount; - balance -= fee; - } - else - balance += amount; - } - - 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"}; - - - std::string payment_id = tx->paymentId(); - if (payment_id.size() == 16 && payment_id.find_first_not_of('0') == std::string::npos) - payment_id.clear(); - - char const* const extended_payment_id = 16 < payment_id.size() ? - "..." : ""; - rows.push_back({ - std::string{date}, - print_amount(amount, direction), - payment_id.substr(0, 16) + extended_payment_id, - tx->description(), - std::to_string(tx->blockHeight()), - print_money(tx->fee()), - tx->hash().substr(0, 16) + "..." - }); - - ++i; - } - - ftxui::Table table{std::move(rows)}; - table.SelectRow(0).Decorate(ftxui::bold); - table.SelectRow(0).SeparatorVertical(ftxui::LIGHT); - table.SelectRow(0).Border(ftxui::LIGHT); - //table.SelectColumn(3).DecorateCells(ftxui::xflex_grow); - if (min_row() <= highlighted_ && highlighted_ - min_row() < row_map_.size()) - { - auto row = table.SelectRow(highlighted_); - row.Decorate(ftxui::inverted); - if (Focused()) - row.Decorate(ftxui::focus); - else if (Active()) - row.Decorate(ftxui::select); - } - - table_ = ftxui::vbox({ + auto table = table_->Render(); // compute balance first in callback + table_cached_ = ftxui::vbox({ title_, - ftxui::text(_("Balance: ") + print_money(balance)), - table.Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center | ftxui::flex - //ftxui::flex(ftxui::yframe(ftxui::vscroll_indicator(table.Render()))) + ftxui::text(_("Balance: ") + print_money(balance_)), + std::move(table) | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center | ftxui::flex }); - return table_; + return table_cached_; } - return ftxui::dbox(table_, decorate::overlay(overlay_->Render())); + return ftxui::dbox(table_cached_, decorate::overlay(overlay_->Render())); } }; } // anonymous diff --git a/src/views/settings.cpp b/src/views/settings.cpp index 7da8f4c..30cd97a 100644 --- a/src/views/settings.cpp +++ b/src/views/settings.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include "decorate/overlay.h" @@ -73,8 +72,7 @@ namespace lwcli { namespace view const auto is_ssl = from_string(ssl); if (!is_ssl) return false; - - // no way to forcibly select ssl without another init call. hopefully doesn't break things + wal.init(wal.getCacheAttribute(std::string{config::server::url}), 0, "", "", bool(*is_ssl), true, wal.getCacheAttribute(std::string{config::server::proxy})); return true; } @@ -251,6 +249,8 @@ namespace lwcli { namespace view ftxui::Component settings(std::shared_ptr wal) { + if (!wal) + throw std::invalid_argument{"view::settings cannot be given nullptr"}; return std::make_shared(std::move(wal)); } diff --git a/src/views/wallet.cpp b/src/views/wallet.cpp index a19869a..30779c6 100644 --- a/src/views/wallet.cpp +++ b/src/views/wallet.cpp @@ -39,6 +39,7 @@ #include "events.h" #include "lwcli_config.h" #include "translate.h" +#include "views/accounts.h" #include "views/history.h" #include "views/settings.h" @@ -52,6 +53,7 @@ namespace lwcli { namespace view { const std::shared_ptr wal; ftxui::Component overlay; + std::uint32_t selected_account = 0; }; ftxui::Component menu_bar(wallet_state* state) @@ -60,7 +62,7 @@ namespace lwcli { namespace view return ftxui::Container::Horizontal({ ftxui::Button("[s]end", [] () {}, ascii()), ftxui::Button("[b]ook", [] () {}, ascii()), - ftxui::Button("[a]ccounts", [] () {}, ascii()), + ftxui::Button("[a]ccounts", [state] () { state->overlay = accounts(state->wal, &state->selected_account); }, ascii()), ftxui::Button("[r]efresh", [wal] () { wal->refreshAsync(); }, ascii()), ftxui::Button("s[e]ttings", [state] () { state->overlay = settings(state->wal); }, ascii()) }); @@ -71,7 +73,6 @@ namespace lwcli { namespace view wallet_state state_; ftxui::Element title_; std::uint32_t active_account_; - std::uint32_t selected_account_; ftxui::Component bar_; ftxui::Component ui_; ftxui::Component history_; @@ -112,7 +113,6 @@ namespace lwcli { namespace view state_{std::move(data)}, title_(nullptr), active_account_(-1), - selected_account_(0), bar_(), ui_(), history_(nullptr) @@ -132,15 +132,15 @@ namespace lwcli { namespace view void update_account() { - if (active_account_ != selected_account_) + if (active_account_ != state_.selected_account) { if (ui_) ui_->Detach(); - history_ = view::history(state_.wal, selected_account_); + history_ = view::history(state_.wal, state_.selected_account); ui_ = ftxui::Container::Vertical({bar_, history_}); Add(ui_); } - active_account_ = selected_account_; + active_account_ = state_.selected_account; } bool OnEvent(ftxui::Event event) override final @@ -162,7 +162,7 @@ namespace lwcli { namespace view else if (event == ftxui::Event::b || event == ftxui::Event::B) ; // state_.overlay = book(state_.wal); else if (event == ftxui::Event::a || event == ftxui::Event::a) - ; // overlay = accounts(state_.wal, &active_account_); + state_.overlay = accounts(state_.wal, &state_.selected_account); else if (event == ftxui::Event::r || event == ftxui::Event::R) state_.wal->refreshAsync(); else if (event == ftxui::Event::e || event == ftxui::Event::E)