Add CLI flags, updated UI loading routine, and moved files around

This commit is contained in:
Lee *!* Clagett
2025-04-06 21:18:36 -04:00
parent 47b60ffd36
commit 0633f571ec
7 changed files with 462 additions and 307 deletions
+2 -2
View File
@@ -26,8 +26,8 @@
# 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.
set(lwcli-views_sources history.cpp keys.cpp manager.cpp wallet.cpp) set(lwcli-views_sources history.cpp keys.cpp manager.cpp settings.cpp wallet.cpp)
set(lwscli-views_headers history.h keys.h manager.h wallet.h) set(lwscli-views_headers history.h keys.h manager.h settings.h wallet.h)
add_library(lwcli-views ${lwcli-views_sources} ${lwcli-views_headers}) 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-decorate lwsf-api)
+58 -36
View File
@@ -31,6 +31,8 @@
#include <ftxui/component/event.hpp> #include <ftxui/component/event.hpp>
#include <ftxui/dom/table.hpp> #include <ftxui/dom/table.hpp>
#include <lws_frontend.h> #include <lws_frontend.h>
#include <map>
#include <unordered_map>
#include "decorate/overlay.h" #include "decorate/overlay.h"
#include "events.h" #include "events.h"
@@ -130,13 +132,13 @@ namespace lwcli { namespace view
virtual ftxui::Element OnRender() override final virtual ftxui::Element OnRender() override final
{ {
std::vector<ftxui::Elements> grid{ std::vector<ftxui::Elements> grid{
{ftxui::text("Description: "), note_input_->Render()}, {ftxui::text(_("Description: ")), note_input_->Render()},
timestamp_, timestamp_,
payment_id_, payment_id_,
{ftxui::text("Confirmations: "), ftxui::text(std::to_string(info_->confirmations()))}, {ftxui::text(_("Confirmations: ")), ftxui::text(std::to_string(info_->confirmations()))},
amount_, amount_,
fee_, fee_,
{ftxui::text("Block Height: "), ftxui::text(std::to_string(info_->blockHeight()))}, {ftxui::text(_("Block Height: ")), ftxui::text(std::to_string(info_->blockHeight()))},
minors_, minors_,
coinbase_ coinbase_
}; };
@@ -148,7 +150,7 @@ namespace lwcli { namespace view
std::string{_(" to Unknown Address")} : _(" to ") + transfer.address; std::string{_(" to Unknown Address")} : _(" to ") + transfer.address;
std::string text; std::string text;
if (base_size == grid.size()) if (base_size == grid.size())
text = "Transfers: "; text = _("Transfers: ");
grid.push_back({ grid.push_back({
ftxui::text(std::move(text)), ftxui::text(std::move(text)),
ftxui::text(print_money(transfer.amount) + address) ftxui::text(print_money(transfer.amount) + address)
@@ -156,15 +158,17 @@ namespace lwcli { namespace view
} }
ftxui::Elements vertical; ftxui::Elements vertical;
vertical.reserve(3); vertical.reserve(4);
vertical.push_back(ftxui::hcenter(ftxui::hbox(cancel_->Render(), ok_->Render())));
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())
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::gridbox(std::move(grid)));
vertical.push_back(ftxui::hcenter(ftxui::hbox(cancel_->Render(), ok_->Render())));
return ftxui::window(title_, ftxui::vbox(std::move(vertical))); return ftxui::window(title_, ftxui::vbox(std::move(vertical)));
} }
@@ -231,8 +235,8 @@ namespace lwcli { namespace view
}, ftxui::ButtonOption::Ascii()); }, ftxui::ButtonOption::Ascii());
container_ = ftxui::Container::Vertical({ 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 ftxui::Element table_; //!< Does not redraw when child is enabled
std::vector<std::vector<std::string>> base_rows; std::vector<std::vector<std::string>> base_rows;
std::ptrdiff_t highlighted_; std::ptrdiff_t highlighted_;
std::size_t row_count_; std::unordered_map<std::size_t, int> row_map_;
const std::uint32_t account_; const std::uint32_t account_;
static constexpr std::size_t min_row() noexcept { return 2; }; static constexpr std::size_t min_row() noexcept { return 2; };
@@ -261,7 +265,7 @@ namespace lwcli { namespace view
table_(nullptr), table_(nullptr),
base_rows(), base_rows(),
highlighted_(min_row() - 1), highlighted_(min_row() - 1),
row_count_(0), row_map_(),
account_(account) account_(account)
{ {
Monero::Subaddress* subaddress = wallet_->subaddress(); Monero::Subaddress* subaddress = wallet_->subaddress();
@@ -294,17 +298,19 @@ namespace lwcli { namespace view
} }
else if (event == ftxui::Event::CtrlQ) else if (event == ftxui::Event::CtrlQ)
throw event::close{}; 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_; ++highlighted_;
else if (event == ftxui::Event::ArrowUp && min_row() <= highlighted_) else if (event == ftxui::Event::ArrowUp && min_row() <= highlighted_)
--highlighted_; --highlighted_;
else if (event == ftxui::Event::PageDown) 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) else if (event == ftxui::Event::PageUp)
highlighted_ = std::max(std::ptrdiff_t(min_row()), highlighted_ - 10); highlighted_ = std::max(std::ptrdiff_t(min_row()), highlighted_ - 10);
else if (event == ftxui::Event::Return) else if (event == ftxui::Event::Return)
{ {
overlay_ = ftxui::Make<tx_details>(wallet_->history(), row_count_ - highlighted_ - 1); overlay_ = ftxui::Make<tx_details>(wallet_->history(), row_map_.at(highlighted_ - min_row()));
Add(overlay_); Add(overlay_);
} }
} }
@@ -315,7 +321,7 @@ namespace lwcli { namespace view
overlay_->Detach(); overlay_->Detach();
overlay_.reset(); overlay_.reset();
} }
return min_row() <= highlighted_ && highlighted_ < row_count_; return min_row() <= highlighted_ && highlighted_ - min_row() < row_map_.size();
} }
ftxui::Element OnRender() override final ftxui::Element OnRender() override final
@@ -324,8 +330,8 @@ namespace lwcli { namespace view
{ {
if (highlighted_ < min_row()) if (highlighted_ < min_row())
highlighted_ = min_row(); highlighted_ = min_row();
else if (row_count_ <= highlighted_) else if (row_map_.size() <= highlighted_ - min_row())
highlighted_ = row_count_ - 1; highlighted_ = row_map_.size() - 1 + min_row();
} }
/* Do not redraw table when showing tx. the /* Do not redraw table when showing tx. the
@@ -335,33 +341,47 @@ namespace lwcli { namespace view
Monero::TransactionHistory* tx_history = wallet_->history(); Monero::TransactionHistory* tx_history = wallet_->history();
if (!tx_history) if (!tx_history)
throw std::runtime_error{"unexpected history nullptr"}; 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(); tx_history->refresh();
std::vector<std::vector<std::string>> rows = base_rows;
std::uint64_t balance = 0;
const auto history = tx_history->getAll(); 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) if (!tx)
throw std::runtime_error{"unexpected tx_info nullptr"}; throw std::runtime_error{"unexpected tx_info nullptr"};
if (tx->subaddrAccount() != account_) if (tx->subaddrAccount() == account_)
continue; ordered.try_emplace({tx->blockHeight(), tx->hash()}, tx, i);
}
std::uint64_t balance = 0;
std::vector<std::vector<std::string>> 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 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();
if (direction == Monero::TransactionInfo::Direction_Out)
if (!tx->isFailed())
{ {
balance -= amount; if (direction == Monero::TransactionInfo::Direction_Out)
balance -= fee; {
balance -= amount;
balance -= fee;
}
else
balance += amount;
} }
else
balance += amount;
std::tm expanded{}; std::tm expanded{};
const std::time_t timestamp = tx->timestamp(); const std::time_t timestamp = tx->timestamp();
@@ -386,15 +406,16 @@ namespace lwcli { namespace view
print_money(tx->fee()), print_money(tx->fee()),
tx->hash().substr(0, 16) tx->hash().substr(0, 16)
}); });
++i;
} }
row_count_ = rows.size();
ftxui::Table table{std::move(rows)}; ftxui::Table table{std::move(rows)};
table.SelectRow(0).Decorate(ftxui::bold); table.SelectRow(0).Decorate(ftxui::bold);
table.SelectRow(0).SeparatorVertical(ftxui::LIGHT); table.SelectRow(0).SeparatorVertical(ftxui::LIGHT);
table.SelectRow(0).Border(ftxui::LIGHT); table.SelectRow(0).Border(ftxui::LIGHT);
table.SelectColumn(3).DecorateCells(ftxui::xflex_grow); //table.SelectColumn(3).DecorateCells(ftxui::xflex_grow);
if (min_row() <= highlighted_ && highlighted_ < row_count_) if (min_row() <= highlighted_ && highlighted_ - min_row() < row_map_.size())
{ {
auto row = table.SelectRow(highlighted_); auto row = table.SelectRow(highlighted_);
row.Decorate(ftxui::inverted); row.Decorate(ftxui::inverted);
@@ -407,7 +428,8 @@ namespace lwcli { namespace view
table_ = ftxui::vbox({ table_ = ftxui::vbox({
title_, title_,
ftxui::text(_("Balance: ") + print_money(balance)), 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_; return table_;
} }
+110 -69
View File
@@ -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<void()> on_enter = nullptr)
{ {
auto opt = ftxui::InputOption::Default(); auto opt = ftxui::InputOption::Default();
opt.password = true; opt.password = true;
opt.multiline = false; opt.multiline = false;
if (on_enter)
opt.on_enter = std::move(on_enter);
return ftxui::Input(pass, std::move(opt)); return ftxui::Input(pass, std::move(opt));
} }
@@ -94,13 +104,9 @@ namespace lwcli { namespace view
std::string file; std::string file;
std::string password; std::string password;
wallet_base() wallet_base(std::string default_file)
: file(), password() : file(std::move(default_file)), password()
{ {}
const char* home = std::getenv("HOME");
if (home)
file = home;
}
}; };
struct new_wallet : wallet_base struct new_wallet : wallet_base
@@ -111,8 +117,8 @@ namespace lwcli { namespace view
std::string proxy; std::string proxy;
bool ssl; bool ssl;
new_wallet() new_wallet(std::string default_file)
: wallet_base(), confirm(), language(config::default_language), server(config::server::default_url), proxy(), ssl(false) : wallet_base(std::move(default_file)), confirm(), language(config::default_language), server(config::server::default_url), proxy(), ssl(false)
{} {}
void setup(Monero::Wallet& wal) void setup(Monero::Wallet& wal)
@@ -136,7 +142,7 @@ namespace lwcli { namespace view
{} {}
}; };
std::optional<std::uint64_t> from_string(const std::string& str) noexcept std::optional<std::uint64_t> from_string(const std::string_view str) noexcept
{ {
std::uint64_t height = 0; std::uint64_t height = 0;
const auto end = str.data() + str.size(); 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}))) 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 false;
} }
return true; return true;
} }
std::pair<std::vector<std::pair<ftxui::Element, ftxui::Component>>, ftxui::Component> get_load_options(start_state* state) using option_set = std::pair<std::vector<std::pair<ftxui::Element, ftxui::Component>>, ftxui::Component>;
option_set get_load_options(std::string default_file, start_state* state)
{ {
struct options struct options
{ {
start_state* state; start_state* state;
wallet_base config; wallet_base config;
options(start_state* state) options(std::string default_file, start_state* state)
: state(state), config() : state(state), config(std::move(default_file))
{} {}
}; };
auto enclosed = std::make_shared<options>(state); auto enclosed = std::make_shared<options>(std::move(default_file), state);
auto load = ftxui::Button("Load", [enclosed] () { const auto load_action = [enclosed] () {
auto prepped = prep_wallet( auto prepped = prep_wallet(
enclosed->state->wm, 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 &enclosed->state->error
); );
if (prepped) if (prepped)
{ {
enclosed->config.password.clear(); if (init_wallet(*prepped, &enclosed->state->error))
init_wallet(*prepped, &enclosed->state->error); {
enclosed->state->wal = prepped; enclosed->config.password.clear();
prepped->startRefresh();
enclosed->state->wal = prepped;
}
} }
}, ftxui::ButtonOption::Ascii()); };
return { return {
{ {
{ftxui::text(_("Filename: ")), last_input(&enclosed->config.file)}, {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<std::vector<std::pair<ftxui::Element, ftxui::Component>>, ftxui::Component> get_create_options(start_state* state) option_set get_create_options(std::string default_file, start_state* state)
{ {
struct options struct options
{ {
start_state* state; start_state* state;
new_wallet config; new_wallet config;
options(start_state* state) options(std::string default_file, start_state* state)
: state(state), config() : state(state), config(std::move(default_file))
{} {}
}; };
auto enclosed = std::make_shared<options>(state); auto enclosed = std::make_shared<options>(std::move(default_file), state);
auto create = ftxui::Button("Create", [enclosed] () { auto create = ftxui::Button("Create", [enclosed] () {
if (!enclosed->config.file.empty()) if (!enclosed->config.file.empty())
{ {
@@ -223,19 +235,21 @@ namespace lwcli { namespace view
{ {
auto prepped = prep_wallet( auto prepped = prep_wallet(
enclosed->state->wm, 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 &enclosed->state->error
); );
if (prepped) if (prepped)
{ {
if (prepped->store({})) if (prepped->store({}))
{ {
enclosed->config.password.clear();
enclosed->config.confirm.clear();
enclosed->config.setup(*prepped); enclosed->config.setup(*prepped);
init_wallet(*prepped, &enclosed->state->error); if (init_wallet(*prepped, &enclosed->state->error))
enclosed->state->overlay = view::keys(prepped, true /* show warning */); {
enclosed->state->wal = prepped; enclosed->config.password.clear();
enclosed->config.confirm.clear();
enclosed->state->overlay = view::keys(prepped, true /* show warning */);
enclosed->state->wal = prepped;
}
} }
else else
enclosed->state->error = "Unable to create file: " + prepped->errorString(); 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(_("Password: ")), password(&enclosed->config.password)},
{ftxui::text(_("Confirm: ")), password(&enclosed->config.confirm)}, {ftxui::text(_("Confirm: ")), password(&enclosed->config.confirm)},
{ftxui::text(_("Language: ")), last_input(&enclosed->config.language)}, {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(_("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 create
}; };
} }
std::pair<std::vector<std::pair<ftxui::Element, ftxui::Component>>, ftxui::Component> get_seed_options(start_state* state) option_set get_seed_options(std::string default_file, start_state* state)
{ {
struct options struct options
{ {
@@ -269,11 +283,11 @@ namespace lwcli { namespace view
std::string height; std::string height;
new_wallet config; new_wallet config;
options(start_state* state) options(std::string default_file, start_state* state)
: state(state), mnemonic(), height("0"), config() : state(state), mnemonic(), height("0"), config(std::move(default_file))
{} {}
}; };
auto enclosed = std::make_shared<options>(state); auto enclosed = std::make_shared<options>(std::move(default_file), state);
auto recover = ftxui::Button("Recover", [enclosed] () { auto recover = ftxui::Button("Recover", [enclosed] () {
const auto height = from_string(enclosed->height); const auto height = from_string(enclosed->height);
if (!height) if (!height)
@@ -294,18 +308,22 @@ namespace lwcli { namespace view
{ {
auto prepped = prep_wallet( auto prepped = prep_wallet(
enclosed->state->wm, 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 &enclosed->state->error
); );
if (prepped) if (prepped)
{ {
if (prepped->store({})) if (prepped->store({}))
{ {
enclosed->config.password.clear();
enclosed->config.confirm.clear();
enclosed->config.setup(*prepped); enclosed->config.setup(*prepped);
init_wallet(*prepped, &enclosed->state->error); if (init_wallet(*prepped, &enclosed->state->error))
enclosed->state->wal = prepped; {
enclosed->mnemonic.clear();
enclosed->config.password.clear();
enclosed->config.confirm.clear();
prepped->rescanBlockchainAsync();
enclosed->state->wal = prepped;
}
} }
else else
enclosed->state->error = "Unable to create file: " + prepped->errorString(); 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(_("Confirm: ")), password(&enclosed->config.confirm)},
{ftxui::text(_("Mnemonic: ")), last_input(&enclosed->mnemonic)}, {ftxui::text(_("Mnemonic: ")), last_input(&enclosed->mnemonic)},
{ftxui::text(_("Height: ")), last_input(&enclosed->height)}, {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(_("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 recover
}; };
} }
std::pair<std::vector<std::pair<ftxui::Element, ftxui::Component>>, ftxui::Component> get_key_options(start_state* state) option_set get_key_options(std::string default_file, start_state* state)
{ {
struct options struct options
{ {
@@ -341,12 +359,12 @@ namespace lwcli { namespace view
std::string height; std::string height;
new_wallet config; new_wallet config;
options(start_state* state) options(std::string default_file, start_state* state)
: state(state), spend_key(), height("0"), config() : state(state), spend_key(), height("0"), config(std::move(default_file))
{} {}
}; };
auto enclosed = std::make_shared<options>(state); auto enclosed = std::make_shared<options>(std::move(default_file), state);
auto recover = ftxui::Button("Recover (Broken)", [enclosed] () { auto recover = ftxui::Button(_("Recover (Broken)"), [enclosed] () {
const auto height = from_string(enclosed->height); const auto height = from_string(enclosed->height);
if (!height) if (!height)
{ {
@@ -370,7 +388,7 @@ namespace lwcli { namespace view
enclosed->config.file, enclosed->config.file,
enclosed->config.password, enclosed->config.password,
enclosed->config.language, enclosed->config.language,
Monero::TESTNET, config::network,
*height, *height,
"address", "address",
"view_key", "view_key",
@@ -382,11 +400,15 @@ namespace lwcli { namespace view
{ {
if (prepped->store({})) if (prepped->store({}))
{ {
enclosed->config.password.clear();
enclosed->config.confirm.clear();
enclosed->config.setup(*prepped); enclosed->config.setup(*prepped);
init_wallet(*prepped, &enclosed->state->error); if (init_wallet(*prepped, &enclosed->state->error))
enclosed->state->wal = prepped; {
enclosed->spend_key.clear();
enclosed->config.password.clear();
enclosed->config.confirm.clear();
prepped->rescanBlockchainAsync();
enclosed->state->wal = prepped;
}
} }
else else
enclosed->state->error = "Unable to create file: " + prepped->errorString(); 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(_("Spend key: ")), last_input(&enclosed->spend_key)},
{ftxui::text(_("Height: ")), last_input(&enclosed->height)}, {ftxui::text(_("Height: ")), last_input(&enclosed->height)},
{ftxui::text(_("Language: ")), last_input(&enclosed->config.language)}, {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(_("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 recover
}; };
@@ -421,6 +443,7 @@ namespace lwcli { namespace view
const ftxui::Element help_; const ftxui::Element help_;
const ftxui::Element disclaimer_; const ftxui::Element disclaimer_;
start_state state_; start_state state_;
const std::string default_file_;
const std::vector<std::string> options_; const std::vector<std::string> options_;
int active_; int active_;
int selected_; int selected_;
@@ -437,16 +460,24 @@ namespace lwcli { namespace view
return ui_; 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: public:
explicit start(std::shared_ptr<Monero::WalletManager>&& wm, std::shared_ptr<Monero::Wallet>* out) explicit start(std::shared_ptr<Monero::WalletManager>&& wm, std::string&& file, std::shared_ptr<Monero::Wallet>* out)
: out_(out), : out_(out),
title_(ftxui::text("wmcli")), title_(ftxui::text("wmcli")),
help_(decorate::banner(ftxui::text(_("Ctrl-Q to close active window, Ctrl-C close app immediately")))), 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")))), disclaimer_(decorate::banner(ftxui::text(_("Beware of mouse events in Tmux/Screen")))),
state_(std::move(wm)), state_(std::move(wm)),
default_file_(std::move(file)),
options_({_("Load Wallet"), _("Create Wallet"), _("Recover from Seed"), _("Recover from Keys")}), options_({_("Load Wallet"), _("Create Wallet"), _("Recover from Seed"), _("Recover from Keys")}),
active_(-1), active_(-1),
selected_(0), selected_(get_selected(default_file_)),
mode_(ftxui::Dropdown(&options_, &selected_)), mode_(ftxui::Dropdown(&options_, &selected_)),
completion_(), completion_(),
stack_(), stack_(),
@@ -459,21 +490,22 @@ namespace lwcli { namespace view
{ {
if (active_ != selected_) if (active_ != selected_)
{ {
const bool first_load = active_ == -1;
active_ = selected_; active_ = selected_;
switch(active_) switch(active_)
{ {
case 0: case 0:
default: 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; break;
case 1: 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; break;
case 2: 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; break;
case 3: 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; break;
} }
@@ -485,6 +517,15 @@ namespace lwcli { namespace view
ui.push_back(completion_); ui.push_back(completion_);
ui_ = ftxui::Container::Vertical(std::move(ui)); 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 // Delay showing wallet if options required overlay
@@ -566,10 +607,10 @@ namespace lwcli { namespace view
} }
public: public:
explicit manager_(std::shared_ptr<Monero::WalletManager>&& wm) explicit manager_(std::shared_ptr<Monero::WalletManager>&& wm, std::string&& file)
: ftxui::ComponentBase(), : ftxui::ComponentBase(),
data_(nullptr), data_(nullptr),
start_(std::make_shared<start>(std::move(wm), &data_)), start_(std::make_shared<start>(std::move(wm), std::move(file), &data_)),
wallet_(nullptr) wallet_(nullptr)
{} {}
@@ -602,10 +643,10 @@ namespace lwcli { namespace view
}; };
} }
ftxui::Component manager(std::shared_ptr<Monero::WalletManager> wm) ftxui::Component manager(std::shared_ptr<Monero::WalletManager> wm, std::string&& file)
{ {
if (!wm) if (!wm)
throw std::runtime_error{"lwcli::view::manaager given nullptr"}; throw std::runtime_error{"lwcli::view::manaager given nullptr"};
return std::make_shared<manager_>(std::move(wm)); return std::make_shared<manager_>(std::move(wm), std::move(file));
} }
}} // lwcli // view }} // lwcli // view
+2 -1
View File
@@ -29,10 +29,11 @@
#include <cstdint> #include <cstdint>
#include <ftxui/component/component_base.hpp> #include <ftxui/component/component_base.hpp>
#include <string>
namespace Monero { class WalletManager; } namespace Monero { class WalletManager; }
namespace lwcli { namespace view namespace lwcli { namespace view
{ {
ftxui::Component manager(std::shared_ptr<Monero::WalletManager> wm); ftxui::Component manager(std::shared_ptr<Monero::WalletManager> wm, std::string&& file);
}} // lwscli // view }} // lwscli // view
+236
View File
@@ -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 <charconv>
#include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
#include <ftxui/dom/table.hpp>
#include <lws_frontend.h>
#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<std::uint64_t> 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<option, 4> 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<option_state, options.size()> 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<Monero::Wallet> wal_;
const ftxui::Element title_;
const std::unique_ptr<configuration> 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<Monero::Wallet>&& wal)
: ftxui::ComponentBase(),
wal_(std::move(wal)),
title_(ftxui::text(_("Settings"))),
config_(std::make_unique<configuration>(*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<ftxui::Elements> 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<Monero::Wallet> wal)
{
return std::make_shared<settings_>(std::move(wal));
}
}} // lwcli //
+38
View File
@@ -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 <ftxui/component/component_base.hpp>
#include <memory>
namespace Monero { class Wallet; }
namespace lwcli { namespace view
{
ftxui::Component settings(std::shared_ptr<Monero::Wallet> wallet);
}} // lwscli // view
+16 -199
View File
@@ -39,198 +39,14 @@
#include "lwcli_config.h" #include "lwcli_config.h"
#include "translate.h" #include "translate.h"
#include "views/history.h" #include "views/history.h"
#include "views/settings.h"
namespace lwcli { namespace view namespace lwcli { namespace view
{ {
namespace namespace
{ {
std::optional<std::uint64_t> 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<option, 4> 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(); } 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<option_state, options.size()> 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<Monero::Wallet> wal_;
const ftxui::Element title_;
const std::unique_ptr<configuration> 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<Monero::Wallet>&& wal)
: ftxui::ComponentBase(),
wal_(std::move(wal)),
title_(ftxui::text(_("Settings"))),
config_(std::make_unique<configuration>(*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<ftxui::Elements> 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<Monero::Wallet> wal)
{
return std::make_shared<settings_>(std::move(wal));
}
struct wallet_state struct wallet_state
{ {
const std::shared_ptr<Monero::Wallet> wal; const std::shared_ptr<Monero::Wallet> wal;
@@ -308,21 +124,22 @@ namespace lwcli { namespace view
state_.overlay->OnEvent(std::move(event)); state_.overlay->OnEvent(std::move(event));
else if (event == ftxui::Event::CtrlQ) else if (event == ftxui::Event::CtrlQ)
throw event::close{}; throw event::close{};
else if (event == ftxui::Event::s || event == ftxui::Event::S) else if (!ui_->OnEvent(event))
; // state_.overlay = send(state_.wal); {
else if (event == ftxui::Event::b || event == ftxui::Event::B) if (event == ftxui::Event::s || event == ftxui::Event::S)
; // state_.overlay = book(state_.wal); ; // state_.overlay = send(state_.wal);
else if (event == ftxui::Event::a || event == ftxui::Event::a) else if (event == ftxui::Event::b || event == ftxui::Event::B)
; // overlay = accounts(state_.wal, &active_account_); ; // state_.overlay = book(state_.wal);
else if (event == ftxui::Event::r || event == ftxui::Event::R) else if (event == ftxui::Event::a || event == ftxui::Event::a)
; //state_.wal->refreshAsync(); ; // overlay = accounts(state_.wal, &active_account_);
else if (event == ftxui::Event::e || event == ftxui::Event::E) else if (event == ftxui::Event::r || event == ftxui::Event::R)
state_.overlay = settings(state_.wal); state_.wal->refreshAsync();
else else if (event == ftxui::Event::e || event == ftxui::Event::E)
ui_->OnEvent(std::move(event)); state_.overlay = settings(state_.wal);
}
// if (!has_overlay && state_.overlay) if (!has_overlay && state_.overlay)
// Add(state_.overlay); Add(state_.overlay);
update_account(); update_account();
} }