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
# 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)
+57 -35
View File
@@ -31,6 +31,8 @@
#include <ftxui/component/event.hpp>
#include <ftxui/dom/table.hpp>
#include <lws_frontend.h>
#include <map>
#include <unordered_map>
#include "decorate/overlay.h"
#include "events.h"
@@ -130,13 +132,13 @@ namespace lwcli { namespace view
virtual ftxui::Element OnRender() override final
{
std::vector<ftxui::Elements> 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<std::vector<std::string>> base_rows;
std::ptrdiff_t highlighted_;
std::size_t row_count_;
std::unordered_map<std::size_t, int> 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<tx_details>(wallet_->history(), row_count_ - highlighted_ - 1);
overlay_ = ftxui::Make<tx_details>(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
@@ -336,32 +342,46 @@ namespace lwcli { namespace view
if (!tx_history)
throw std::runtime_error{"unexpected history nullptr"};
std::map<std::pair<std::uint64_t, std::string>, std::pair<const Monero::TransactionInfo*, std::size_t>, std::greater<>> ordered;
tx_history->refresh();
std::vector<std::vector<std::string>> 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<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 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_;
}
+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();
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<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;
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<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
{
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<options>(state);
auto load = ftxui::Button("Load", [enclosed] () {
auto enclosed = std::make_shared<options>(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<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
{
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<options>(state);
auto enclosed = std::make_shared<options>(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<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
{
@@ -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<options>(state);
auto enclosed = std::make_shared<options>(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<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
{
@@ -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<options>(state);
auto recover = ftxui::Button("Recover (Broken)", [enclosed] () {
auto enclosed = std::make_shared<options>(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<std::string> 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<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),
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<Monero::WalletManager>&& wm)
explicit manager_(std::shared_ptr<Monero::WalletManager>&& wm, std::string&& file)
: ftxui::ComponentBase(),
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)
{}
@@ -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)
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
+2 -1
View File
@@ -29,10 +29,11 @@
#include <cstdint>
#include <ftxui/component/component_base.hpp>
#include <string>
namespace Monero { class WalletManager; }
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
+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 "translate.h"
#include "views/history.h"
#include "views/settings.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, _("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<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
{
const std::shared_ptr<Monero::Wallet> 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();
}