From 0633f571ec71b91d1a9f70ac3d17e0f2601b7fd7 Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Sun, 6 Apr 2025 21:18:36 -0400 Subject: [PATCH] Add CLI flags, updated UI loading routine, and moved files around --- src/views/CMakeLists.txt | 4 +- src/views/history.cpp | 94 ++++++++++------ src/views/manager.cpp | 179 +++++++++++++++++------------ src/views/manager.h | 3 +- src/views/settings.cpp | 236 +++++++++++++++++++++++++++++++++++++++ src/views/settings.h | 38 +++++++ src/views/wallet.cpp | 215 +++-------------------------------- 7 files changed, 462 insertions(+), 307 deletions(-) create mode 100644 src/views/settings.cpp create mode 100644 src/views/settings.h diff --git a/src/views/CMakeLists.txt b/src/views/CMakeLists.txt index 29d68e9..3a13787 100644 --- a/src/views/CMakeLists.txt +++ b/src/views/CMakeLists.txt @@ -26,8 +26,8 @@ # 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 wallet.cpp) -set(lwscli-views_headers history.h keys.h manager.h wallet.h) +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) add_library(lwcli-views ${lwcli-views_sources} ${lwcli-views_headers}) target_link_libraries(lwcli-views PRIVATE component dom lwcli-decorate lwsf-api) diff --git a/src/views/history.cpp b/src/views/history.cpp index 5e2c811..6ff79cb 100644 --- a/src/views/history.cpp +++ b/src/views/history.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include #include "decorate/overlay.h" #include "events.h" @@ -130,13 +132,13 @@ namespace lwcli { namespace view virtual ftxui::Element OnRender() override final { std::vector grid{ - {ftxui::text("Description: "), note_input_->Render()}, + {ftxui::text(_("Description: ")), note_input_->Render()}, timestamp_, payment_id_, - {ftxui::text("Confirmations: "), ftxui::text(std::to_string(info_->confirmations()))}, + {ftxui::text(_("Confirmations: ")), ftxui::text(std::to_string(info_->confirmations()))}, amount_, fee_, - {ftxui::text("Block Height: "), ftxui::text(std::to_string(info_->blockHeight()))}, + {ftxui::text(_("Block Height: ")), ftxui::text(std::to_string(info_->blockHeight()))}, minors_, coinbase_ }; @@ -148,7 +150,7 @@ namespace lwcli { namespace view std::string{_(" to Unknown Address")} : _(" to ") + transfer.address; std::string text; if (base_size == grid.size()) - text = "Transfers: "; + text = _("Transfers: "); grid.push_back({ ftxui::text(std::move(text)), ftxui::text(print_money(transfer.amount) + address) @@ -156,15 +158,17 @@ namespace lwcli { namespace view } ftxui::Elements vertical; - vertical.reserve(3); + vertical.reserve(4); + vertical.push_back(ftxui::hcenter(ftxui::hbox(cancel_->Render(), ok_->Render()))); 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()) - vertical.push_back(ftxui::inverted(ftxui::hcenter(ftxui::text("PENDING")))); + vertical.push_back(ftxui::inverted(ftxui::hcenter(ftxui::text(_("PENDING"))))); + else + vertical.push_back(ftxui::separator()); vertical.push_back(ftxui::gridbox(std::move(grid))); - vertical.push_back(ftxui::hcenter(ftxui::hbox(cancel_->Render(), ok_->Render()))); return ftxui::window(title_, ftxui::vbox(std::move(vertical))); } @@ -231,8 +235,8 @@ namespace lwcli { namespace view }, ftxui::ButtonOption::Ascii()); container_ = ftxui::Container::Vertical({ - note_input_, - ftxui::Container::Horizontal({cancel_, ok_}) + ftxui::Container::Horizontal({cancel_, ok_}), + note_input_ }); } }; @@ -245,7 +249,7 @@ namespace lwcli { namespace view ftxui::Element table_; //!< Does not redraw when child is enabled std::vector> base_rows; std::ptrdiff_t highlighted_; - std::size_t row_count_; + std::unordered_map row_map_; const std::uint32_t account_; static constexpr std::size_t min_row() noexcept { return 2; }; @@ -261,7 +265,7 @@ namespace lwcli { namespace view table_(nullptr), base_rows(), highlighted_(min_row() - 1), - row_count_(0), + row_map_(), account_(account) { Monero::Subaddress* subaddress = wallet_->subaddress(); @@ -294,17 +298,19 @@ namespace lwcli { namespace view } else if (event == ftxui::Event::CtrlQ) throw event::close{}; - else if (event == ftxui::Event::ArrowDown && highlighted_ + 1 < row_count_) + 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_count_), highlighted_ + 10); + 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_count_ - highlighted_ - 1); + overlay_ = ftxui::Make(wallet_->history(), row_map_.at(highlighted_ - min_row())); Add(overlay_); } } @@ -315,7 +321,7 @@ namespace lwcli { namespace view overlay_->Detach(); overlay_.reset(); } - return min_row() <= highlighted_ && highlighted_ < row_count_; + return min_row() <= highlighted_ && highlighted_ - min_row() < row_map_.size(); } ftxui::Element OnRender() override final @@ -324,8 +330,8 @@ namespace lwcli { namespace view { if (highlighted_ < min_row()) highlighted_ = min_row(); - else if (row_count_ <= highlighted_) - highlighted_ = row_count_ - 1; + else if (row_map_.size() <= highlighted_ - min_row()) + highlighted_ = row_map_.size() - 1 + min_row(); } /* Do not redraw table when showing tx. the @@ -335,33 +341,47 @@ namespace lwcli { namespace view 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(); - std::vector> rows = base_rows; - - std::uint64_t balance = 0; - const auto history = tx_history->getAll(); - for (auto it = history.rbegin(); it != history.rend(); ++it) + for (std::size_t i = 0; i < history.size(); ++i) { - const Monero::TransactionInfo* tx = *it; + const Monero::TransactionInfo* tx = history[i]; if (!tx) throw std::runtime_error{"unexpected tx_info nullptr"}; - if (tx->subaddrAccount() != account_) - continue; + 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 (direction == Monero::TransactionInfo::Direction_Out) + + if (!tx->isFailed()) { - balance -= amount; - balance -= fee; + if (direction == Monero::TransactionInfo::Direction_Out) + { + balance -= amount; + balance -= fee; + } + else + balance += amount; } - else - balance += amount; std::tm expanded{}; const std::time_t timestamp = tx->timestamp(); @@ -386,15 +406,16 @@ namespace lwcli { namespace view print_money(tx->fee()), tx->hash().substr(0, 16) }); + + ++i; } - row_count_ = rows.size(); 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_ < row_count_) + //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); @@ -407,7 +428,8 @@ namespace lwcli { namespace view table_ = ftxui::vbox({ title_, ftxui::text(_("Balance: ") + print_money(balance)), - ftxui::yframe(ftxui::vscroll_indicator(table.Render())) + table.Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center | ftxui::flex + //ftxui::flex(ftxui::yframe(ftxui::vscroll_indicator(table.Render()))) }); return table_; } diff --git a/src/views/manager.cpp b/src/views/manager.cpp index ec5d02f..12fdfff 100644 --- a/src/views/manager.cpp +++ b/src/views/manager.cpp @@ -58,11 +58,21 @@ namespace lwcli { namespace view } }; - ftxui::Component password(std::string* pass) + std::string get_home() + { + const char* home = std::getenv("HOME"); + if (home) + return home; + return {}; + } + + ftxui::Component password(std::string* pass, std::function on_enter = nullptr) { auto opt = ftxui::InputOption::Default(); opt.password = true; opt.multiline = false; + if (on_enter) + opt.on_enter = std::move(on_enter); return ftxui::Input(pass, std::move(opt)); } @@ -94,13 +104,9 @@ namespace lwcli { namespace view std::string file; std::string password; - wallet_base() - : file(), password() - { - const char* home = std::getenv("HOME"); - if (home) - file = home; - } + wallet_base(std::string default_file) + : file(std::move(default_file)), password() + {} }; struct new_wallet : wallet_base @@ -111,8 +117,8 @@ namespace lwcli { namespace view std::string proxy; bool ssl; - new_wallet() - : wallet_base(), confirm(), language(config::default_language), server(config::server::default_url), proxy(), ssl(false) + new_wallet(std::string default_file) + : wallet_base(std::move(default_file)), confirm(), language(config::default_language), server(config::server::default_url), proxy(), ssl(false) {} void setup(Monero::Wallet& wal) @@ -136,7 +142,7 @@ namespace lwcli { namespace view {} }; - std::optional from_string(const std::string& str) noexcept + std::optional from_string(const std::string_view str) noexcept { std::uint64_t height = 0; const auto end = str.data() + str.size(); @@ -159,57 +165,63 @@ namespace lwcli { namespace view if (!wal.init(wal.getCacheAttribute(std::string{config::server::url}), 0, "", "", *ssl, true, wal.getCacheAttribute(std::string{config::server::proxy}))) { - *error = wal.errorString(); + *error = "Failure to initialize" + wal.errorString(); return false; } return true; } - std::pair>, ftxui::Component> get_load_options(start_state* state) + using option_set = std::pair>, ftxui::Component>; + + option_set get_load_options(std::string default_file, start_state* state) { struct options { start_state* state; wallet_base config; - options(start_state* state) - : state(state), config() + options(std::string default_file, start_state* state) + : state(state), config(std::move(default_file)) {} }; - auto enclosed = std::make_shared(state); - auto load = ftxui::Button("Load", [enclosed] () { + auto enclosed = std::make_shared(std::move(default_file), state); + const auto load_action = [enclosed] () { auto prepped = prep_wallet( enclosed->state->wm, - enclosed->state->wm->openWallet(enclosed->config.file, enclosed->config.password, Monero::TESTNET), + enclosed->state->wm->openWallet(enclosed->config.file, enclosed->config.password, config::network), &enclosed->state->error ); if (prepped) { - enclosed->config.password.clear(); - init_wallet(*prepped, &enclosed->state->error); - enclosed->state->wal = prepped; + if (init_wallet(*prepped, &enclosed->state->error)) + { + enclosed->config.password.clear(); + prepped->startRefresh(); + enclosed->state->wal = prepped; + } } - }, ftxui::ButtonOption::Ascii()); + }; + return { { {ftxui::text(_("Filename: ")), last_input(&enclosed->config.file)}, - {ftxui::text(_("Password: ")), password(&enclosed->config.password)} + {ftxui::text(_("Password: ")), password(&enclosed->config.password, load_action)} }, - load + ftxui::Button(_("Load"), load_action, ftxui::ButtonOption::Ascii()) }; } - std::pair>, ftxui::Component> get_create_options(start_state* state) + option_set get_create_options(std::string default_file, start_state* state) { struct options { start_state* state; new_wallet config; - options(start_state* state) - : state(state), config() + options(std::string default_file, start_state* state) + : state(state), config(std::move(default_file)) {} }; - auto enclosed = std::make_shared(state); + auto enclosed = std::make_shared(std::move(default_file), state); auto create = ftxui::Button("Create", [enclosed] () { if (!enclosed->config.file.empty()) { @@ -223,19 +235,21 @@ namespace lwcli { namespace view { auto prepped = prep_wallet( enclosed->state->wm, - enclosed->state->wm->createWallet(enclosed->config.file, enclosed->config.password, enclosed->config.language, Monero::TESTNET), + enclosed->state->wm->createWallet(enclosed->config.file, enclosed->config.password, enclosed->config.language, config::network), &enclosed->state->error ); if (prepped) { if (prepped->store({})) { - enclosed->config.password.clear(); - enclosed->config.confirm.clear(); enclosed->config.setup(*prepped); - init_wallet(*prepped, &enclosed->state->error); - enclosed->state->overlay = view::keys(prepped, true /* show warning */); - enclosed->state->wal = prepped; + if (init_wallet(*prepped, &enclosed->state->error)) + { + enclosed->config.password.clear(); + enclosed->config.confirm.clear(); + enclosed->state->overlay = view::keys(prepped, true /* show warning */); + enclosed->state->wal = prepped; + } } else enclosed->state->error = "Unable to create file: " + prepped->errorString(); @@ -253,14 +267,14 @@ namespace lwcli { namespace view {ftxui::text(_("Password: ")), password(&enclosed->config.password)}, {ftxui::text(_("Confirm: ")), password(&enclosed->config.confirm)}, {ftxui::text(_("Language: ")), last_input(&enclosed->config.language)}, - {ftxui::text(_("LWS Server: ")), last_input(&enclosed->config.server)}, + {ftxui::text(_("API Server: ")), last_input(&enclosed->config.server)}, {ftxui::text(_("Proxy: ")), last_input(&enclosed->config.proxy)}, - {ftxui::text(_("Server Options: ")), ftxui::Checkbox(_("SSL"), &enclosed->config.ssl)} + {ftxui::text(_("Options: ")), ftxui::Checkbox(_("TLS/SSL Cert Check"), &enclosed->config.ssl)} }, create }; } - std::pair>, ftxui::Component> get_seed_options(start_state* state) + option_set get_seed_options(std::string default_file, start_state* state) { struct options { @@ -269,11 +283,11 @@ namespace lwcli { namespace view std::string height; new_wallet config; - options(start_state* state) - : state(state), mnemonic(), height("0"), config() + options(std::string default_file, start_state* state) + : state(state), mnemonic(), height("0"), config(std::move(default_file)) {} }; - auto enclosed = std::make_shared(state); + auto enclosed = std::make_shared(std::move(default_file), state); auto recover = ftxui::Button("Recover", [enclosed] () { const auto height = from_string(enclosed->height); if (!height) @@ -294,18 +308,22 @@ namespace lwcli { namespace view { auto prepped = prep_wallet( enclosed->state->wm, - enclosed->state->wm->recoveryWallet(enclosed->config.file, enclosed->config.password, enclosed->mnemonic, Monero::TESTNET, *height), + enclosed->state->wm->recoveryWallet(enclosed->config.file, enclosed->config.password, enclosed->mnemonic, config::network, *height), &enclosed->state->error ); if (prepped) { if (prepped->store({})) { - enclosed->config.password.clear(); - enclosed->config.confirm.clear(); enclosed->config.setup(*prepped); - init_wallet(*prepped, &enclosed->state->error); - enclosed->state->wal = prepped; + if (init_wallet(*prepped, &enclosed->state->error)) + { + enclosed->mnemonic.clear(); + enclosed->config.password.clear(); + enclosed->config.confirm.clear(); + prepped->rescanBlockchainAsync(); + enclosed->state->wal = prepped; + } } else enclosed->state->error = "Unable to create file: " + prepped->errorString(); @@ -324,15 +342,15 @@ namespace lwcli { namespace view {ftxui::text(_("Confirm: ")), password(&enclosed->config.confirm)}, {ftxui::text(_("Mnemonic: ")), last_input(&enclosed->mnemonic)}, {ftxui::text(_("Height: ")), last_input(&enclosed->height)}, - {ftxui::text(_("LWS Server: ")), last_input(&enclosed->config.server)}, + {ftxui::text(_("API Server: ")), last_input(&enclosed->config.server)}, {ftxui::text(_("Proxy: ")), last_input(&enclosed->config.proxy)}, - {ftxui::text(_("Server Options: ")), ftxui::Checkbox(_("SSL"), &enclosed->config.ssl)} + {ftxui::text(_("Options: ")), ftxui::Checkbox(_("TLS/SSL Cert Check"), &enclosed->config.ssl)} }, recover }; } - std::pair>, ftxui::Component> get_key_options(start_state* state) + option_set get_key_options(std::string default_file, start_state* state) { struct options { @@ -341,12 +359,12 @@ namespace lwcli { namespace view std::string height; new_wallet config; - options(start_state* state) - : state(state), spend_key(), height("0"), config() + options(std::string default_file, start_state* state) + : state(state), spend_key(), height("0"), config(std::move(default_file)) {} }; - auto enclosed = std::make_shared(state); - auto recover = ftxui::Button("Recover (Broken)", [enclosed] () { + auto enclosed = std::make_shared(std::move(default_file), state); + auto recover = ftxui::Button(_("Recover (Broken)"), [enclosed] () { const auto height = from_string(enclosed->height); if (!height) { @@ -370,7 +388,7 @@ namespace lwcli { namespace view enclosed->config.file, enclosed->config.password, enclosed->config.language, - Monero::TESTNET, + config::network, *height, "address", "view_key", @@ -382,11 +400,15 @@ namespace lwcli { namespace view { if (prepped->store({})) { - enclosed->config.password.clear(); - enclosed->config.confirm.clear(); enclosed->config.setup(*prepped); - init_wallet(*prepped, &enclosed->state->error); - enclosed->state->wal = prepped; + if (init_wallet(*prepped, &enclosed->state->error)) + { + enclosed->spend_key.clear(); + enclosed->config.password.clear(); + enclosed->config.confirm.clear(); + prepped->rescanBlockchainAsync(); + enclosed->state->wal = prepped; + } } else enclosed->state->error = "Unable to create file: " + prepped->errorString(); @@ -406,9 +428,9 @@ namespace lwcli { namespace view {ftxui::text(_("Spend key: ")), last_input(&enclosed->spend_key)}, {ftxui::text(_("Height: ")), last_input(&enclosed->height)}, {ftxui::text(_("Language: ")), last_input(&enclosed->config.language)}, - {ftxui::text(_("LWS Server: ")), last_input(&enclosed->config.server)}, + {ftxui::text(_("API Server: ")), last_input(&enclosed->config.server)}, {ftxui::text(_("Proxy: ")), last_input(&enclosed->config.proxy)}, - {ftxui::text(_("Connection Options: ")), ftxui::Checkbox(_("SSL"), &enclosed->config.ssl)} + {ftxui::text(_("Options: ")), ftxui::Checkbox(_("TLS/SSL Cert Check"), &enclosed->config.ssl)} }, recover }; @@ -421,6 +443,7 @@ namespace lwcli { namespace view const ftxui::Element help_; const ftxui::Element disclaimer_; start_state state_; + const std::string default_file_; const std::vector options_; int active_; int selected_; @@ -437,16 +460,24 @@ namespace lwcli { namespace view return ui_; } + static int get_selected(const std::string& file) + { + // default to load file if exists, and create if does not exist + std::error_code ec{}; + return file.empty() || std::filesystem::exists(file, ec) ? 0 : 1; + } + public: - explicit start(std::shared_ptr&& wm, std::shared_ptr* out) + explicit start(std::shared_ptr&& wm, std::string&& file, std::shared_ptr* out) : out_(out), title_(ftxui::text("wmcli")), help_(decorate::banner(ftxui::text(_("Ctrl-Q to close active window, Ctrl-C close app immediately")))), disclaimer_(decorate::banner(ftxui::text(_("Beware of mouse events in Tmux/Screen")))), state_(std::move(wm)), + default_file_(std::move(file)), options_({_("Load Wallet"), _("Create Wallet"), _("Recover from Seed"), _("Recover from Keys")}), active_(-1), - selected_(0), + selected_(get_selected(default_file_)), mode_(ftxui::Dropdown(&options_, &selected_)), completion_(), stack_(), @@ -459,21 +490,22 @@ namespace lwcli { namespace view { if (active_ != selected_) { + const bool first_load = active_ == -1; active_ = selected_; switch(active_) { case 0: default: - std::tie(stack_, completion_) = get_load_options(&state_); + std::tie(stack_, completion_) = get_load_options((default_file_.empty() ? get_home() : default_file_), &state_); break; case 1: - std::tie(stack_, completion_) = get_create_options(&state_); + std::tie(stack_, completion_) = get_create_options((default_file_.empty() ? get_home() : default_file_), &state_); break; case 2: - std::tie(stack_, completion_) = get_seed_options(&state_); + std::tie(stack_, completion_) = get_seed_options((default_file_.empty() ? get_home() : default_file_), &state_); break; case 3: - std::tie(stack_, completion_) = get_key_options(&state_); + std::tie(stack_, completion_) = get_key_options((default_file_.empty() ? get_home() : default_file_), &state_); break; } @@ -485,6 +517,15 @@ namespace lwcli { namespace view ui.push_back(completion_); ui_ = ftxui::Container::Vertical(std::move(ui)); + + // skip directly to password on init if default file given + if (first_load) + { + if (!default_file_.empty()) + stack_.at(1).second->TakeFocus(); // jump to password field (load or create wallet) + else + stack_.at(0).second->TakeFocus(); // jump tp file (load wallet) + } } // Delay showing wallet if options required overlay @@ -566,10 +607,10 @@ namespace lwcli { namespace view } public: - explicit manager_(std::shared_ptr&& wm) + explicit manager_(std::shared_ptr&& wm, std::string&& file) : ftxui::ComponentBase(), data_(nullptr), - start_(std::make_shared(std::move(wm), &data_)), + start_(std::make_shared(std::move(wm), std::move(file), &data_)), wallet_(nullptr) {} @@ -602,10 +643,10 @@ namespace lwcli { namespace view }; } - ftxui::Component manager(std::shared_ptr wm) + ftxui::Component manager(std::shared_ptr wm, std::string&& file) { if (!wm) throw std::runtime_error{"lwcli::view::manaager given nullptr"}; - return std::make_shared(std::move(wm)); + return std::make_shared(std::move(wm), std::move(file)); } }} // lwcli // view diff --git a/src/views/manager.h b/src/views/manager.h index f734854..66f11c8 100644 --- a/src/views/manager.h +++ b/src/views/manager.h @@ -29,10 +29,11 @@ #include #include +#include namespace Monero { class WalletManager; } namespace lwcli { namespace view { - ftxui::Component manager(std::shared_ptr wm); + ftxui::Component manager(std::shared_ptr wm, std::string&& file); }} // lwscli // view diff --git a/src/views/settings.cpp b/src/views/settings.cpp new file mode 100644 index 0000000..6e90191 --- /dev/null +++ b/src/views/settings.cpp @@ -0,0 +1,236 @@ +// 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 + +#include "decorate/overlay.h" +#include "events.h" +#include "lwcli_config.h" +#include "translate.h" +#include "views/history.h" + + +namespace lwcli { namespace view +{ + namespace + { + std::optional from_string(const std::string_view str) noexcept + { + std::uint64_t out = 0; + const auto end = str.data() + str.size(); + auto [ptr, ec] = std::from_chars(str.data(), end, out); + if (bool(ec) || ptr != end) + return std::nullopt; + return out; + } + + bool set_proxy(Monero::Wallet& wal, const std::string& proxy) + { + wal.setProxy(proxy); + return true; + } + + bool set_url(Monero::Wallet& wal, const std::string& url) + { + const bool is_ssl = bool(from_string(wal.getCacheAttribute(std::string{config::server::ssl}))); + wal.init(url, 0, "", "", is_ssl, true, wal.getCacheAttribute(std::string{config::server::proxy})); + return true; + } + + bool set_refresh(Monero::Wallet& wal, const std::string& interval) + { + const auto number = from_string(interval); + if (!number) + return false; + wal.setAutoRefreshInterval(std::chrono::milliseconds{std::chrono::seconds{*number}}.count()); + return true; + } + + bool set_ssl(Monero::Wallet& wal, const std::string& ssl) + { + 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; + } + + struct option + { + using updater = bool(Monero::Wallet&, const std::string&); + const std::string_view path; + const std::string_view description; + updater* const update; + }; + + const std::array options{{ + {config::server::proxy, _("Proxy"), set_proxy}, + {config::server::refresh_interval, _("Refresh Interval (seconds)"), set_refresh}, + {config::server::ssl, _("TLS/SSL Cert Check"), set_ssl}, + {config::server::url, _("API Server"), set_url} + }}; + + 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(); } + + struct option_state + { + ftxui::Element description; + std::string original; + std::string value; + ftxui::Component ui; + }; + + struct configuration + { + std::array states; + + configuration(const Monero::Wallet& wal) + : states() + { + for (std::size_t i = 0; i < options.size(); ++i) + { + states[i].description = ftxui::text(std::string{options[i].description} + ": "); + states[i].original = wal.getCacheAttribute(std::string{options[i].path}); + states[i].value = states[i].original; + states[i].ui = last_input(&states[i].value); + } + } + + void store(Monero::Wallet& wal, std::string& error) + { + for (std::size_t i = 0; i < options.size(); ++i) + { + if (states[i].original != states[i].value) + { + if (!options[i].update || options[i].update(wal, states[i].value)) + wal.setCacheAttribute(std::string{options[i].path}, states[i].value); + else + { + error = std::string{options[i].description} + " is invalid"; + break; + } + } + } + } + }; + + class settings_ final : public ftxui::ComponentBase + { + const std::shared_ptr wal_; + const ftxui::Element title_; + const std::unique_ptr config_; + std::string error_; + ftxui::Component buttons_; + ftxui::Component ui_; + + bool Focusable() const override final { return true; } + ftxui::Component ActiveChild() override final { return ui_; } + + public: + explicit settings_(std::shared_ptr&& wal) + : ftxui::ComponentBase(), + wal_(std::move(wal)), + title_(ftxui::text(_("Settings"))), + config_(std::make_unique(*wal_)), + error_(), + buttons_(), + ui_() + { + buttons_ = ftxui::Container::Horizontal({ + ftxui::Button(_("Cancel"), [] () { throw event::close{}; }, ascii()), + ftxui::Button(_("Save"), [this] () { + error_.clear(); + config_->store(*wal_, error_); + if (error_.empty()) + throw event::close{}; + }, ascii()) + }); + + ftxui::Components ui; + ui.reserve(config_->states.size() + 1); + ui.push_back(buttons_); + for (const auto& opt : config_->states) + ui.push_back(opt.ui); + + ui_ = ftxui::Container::Vertical(std::move(ui)); + Add(ui_); + } + + bool OnEvent(ftxui::Event event) override final + { + if (event == ftxui::Event::CtrlQ) + throw event::close{}; + ui_->OnEvent(std::move(event)); + return true; + } + + ftxui::Element OnRender() override final + { + const auto min_size = ftxui::size(ftxui::WIDTH, ftxui::GREATER_THAN, 5); + std::vector grid; + grid.reserve(config_->states.size()); + for (const auto& opt : config_->states) + grid.push_back({opt.description, min_size(opt.ui->Render())}); + + ftxui::Element highlighted; + if (error_.empty()) + highlighted = ftxui::separator(); + else + highlighted = ftxui::inverted(ftxui::text(error_)); + + return ftxui::window(title_, ftxui::vbox({ + ftxui::hcenter(buttons_->Render()), + highlighted, + ftxui::gridbox(std::move(grid)) + })); + } + }; + } + + ftxui::Component settings(std::shared_ptr wal) + { + return std::make_shared(std::move(wal)); + } + +}} // lwcli // diff --git a/src/views/settings.h b/src/views/settings.h new file mode 100644 index 0000000..3c171f1 --- /dev/null +++ b/src/views/settings.h @@ -0,0 +1,38 @@ +// 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 + +namespace Monero { class Wallet; } +namespace lwcli { namespace view +{ + ftxui::Component settings(std::shared_ptr wallet); +}} // lwscli // view + diff --git a/src/views/wallet.cpp b/src/views/wallet.cpp index 47267e2..efcbbce 100644 --- a/src/views/wallet.cpp +++ b/src/views/wallet.cpp @@ -39,198 +39,14 @@ #include "lwcli_config.h" #include "translate.h" #include "views/history.h" +#include "views/settings.h" namespace lwcli { namespace view { namespace { - std::optional from_string(const std::string_view str) noexcept - { - std::uint64_t out = 0; - const auto end = str.data() + str.size(); - auto [ptr, ec] = std::from_chars(str.data(), end, out); - if (bool(ec) || ptr != end) - return std::nullopt; - return out; - } - - bool set_proxy(Monero::Wallet& wal, const std::string& proxy) - { - wal.setProxy(proxy); - return true; - } - - bool set_url(Monero::Wallet& wal, const std::string& url) - { - const bool is_ssl = bool(from_string(wal.getCacheAttribute(std::string{config::server::ssl}))); - wal.init(url, 0, "", "", is_ssl, true, wal.getCacheAttribute(std::string{config::server::proxy})); - return true; - } - - bool set_refresh(Monero::Wallet& wal, const std::string& interval) - { - const auto number = from_string(interval); - if (!number) - return false; - wal.setAutoRefreshInterval(std::chrono::milliseconds{std::chrono::seconds{*number}}.count()); - return true; - } - - bool set_ssl(Monero::Wallet& wal, const std::string& ssl) - { - 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; - } - - struct option - { - using updater = bool(Monero::Wallet&, const std::string&); - const std::string_view path; - const std::string_view description; - updater* const update; - }; - - const std::array options{{ - {config::server::proxy, _("LWS Proxy"), set_proxy}, - {config::server::refresh_interval, _("LWS Refresh Interval (seconds)"), set_refresh}, - {config::server::ssl, _("LWS SSL Verify"), set_ssl}, - {config::server::url, _("LWS URL"), set_url} - }}; - - 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(); } - struct option_state - { - ftxui::Element description; - std::string original; - std::string value; - ftxui::Component ui; - }; - - struct configuration - { - std::array states; - - configuration(const Monero::Wallet& wal) - : states() - { - for (std::size_t i = 0; i < options.size(); ++i) - { - states[i].description = ftxui::text(std::string{options[i].description} + ": "); - states[i].original = wal.getCacheAttribute(std::string{options[i].path}); - states[i].value = states[i].original; - states[i].ui = last_input(&states[i].value); - } - } - - void store(Monero::Wallet& wal, std::string& error) - { - for (std::size_t i = 0; i < options.size(); ++i) - { - if (states[i].original != states[i].value) - { - if (!options[i].update || options[i].update(wal, states[i].value)) - wal.setCacheAttribute(std::string{options[i].path}, states[i].value); - else - { - error = std::string{options[i].description} + " is invalid"; - break; - } - } - } - } - }; - - class settings_ final : public ftxui::ComponentBase - { - const std::shared_ptr wal_; - const ftxui::Element title_; - const std::unique_ptr config_; - std::string error_; - ftxui::Component buttons_; - ftxui::Component ui_; - - bool Focusable() const override final { return true; } - ftxui::Component ActiveChild() override final { return ui_; } - - public: - explicit settings_(std::shared_ptr&& wal) - : ftxui::ComponentBase(), - wal_(std::move(wal)), - title_(ftxui::text(_("Settings"))), - config_(std::make_unique(*wal_)), - error_(), - buttons_(), - ui_() - { - buttons_ = ftxui::Container::Horizontal({ - ftxui::Button("Cancel", [] () { throw event::close{}; }, ascii()), - ftxui::Button("Save", [this] () { - error_.clear(); - config_->store(*wal_, error_); - if (error_.empty()) - throw event::close{}; - }, ascii()) - }); - - ftxui::Components ui; - ui.reserve(config_->states.size() + 1); - ui.push_back(buttons_); - for (const auto& opt : config_->states) - ui.push_back(opt.ui); - - ui_ = ftxui::Container::Vertical(std::move(ui)); - Add(ui_); - } - - bool OnEvent(ftxui::Event event) override final - { - if (event == ftxui::Event::CtrlQ) - throw event::close{}; - ui_->OnEvent(std::move(event)); - return true; - } - - ftxui::Element OnRender() override final - { - const auto min_size = ftxui::size(ftxui::WIDTH, ftxui::GREATER_THAN, 5); - std::vector grid; - grid.reserve(config_->states.size()); - for (const auto& opt : config_->states) - grid.push_back({opt.description, min_size(opt.ui->Render())}); - - ftxui::Element highlighted; - if (error_.empty()) - highlighted = ftxui::separator(); - else - highlighted = ftxui::inverted(ftxui::text(error_)); - - return ftxui::window(title_, ftxui::vbox({ - ftxui::hcenter(buttons_->Render()), - highlighted, - ftxui::gridbox(std::move(grid)) - })); - } - }; - - ftxui::Component settings(std::shared_ptr wal) - { - return std::make_shared(std::move(wal)); - } - struct wallet_state { const std::shared_ptr wal; @@ -308,21 +124,22 @@ namespace lwcli { namespace view state_.overlay->OnEvent(std::move(event)); else if (event == ftxui::Event::CtrlQ) throw event::close{}; - else if (event == ftxui::Event::s || event == ftxui::Event::S) - ; // state_.overlay = send(state_.wal); - 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_); - else if (event == ftxui::Event::r || event == ftxui::Event::R) - ; //state_.wal->refreshAsync(); - else if (event == ftxui::Event::e || event == ftxui::Event::E) - state_.overlay = settings(state_.wal); - else - ui_->OnEvent(std::move(event)); + else if (!ui_->OnEvent(event)) + { + if (event == ftxui::Event::s || event == ftxui::Event::S) + ; // state_.overlay = send(state_.wal); + 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_); + else if (event == ftxui::Event::r || event == ftxui::Event::R) + state_.wal->refreshAsync(); + else if (event == ftxui::Event::e || event == ftxui::Event::E) + state_.overlay = settings(state_.wal); + } - // if (!has_overlay && state_.overlay) - // Add(state_.overlay); + if (!has_overlay && state_.overlay) + Add(state_.overlay); update_account(); }