diff --git a/CMakeLists.txt b/CMakeLists.txt index 2347d46..b02fd0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -452,10 +452,10 @@ set(APP_SOURCES src/ui/windows/mining_tab_helpers.cpp src/ui/windows/peers_tab.cpp src/ui/windows/network_tab.cpp - src/ui/windows/lite_console_tab.cpp src/ui/windows/explorer_tab.cpp src/ui/windows/market_tab.cpp src/ui/windows/console_tab.cpp + src/ui/windows/console_command_executor.cpp src/ui/windows/console_command_reference.cpp src/ui/windows/console_input_model.cpp src/ui/windows/console_output_model.cpp diff --git a/src/app.cpp b/src/app.cpp index 2c879cd..ab29e91 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -31,7 +31,7 @@ #include "ui/windows/mining_tab.h" #include "ui/windows/peers_tab.h" #include "ui/windows/network_tab.h" -#include "ui/windows/lite_console_tab.h" +#include "ui/windows/console_command_executor.h" #include "ui/windows/explorer_tab.h" #include "ui/windows/market_tab.h" #include "ui/windows/settings_window.h" @@ -205,6 +205,25 @@ std::string App::poolMiningInstalledVersion() return daemon::XmrigManager::installedVersion(); } +// Console backend accessors — prefer the fast-lane RPC connection (so console commands +// don't queue behind the refresh batch), falling back to the main connection. +rpc::RPCClient* App::consoleRpc() +{ + return (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); +} +rpc::RPCWorker* App::consoleWorker() +{ + return (fast_rpc_ && fast_rpc_->isConnected() && fast_worker_) ? fast_worker_.get() : worker_.get(); +} +daemon::EmbeddedDaemon* App::consoleDaemon() +{ + return daemon_controller_ ? daemon_controller_->daemon() : nullptr; +} +daemon::XmrigManager* App::consoleXmrig() +{ + return xmrig_manager_.get(); +} + bool App::sendStopCommandSafely(rpc::RPCClient& client, const char* context) { const char* label = context ? context : "App"; @@ -1814,23 +1833,23 @@ void App::render() case ui::NavPage::LiteNetwork: ui::RenderLiteNetworkTab(this); break; - case ui::NavPage::LiteConsole: - ui::RenderLiteConsoleTab(this); - break; case ui::NavPage::Explorer: ui::RenderExplorerTab(this); break; case ui::NavPage::Market: ui::RenderMarketTab(this); break; + // One console UI for both variants; the executor supplies the backend (full-node + // RPC vs lite). The two NavPages are mutually exclusive per build. + case ui::NavPage::LiteConsole: case ui::NavPage::Console: - // Use fast-lane worker for console commands to avoid head-of-line - // blocking behind the consolidated refreshData() batch. - // Fall back to main rpc/worker if fast-lane hasn't connected yet. - console_tab_.render(daemon_controller_ ? daemon_controller_->daemon() : nullptr, - (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(), - (fast_rpc_ && fast_rpc_->isConnected() && fast_worker_) ? fast_worker_.get() : worker_.get(), - xmrig_manager_.get()); + if (!console_exec_) { + if (isLiteBuild()) + console_exec_ = std::make_unique(this); + else + console_exec_ = std::make_unique(this); + } + console_tab_.render(*console_exec_); break; case ui::NavPage::Settings: ui::RenderSettingsPage(this); diff --git a/src/app.h b/src/app.h index 198e39c..dcb2564 100644 --- a/src/app.h +++ b/src/app.h @@ -155,6 +155,12 @@ public: // Accessors for subsystems rpc::RPCClient* rpc() { return rpc_.get(); } rpc::RPCWorker* worker() { return worker_.get(); } + // Console backend accessors (fast-lane-preferring, defined in app.cpp where the + // subsystem types are complete) used by the shared console executor. + rpc::RPCClient* consoleRpc(); + rpc::RPCWorker* consoleWorker(); + daemon::EmbeddedDaemon* consoleDaemon(); + daemon::XmrigManager* consoleXmrig(); config::Settings* settings() { return settings_.get(); } // Lite wallet controller (non-null only in lite builds with a linked backend). wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); } @@ -607,8 +613,9 @@ private: int coin_logo_h_ = 0; bool coin_logo_loaded_ = false; - // Console tab + // Console tab + its backend executor (full-node RPC or lite backend), created lazily. ui::ConsoleTab console_tab_; + std::unique_ptr console_exec_; // Pending payment from URI bool pending_payment_valid_ = false; diff --git a/src/ui/windows/console_command_executor.cpp b/src/ui/windows/console_command_executor.cpp new file mode 100644 index 0000000..0e3b253 --- /dev/null +++ b/src/ui/windows/console_command_executor.cpp @@ -0,0 +1,313 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#include "console_command_executor.h" +#include "console_tab.h" // ConsoleTab::COLOR_* statics +#include "console_input_model.h" // BuildConsoleRpcCall + +#include "../../app.h" +#include "../../data/wallet_state.h" +#include "../../rpc/rpc_client.h" +#include "../../rpc/rpc_worker.h" +#include "../../daemon/embedded_daemon.h" +#include "../../daemon/xmrig_manager.h" +#include "../../wallet/lite_wallet_controller.h" +#include "../../wallet/lite_diagnostics.h" +#include "../../util/i18n.h" +#include "../material/colors.h" + +#include + +#include +#include +#include + +namespace dragonx { +namespace ui { + +using namespace material; + +// ============================================================================ +// FullNodeConsoleExecutor +// ============================================================================ + +bool FullNodeConsoleExecutor::isReady() const +{ + rpc::RPCClient* rpc = app_->consoleRpc(); + return rpc && rpc->isConnected(); +} + +void FullNodeConsoleExecutor::submit(const std::string& cmd) +{ + rpc::RPCClient* rpc = app_->consoleRpc(); + if (!rpc || !rpc->isConnected()) { + std::lock_guard lk(results_mutex_); + results_.push_back({std::string(TR("console_not_connected")), true}); + return; + } + + auto call = BuildConsoleRpcCall(cmd); + if (!call.valid) return; + const std::string method = call.method; + const nlohmann::json params = call.params; + + rpc::RPCWorker* worker = app_->consoleWorker(); + if (worker) { + worker->post([rpc, method, params, this]() -> rpc::RPCWorker::MainCb { + std::string result_str; + bool is_error = false; + try { + rpc::RPCClient::TraceScope trace("Console tab / User command"); + result_str = rpc->callRaw(method, params); + } catch (const std::exception& e) { + result_str = e.what(); + is_error = true; + } + return [this, result_str, is_error]() { + std::lock_guard lk(results_mutex_); + results_.push_back({result_str, is_error}); + }; + }); + } else { + std::string result_str; + bool is_error = false; + try { + rpc::RPCClient::TraceScope trace("Console tab / User command"); + result_str = rpc->callRaw(method, params); + } catch (const std::exception& e) { + result_str = e.what(); + is_error = true; + } + std::lock_guard lk(results_mutex_); + results_.push_back({result_str, is_error}); + } +} + +bool FullNodeConsoleExecutor::pollResult(std::string& result, bool& isError) +{ + std::lock_guard lk(results_mutex_); + if (results_.empty()) return false; + result = std::move(results_.front().first); + isError = results_.front().second; + results_.pop_front(); + return true; +} + +void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) +{ + using DState = daemon::EmbeddedDaemon::State; + + daemon::EmbeddedDaemon* d = app_->consoleDaemon(); + if (d) { + const DState cur = d->getState(); + const int prev = last_daemon_state_; + if (cur == DState::Starting && prev == static_cast(DState::Stopped)) { + add("", ConsoleTab::COLOR_RESULT); + add(TR("console_starting_node"), ConsoleTab::COLOR_INFO); + add(TR("console_capturing_output"), ConsoleTab::COLOR_INFO); + add("", ConsoleTab::COLOR_RESULT); + } else if (cur == DState::Running && prev != static_cast(DState::Running)) { + add(TR("console_daemon_started"), ConsoleTab::COLOR_INFO); + } else if (cur == DState::Stopped && prev == static_cast(DState::Running)) { + add("", ConsoleTab::COLOR_RESULT); + add(TR("console_daemon_stopped"), ConsoleTab::COLOR_INFO); + } else if (cur == DState::Error) { + add(std::string(TR("console_daemon_error")) + d->getLastError() + " ===", ConsoleTab::COLOR_ERROR); + } + last_daemon_state_ = static_cast(cur); + } + + rpc::RPCClient* rpc = app_->consoleRpc(); + if (rpc) { + const bool now = rpc->isConnected(); + if (now && !last_rpc_connected_) add(TR("console_connected"), ConsoleTab::COLOR_INFO); + else if (!now && last_rpc_connected_) add(TR("console_disconnected"), ConsoleTab::COLOR_ERROR); + last_rpc_connected_ = now; + } + + if (d) { + std::string out = d->getOutputSince(last_daemon_output_size_); + if (!out.empty()) { + std::istringstream ss(out); + std::string line; + while (std::getline(ss, line)) { + if (line.empty()) continue; + ImU32 c = ConsoleTab::COLOR_DAEMON; + if (line.find("[ERROR]") != std::string::npos || + line.find("error:") != std::string::npos || + line.find("Error:") != std::string::npos) + c = ConsoleTab::COLOR_ERROR; + add("[daemon] " + line, c); + } + } + } + + daemon::XmrigManager* x = app_->consoleXmrig(); + if (x && x->isRunning()) { + std::string out = x->getOutputSince(last_xmrig_output_size_); + if (!out.empty()) { + std::istringstream ss(out); + std::string line; + while (std::getline(ss, line)) { + if (line.empty()) continue; + ImU32 c = ConsoleTab::COLOR_DAEMON; + if (line.find("error") != std::string::npos || + line.find("ERROR") != std::string::npos || + line.find("failed") != std::string::npos) + c = ConsoleTab::COLOR_ERROR; + else if (line.find("accepted") != std::string::npos) + c = ConsoleTab::COLOR_INFO; + add("[xmrig] " + line, c); + } + } + } else { + last_xmrig_output_size_ = 0; // reset so we get fresh output when it restarts + } +} + +void FullNodeConsoleExecutor::printHelp(const ConsoleAddLineFn& add) +{ + add(TR("console_available_commands"), ConsoleTab::COLOR_INFO); + add(TR("console_help_clear"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_help"), ConsoleTab::COLOR_RESULT); + add("", ConsoleTab::COLOR_RESULT); + add(TR("console_common_rpc"), ConsoleTab::COLOR_INFO); + add(TR("console_help_getinfo"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_getbalance"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_gettotalbalance"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_getblockcount"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_getpeerinfo"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_setgenerate"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_getmininginfo"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_stop"), ConsoleTab::COLOR_RESULT); + add("", ConsoleTab::COLOR_RESULT); + add(TR("console_click_commands"), ConsoleTab::COLOR_INFO); + add(TR("console_tab_completion"), ConsoleTab::COLOR_INFO); +} + +ConsoleStatusLine FullNodeConsoleExecutor::toolbarStatus() const +{ + ConsoleStatusLine s; + daemon::EmbeddedDaemon* d = app_->consoleDaemon(); + if (!d) return s; // empty text -> toolbar shows the generic "no daemon" label + using DState = daemon::EmbeddedDaemon::State; + switch (d->getState()) { + case DState::Stopped: s.text = TR("console_status_stopped"); s.color = IM_COL32(150,150,150,255); break; + case DState::Starting: s.text = TR("console_status_starting"); s.color = Warning(); s.pulse = true; break; + case DState::Running: s.text = TR("console_status_running"); s.color = Success(); break; + case DState::Stopping: s.text = TR("console_status_stopping"); s.color = Warning(); s.pulse = true; break; + case DState::Error: s.text = TR("console_status_error"); s.color = Error(); break; + } + return s; +} + +// ============================================================================ +// LiteConsoleExecutor +// ============================================================================ + +namespace { +// Colour error/success lines from the lite diagnostics stream for at-a-glance scanning. +ImU32 liteLogColor(const std::string& line) +{ + const auto has = [&line](const char* s) { return line.find(s) != std::string::npos; }; + if (has("failed") || has(" unreachable") || has("blocked") || has("could not") || + has("Error") || has("error")) + return Error(); + if (has(": connected") || has("opened") || has("wallet ready") || has("Ready")) + return Success(); + return OnSurfaceMedium(); +} +} // namespace + +bool LiteConsoleExecutor::isReady() const +{ + wallet::LiteWalletController* lw = app_->liteWallet(); + return lw && lw->walletOpen(); +} + +bool LiteConsoleExecutor::busy() const +{ + wallet::LiteWalletController* lw = app_->liteWallet(); + return lw && lw->consoleCommandInProgress(); +} + +void LiteConsoleExecutor::submit(const std::string& cmd) +{ + wallet::LiteWalletController* lw = app_->liteWallet(); + if (!lw) return; + lw->runConsoleCommand(cmd); // rejection (busy) surfaces via the busy() indicator +} + +bool LiteConsoleExecutor::pollResult(std::string& result, bool& isError) +{ + wallet::LiteWalletController* lw = app_->liteWallet(); + if (!lw) return false; + wallet::LiteConsoleResult res; + if (!lw->takeConsoleResult(res)) return false; + result = res.response.empty() ? std::string("(no output)") : res.response; + isError = !res.ok; + return true; +} + +void LiteConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) +{ + auto& diag = wallet::LiteDiagnostics::instance(); + const std::uint64_t gen = diag.generation(); + if (gen == diag_gen_) return; + + const auto snap = diag.snapshot(); + std::size_t startIdx = 0; + if (diag_gen_ != static_cast(-1)) { + const std::uint64_t added = gen - diag_gen_; + startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast(added); + } + for (std::size_t i = startIdx; i < snap.size(); ++i) + add(snap[i], liteLogColor(snap[i])); + diag_gen_ = gen; +} + +void LiteConsoleExecutor::printHelp(const ConsoleAddLineFn& add) +{ + add(TR("console_available_commands"), ConsoleTab::COLOR_INFO); + add(TR("console_help_clear"), ConsoleTab::COLOR_RESULT); + add(TR("console_help_help"), ConsoleTab::COLOR_RESULT); + add(TR("lite_console_help_passthrough"), ConsoleTab::COLOR_INFO); +} + +std::vector LiteConsoleExecutor::statusLines() const +{ + std::vector out; + wallet::LiteWalletController* lw = app_->liteWallet(); + if (lw && lw->walletOpen()) { + const SyncInfo& sync = app_->state().sync; + char buf[96]; + if (sync.syncing && !sync.isSynced()) { + double vp = sync.verification_progress; + if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0; + std::snprintf(buf, sizeof(buf), "Syncing %.1f%% (block %d / %d)", + vp * 100.0, sync.blocks, sync.headers); + } else { + std::snprintf(buf, sizeof(buf), "Synced (block %d)", sync.blocks); + } + out.push_back({std::string(buf), OnSurfaceMedium(), false}); + } + const std::string& err = app_->liteOpenError(); + if (!err.empty() && (!lw || !lw->walletOpen())) + out.push_back({std::string("Last error: ") + err, Error(), false}); + return out; +} + +ConsoleStatusLine LiteConsoleExecutor::toolbarStatus() const +{ + ConsoleStatusLine s; + wallet::LiteWalletController* lw = app_->liteWallet(); + if (!lw) { s.text = "No backend"; s.color = Error(); return s; } + if (lw->walletOpen()) { s.text = "Connected"; s.color = Success(); } + else if (lw->openInProgress()) { s.text = "Connecting"; s.color = Warning(); s.pulse = true; } + else { s.text = "Disconnected"; s.color = Error(); } + return s; +} + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/console_command_executor.h b/src/ui/windows/console_command_executor.h new file mode 100644 index 0000000..83289d1 --- /dev/null +++ b/src/ui/windows/console_command_executor.h @@ -0,0 +1,111 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 +// +// ConsoleCommandExecutor — the pluggable backend behind the shared ConsoleTab UI. It +// abstracts the only real differences between the full-node console (dragonxd log + RPC +// command execution) and the lite console (lite diagnostics ring + backend command), +// so both variants use one rich terminal widget instead of two implementations. + +#pragma once + +#include "imgui.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace dragonx { +class App; +namespace ui { + +using ConsoleAddLineFn = std::function; + +struct ConsoleStatusLine { + std::string text; + ImU32 color = IM_COL32(200, 200, 200, 255); + bool pulse = false; // animate the toolbar dot (starting/connecting) +}; + +class ConsoleCommandExecutor { +public: + virtual ~ConsoleCommandExecutor() = default; + + // True when commands can be run (RPC connected / lite wallet open). + virtual bool isReady() const = 0; + // A command is currently in flight (input is disabled while true). + virtual bool busy() const { return false; } + // Submit a user command for async execution. The UI has already echoed "> cmd" and + // handled the built-ins (clear/help/quit). Results arrive later via pollResult(). + virtual void submit(const std::string& cmd) = 0; + // Pop one completed command result (raw text + error flag) if available; the UI + // formats/colors it via FormatConsoleRpcResultLines. Returns false if none pending. + virtual bool pollResult(std::string& result, bool& isError) = 0; + + // Pull any new passive log lines (daemon/xmrig output, or the lite diagnostics ring) + // and hand each to `add`. Called once per frame. + virtual void pollLogLines(const ConsoleAddLineFn& add) { (void)add; } + + // UI-chrome capabilities. + virtual bool hasRpcReference() const { return false; } // show the RPC command-reference popup + virtual bool hasLogFilters() const { return false; } // show daemon/errors/rpc-trace/app filters + + // Print the backend-appropriate `help` output via `add`. + virtual void printHelp(const ConsoleAddLineFn& add) = 0; + + // Extra status lines drawn above the toolbar (lite shows sync + last error). + virtual std::vector statusLines() const { return {}; } + // Short status shown in the toolbar (daemon state / lite connection). Empty text => + // the toolbar shows its generic "no daemon" label. + virtual ConsoleStatusLine toolbarStatus() const { return {}; } +}; + +// ── Full-node: dragonxd log ingestion + RPC command execution ──────────────── +class FullNodeConsoleExecutor : public ConsoleCommandExecutor { +public: + explicit FullNodeConsoleExecutor(App* app) : app_(app) {} + + bool isReady() const override; + void submit(const std::string& cmd) override; + bool pollResult(std::string& result, bool& isError) override; + void pollLogLines(const ConsoleAddLineFn& add) override; + bool hasRpcReference() const override { return true; } + bool hasLogFilters() const override { return true; } + void printHelp(const ConsoleAddLineFn& add) override; + ConsoleStatusLine toolbarStatus() const override; + +private: + App* app_; + size_t last_daemon_output_size_ = 0; + size_t last_xmrig_output_size_ = 0; + int last_daemon_state_ = -1; // daemon::EmbeddedDaemon::State as int + bool last_rpc_connected_ = false; + std::deque> results_; // {text, isError} + std::mutex results_mutex_; +}; + +// ── Lite: diagnostics ring + backend console command ───────────────────────── +class LiteConsoleExecutor : public ConsoleCommandExecutor { +public: + explicit LiteConsoleExecutor(App* app) : app_(app) {} + + bool isReady() const override; + bool busy() const override; + void submit(const std::string& cmd) override; + bool pollResult(std::string& result, bool& isError) override; + void pollLogLines(const ConsoleAddLineFn& add) override; + void printHelp(const ConsoleAddLineFn& add) override; + std::vector statusLines() const override; + ConsoleStatusLine toolbarStatus() const override; + +private: + App* app_; + std::uint64_t diag_gen_ = static_cast(-1); // last consumed diagnostics generation +}; + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 7370903..5ba09c2 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -6,6 +6,7 @@ // tab completion, daemon log display, and color-coded output. #include "console_tab.h" +#include "console_command_executor.h" #include "console_command_reference.h" #include "console_input_model.h" #include "console_output_model.h" @@ -23,6 +24,7 @@ #include "../../util/i18n.h" #include +#include #include #include #include @@ -143,7 +145,7 @@ ConsoleTab::~ConsoleTab() if (s_rpc_trace_console == this) s_rpc_trace_console = nullptr; } -void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc::RPCWorker* worker, daemon::XmrigManager* xmrig) +void ConsoleTab::render(ConsoleCommandExecutor& exec) { using namespace material; @@ -173,103 +175,25 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc } } - // Check for daemon state changes - if (daemon) { - auto current_state = daemon->getState(); - - // Show message when daemon starts - if (current_state == daemon::EmbeddedDaemon::State::Starting && - last_daemon_state_ == daemon::EmbeddedDaemon::State::Stopped) { - addLine("", COLOR_RESULT); - addLine(TR("console_starting_node"), COLOR_INFO); - addLine(TR("console_capturing_output"), COLOR_INFO); - addLine("", COLOR_RESULT); - shown_startup_message_ = true; - } - else if (current_state == daemon::EmbeddedDaemon::State::Running && - last_daemon_state_ != daemon::EmbeddedDaemon::State::Running) { - addLine(TR("console_daemon_started"), COLOR_INFO); - } - else if (current_state == daemon::EmbeddedDaemon::State::Stopped && - last_daemon_state_ == daemon::EmbeddedDaemon::State::Running) { - addLine("", COLOR_RESULT); - addLine(TR("console_daemon_stopped"), COLOR_INFO); - } - else if (current_state == daemon::EmbeddedDaemon::State::Error) { - addLine(std::string(TR("console_daemon_error")) + daemon->getLastError() + " ===", COLOR_ERROR); - } - - last_daemon_state_ = current_state; + // Pull passive log lines (daemon/xmrig output, or the lite diagnostics ring) and any + // completed command results from the backend executor. + exec.pollLogLines([this](const std::string& l, ImU32 c) { addLine(l, c); }); + { + std::string result; + bool isError = false; + while (exec.pollResult(result, isError)) addFormattedResult(result, isError); } - // Track RPC connection state and show a message when connected - if (rpc) { - bool connected_now = rpc->isConnected(); - if (connected_now && !last_rpc_connected_) { - addLine(TR("console_connected"), COLOR_INFO); - } else if (!connected_now && last_rpc_connected_) { - addLine(TR("console_disconnected"), COLOR_ERROR); - } - last_rpc_connected_ = connected_now; - } - - // Check for new daemon output — always capture so toggle works as a live filter - if (daemon) { - std::string new_output = daemon->getOutputSince(last_daemon_output_size_); - if (!new_output.empty()) { - // Split by newlines and add each line - std::istringstream stream(new_output); - std::string line; - while (std::getline(stream, line)) { - if (!line.empty()) { - // Color based on content: [ERROR] -> red, [WARN] -> warning color - ImU32 lineColor = COLOR_DAEMON; - if (line.find("[ERROR]") != std::string::npos || - line.find("error:") != std::string::npos || - line.find("Error:") != std::string::npos) { - lineColor = COLOR_ERROR; - } - addLine("[daemon] " + line, lineColor); - } - } - } - } - - // Check for new xmrig output (pool mining) - if (xmrig && xmrig->isRunning()) { - std::string new_output = xmrig->getOutputSince(last_xmrig_output_size_); - if (!new_output.empty()) { - std::istringstream stream(new_output); - std::string line; - while (std::getline(stream, line)) { - if (!line.empty()) { - // Color xmrig output - errors in red, accepted shares in green - ImU32 lineColor = COLOR_DAEMON; - if (line.find("error") != std::string::npos || - line.find("ERROR") != std::string::npos || - line.find("failed") != std::string::npos) { - lineColor = COLOR_ERROR; - } else if (line.find("accepted") != std::string::npos) { - lineColor = COLOR_INFO; - } - addLine("[xmrig] " + line, lineColor); - } - } - } - } else if (!xmrig || !xmrig->isRunning()) { - // Reset offset when xmrig stops so we get fresh output next time - if (last_xmrig_output_size_ != 0) { - last_xmrig_output_size_ = 0; - } - } - // Main console layout ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar); + // Optional status header (lite: sync + last error) above the toolbar. + renderStatusHeader(exec); + // Toolbar - renderToolbar(daemon); - + renderToolbar(exec); + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Output area (scrollable) — glass panel background @@ -384,8 +308,8 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Input area - renderInput(rpc, worker); - + renderInput(exec); + ImGui::EndChild(); } @@ -397,7 +321,7 @@ void ConsoleTab::renderCommandsPopupModal() renderCommandsPopup(); } -void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon) +void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec) { using namespace material; ImDrawList* dl = ImGui::GetWindowDrawList(); @@ -414,56 +338,29 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (toolbarH - ImGui::GetFrameHeight()) * 0.5f); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + Layout::spacingMd()); - // Daemon status with colored dot - if (daemon) { - auto state = daemon->getState(); - const char* status_text = TR("console_status_unknown"); - ImU32 dotCol = IM_COL32(150, 150, 150, 255); - bool pulse = false; + // Backend status with colored dot (daemon state / lite connection). + { + ConsoleStatusLine st = exec.toolbarStatus(); + if (!st.text.empty()) { + ImVec2 cp = ImGui::GetCursorScreenPos(); + float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale(); + float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f; + float dotX = cp.x + dotR + 2; - switch (state) { - case daemon::EmbeddedDaemon::State::Stopped: - status_text = TR("console_status_stopped"); - dotCol = IM_COL32(150, 150, 150, 255); - break; - case daemon::EmbeddedDaemon::State::Starting: - status_text = TR("console_status_starting"); - dotCol = Warning(); - pulse = true; - break; - case daemon::EmbeddedDaemon::State::Running: - status_text = TR("console_status_running"); - dotCol = Success(); - break; - case daemon::EmbeddedDaemon::State::Stopping: - status_text = TR("console_status_stopping"); - dotCol = Warning(); - pulse = true; - break; - case daemon::EmbeddedDaemon::State::Error: - status_text = TR("console_status_error"); - dotCol = Error(); - break; - } + if (st.pulse) { + float a = schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-fast").size); + ImU32 pCol = (st.color & 0x00FFFFFF) | ((ImU32)(255 * a) << 24); + dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, pCol); + } else { + dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, st.color); + } - ImVec2 cp = ImGui::GetCursorScreenPos(); - float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale(); - float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f; - float dotX = cp.x + dotR + 2; - - if (pulse) { - float a = schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-fast").size); - ImU32 pCol = (dotCol & 0x00FFFFFF) | ((ImU32)(255 * a) << 24); - dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, pCol); + ImGui::Dummy(ImVec2(dotR * 2 + 6, 0)); + ImGui::SameLine(); + Type().textColored(TypeStyle::Caption, st.color, st.text.c_str()); } else { - dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, dotCol); + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon")); } - - ImGui::Dummy(ImVec2(dotR * 2 + 6, 0)); - ImGui::SameLine(); - Type().textColored(TypeStyle::Caption, dotCol, status_text); - } else { - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon")); } ImGui::SameLine(); @@ -482,6 +379,8 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon) ImGui::Spacing(); ImGui::SameLine(); + // Log filters (daemon/errors/rpc-trace/app) apply only to a daemon log source. + if (exec.hasLogFilters()) { // Daemon messages toggle { static bool s_prev_daemon_enabled = true; @@ -554,6 +453,8 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon) ImGui::Spacing(); ImGui::SameLine(); + } // exec.hasLogFilters() + // Clear button if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) { clear(); @@ -587,15 +488,16 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon) ImGui::SameLine(); - // Commands reference button - if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) { - show_commands_popup_ = true; + // Commands reference button (full-node RPC reference only) + if (exec.hasRpcReference()) { + if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) { + show_commands_popup_ = true; + } + if (ImGui::IsItemHovered()) { + material::Tooltip("%s", TR("console_show_rpc_ref")); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - material::Tooltip("%s", TR("console_show_rpc_ref")); - } - - ImGui::SameLine(); // Line count { @@ -1192,7 +1094,7 @@ void ConsoleTab::clearSelection() sel_end_ = {-1, 0}; } -void ConsoleTab::renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker) +void ConsoleTab::renderInput(ConsoleCommandExecutor& exec) { using namespace material; @@ -1269,15 +1171,49 @@ void ConsoleTab::renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker) return 0; }; + const bool busy = exec.busy(); + if (busy) ImGui::BeginDisabled(); if (ImGui::InputText("##ConsoleInput", input_buffer_, sizeof(input_buffer_), flags, callback, this)) { std::string cmd(input_buffer_); + // Trim surrounding whitespace. + size_t tb = cmd.find_first_not_of(" \t"); + if (tb == std::string::npos) cmd.clear(); else cmd = cmd.substr(tb); + while (!cmd.empty() && (cmd.back() == ' ' || cmd.back() == '\t')) cmd.pop_back(); + if (!cmd.empty()) { - executeCommand(cmd, rpc, worker); + addLine("> " + cmd, COLOR_COMMAND); + AppendConsoleHistory(command_history_, cmd, 100); + history_index_ = -1; + + // First token, lowercased, for built-in interception. + std::string first; + { + size_t fb = cmd.find_first_not_of(" \t"); + size_t fe = cmd.find_first_of(" \t", fb); + first = cmd.substr(fb, fe == std::string::npos ? std::string::npos : fe - fb); + std::transform(first.begin(), first.end(), first.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + } + auto add = [this](const std::string& l, ImU32 c) { addLine(l, c); }; + if (first == "clear" || first == "cls") { + // View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history). + clear(); + clearSelection(); + } else if (first == "help") { + exec.printHelp(add); + } else if (first == "quit" || first == "exit") { + addLine(TR("console_quit_note"), COLOR_INFO); + } else if (!exec.isReady()) { + addLine(TR("console_not_connected"), COLOR_ERROR); + } else { + exec.submit(cmd); + } input_buffer_[0] = '\0'; reclaim_focus = true; } } - + if (busy) ImGui::EndDisabled(); + ImGui::PopItemWidth(); // Auto-focus on input @@ -1480,106 +1416,37 @@ void ConsoleTab::renderCommandsPopup() material::EndOverlayDialog(); } -void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc::RPCWorker* worker) +void ConsoleTab::addFormattedResult(const std::string& result, bool is_error) { using namespace material; - // Add to history (avoid duplicates) - AppendConsoleHistory(command_history_, cmd, 100); - history_index_ = -1; - - // Echo command - addLine("> " + cmd, COLOR_COMMAND); - - // Handle built-in commands - if (cmd == "clear") { - clear(); - return; - } - - if (cmd == "help") { - addLine(TR("console_available_commands"), COLOR_INFO); - addLine(TR("console_help_clear"), COLOR_RESULT); - addLine(TR("console_help_help"), COLOR_RESULT); - addLine("", COLOR_RESULT); - addLine(TR("console_common_rpc"), COLOR_INFO); - addLine(TR("console_help_getinfo"), COLOR_RESULT); - addLine(TR("console_help_getbalance"), COLOR_RESULT); - addLine(TR("console_help_gettotalbalance"), COLOR_RESULT); - addLine(TR("console_help_getblockcount"), COLOR_RESULT); - addLine(TR("console_help_getpeerinfo"), COLOR_RESULT); - addLine(TR("console_help_setgenerate"), COLOR_RESULT); - addLine(TR("console_help_getmininginfo"), COLOR_RESULT); - addLine(TR("console_help_stop"), COLOR_RESULT); - addLine("", COLOR_RESULT); - addLine(TR("console_click_commands"), COLOR_INFO); - addLine(TR("console_tab_completion"), COLOR_INFO); - return; - } - - // Execute RPC command - if (!rpc || !rpc->isConnected()) { - addLine(TR("console_not_connected"), COLOR_ERROR); - return; - } - - auto call = BuildConsoleRpcCall(cmd); - if (!call.valid) return; + ImU32 json_key_col = WithAlpha(Secondary(), 255); + ImU32 json_str_col = WithAlpha(Success(), 255); + ImU32 json_num_col = WithAlpha(Warning(), 255); + ImU32 json_brace_col = IM_COL32(200, 200, 200, 150); - std::string method = call.method; - nlohmann::json params = call.params; - - // Execute RPC call on worker thread to avoid blocking UI - if (worker) { - // Capture 'this' for addLine calls in MainCb (runs on main thread, ConsoleTab outlives callbacks) - auto self = this; - worker->post([rpc, method, params, self]() -> rpc::RPCWorker::MainCb { - std::string result_str; - bool is_error = false; - try { - rpc::RPCClient::TraceScope trace("Console tab / User command"); - result_str = rpc->callRaw(method, params); - } catch (const std::exception& e) { - result_str = e.what(); - is_error = true; - } - return [result_str, is_error, self]() { - // Process results on main thread where ImGui colors are available - using namespace material; - ImU32 json_key_col = WithAlpha(Secondary(), 255); - ImU32 json_str_col = WithAlpha(Success(), 255); - ImU32 json_num_col = WithAlpha(Warning(), 255); - ImU32 json_brace_col = IM_COL32(200, 200, 200, 150); - - for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, is_error)) { - ImU32 lineCol = COLOR_RESULT; - switch (resultLine.role) { - case ConsoleResultLineRole::Error: lineCol = COLOR_ERROR; break; - case ConsoleResultLineRole::JsonKey: lineCol = json_key_col; break; - case ConsoleResultLineRole::JsonString: lineCol = json_str_col; break; - case ConsoleResultLineRole::JsonNumber: lineCol = json_num_col; break; - case ConsoleResultLineRole::JsonBrace: lineCol = json_brace_col; break; - case ConsoleResultLineRole::Result: break; - } - self->addLine(resultLine.text, lineCol); - } - }; - }); - } else { - // Fallback: synchronous execution if no worker available - try { - rpc::RPCClient::TraceScope trace("Console tab / User command"); - std::string result_str = rpc->callRaw(method, params); - for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, false)) { - addLine(resultLine.text, COLOR_RESULT); - } - } catch (const std::exception& e) { - for (const auto& resultLine : FormatConsoleRpcResultLines(e.what(), true)) { - addLine(resultLine.text, COLOR_ERROR); - } + for (const auto& resultLine : FormatConsoleRpcResultLines(result, is_error)) { + ImU32 lineCol = COLOR_RESULT; + switch (resultLine.role) { + case ConsoleResultLineRole::Error: lineCol = COLOR_ERROR; break; + case ConsoleResultLineRole::JsonKey: lineCol = json_key_col; break; + case ConsoleResultLineRole::JsonString: lineCol = json_str_col; break; + case ConsoleResultLineRole::JsonNumber: lineCol = json_num_col; break; + case ConsoleResultLineRole::JsonBrace: lineCol = json_brace_col; break; + case ConsoleResultLineRole::Result: break; } + addLine(resultLine.text, lineCol); } } +void ConsoleTab::renderStatusHeader(ConsoleCommandExecutor& exec) +{ + using namespace material; + auto lines = exec.statusLines(); + for (const auto& sl : lines) + Type().textColored(TypeStyle::Caption, sl.color, sl.text.c_str()); + if (!lines.empty()) ImGui::Dummy(ImVec2(0, Layout::spacingXs())); +} + void ConsoleTab::addLine(const std::string& line, ImU32 color) { std::lock_guard lock(lines_mutex_); @@ -1635,10 +1502,9 @@ void ConsoleTab::clear() { std::lock_guard lock(lines_mutex_); lines_.clear(); - last_daemon_output_size_ = 0; - last_xmrig_output_size_ = 0; } - // addLine() takes the lock itself, so call it outside the locked scope + // The executor keeps its own log cursors, so a view-clear stays cleared (new output + // still appends). addLine() takes the lock itself, so call it outside the locked scope. addLine(TR("console_cleared"), COLOR_INFO); } diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index 0dae1f1..8afcb98 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -18,23 +18,23 @@ namespace dragonx { namespace ui { +class ConsoleCommandExecutor; + /** - * @brief Console tab for daemon output and command input - * - * Shows dragonxd output and allows executing RPC commands. + * @brief Console tab — a rich terminal shared by the full-node and lite variants. + * + * The command execution + log sources are provided by a ConsoleCommandExecutor, so this + * one widget serves both dragonxd (RPC) and the lite backend. */ class ConsoleTab { public: ConsoleTab(); ~ConsoleTab(); - + /** - * @brief Render the console tab - * @param daemon Pointer to embedded daemon (may be null) - * @param rpc Pointer to RPC client for command execution - * @param xmrig Pointer to xmrig manager for pool mining output (may be null) + * @brief Render the console tab against a backend executor. */ - void render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc::RPCWorker* worker, daemon::XmrigManager* xmrig = nullptr); + void render(ConsoleCommandExecutor& exec); /** * @brief Render the RPC Command Reference popup at top-level scope. @@ -93,11 +93,13 @@ private: ImU32 color; }; - void executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc::RPCWorker* worker); void addCommandResult(const std::string& cmd, const std::string& result, bool is_error = false); - void renderToolbar(daemon::EmbeddedDaemon* daemon); + // Format + color a completed command result (JSON-aware) into console lines. + void addFormattedResult(const std::string& result, bool is_error); + void renderStatusHeader(ConsoleCommandExecutor& exec); + void renderToolbar(ConsoleCommandExecutor& exec); void renderOutput(); - void renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker); + void renderInput(ConsoleCommandExecutor& exec); void renderCommandsPopup(); // Selection helpers @@ -123,12 +125,8 @@ private: bool scroll_to_bottom_ = false; float scroll_up_cooldown_ = 0.0f; // seconds to wait before re-enabling auto-scroll int new_lines_since_scroll_ = 0; // new lines while scrolled up (for indicator) - size_t last_daemon_output_size_ = 0; - size_t last_xmrig_output_size_ = 0; - bool shown_startup_message_ = false; - daemon::EmbeddedDaemon::State last_daemon_state_ = daemon::EmbeddedDaemon::State::Stopped; - bool last_rpc_connected_ = false; - + // (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor) + // Text selection state bool is_selecting_ = false; bool has_selection_ = false; diff --git a/src/ui/windows/lite_console_tab.cpp b/src/ui/windows/lite_console_tab.cpp deleted file mode 100644 index 3fb9419..0000000 --- a/src/ui/windows/lite_console_tab.cpp +++ /dev/null @@ -1,274 +0,0 @@ -// DragonX Wallet - ImGui Edition -// Copyright 2024-2026 The Hush Developers -// Released under the GPLv3 - -#include "lite_console_tab.h" -#include "console_input_model.h" // reuse the generic command-history helpers - -#include "../../app.h" -#include "../../data/wallet_state.h" -#include "../../util/i18n.h" -#include "../../wallet/lite_diagnostics.h" -#include "../../wallet/lite_wallet_controller.h" -#include "../layout.h" -#include "../material/type.h" -#include "../material/colors.h" -#include "imgui.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace dragonx { -namespace ui { - -using namespace material; - -namespace { - -struct ConsoleLine { - std::string text; - ImU32 color; -}; - -// Unified, in-memory console buffer: it interleaves the automatic diagnostics log (lifecycle / -// connection / sync events) with the user's interactive commands and their responses, the same -// way the full-node Console interleaves the daemon log with RPC I/O. NOTE: this buffer can hold -// secret command output (e.g. `seed`/`export`) and is NEVER persisted to LiteDiagnostics. -std::vector s_lines; -std::uint64_t s_diagGen = static_cast(-1); // last consumed diagnostics generation -char s_input[512] = ""; -std::vector s_history; -int s_historyIndex = -1; -bool s_autoScroll = true; -bool s_scrollToBottom = false; -bool s_focusInput = false; - -// Colour error/success lines for at-a-glance scanning (substring match on the messages the -// controller emits). Anything else renders in the muted default colour. -ImU32 logColor(const std::string& line) -{ - const auto has = [&line](const char* s) { return line.find(s) != std::string::npos; }; - if (has("failed") || has(" unreachable") || has("blocked") || has("could not") || - has("Error") || has("error")) - return Error(); - if (has(": connected") || has("opened") || has("wallet ready") || has("Ready")) - return Success(); - return OnSurfaceMedium(); -} - -// Append text as one or more lines (splitting on '\n' so multi-line JSON responses render row by row). -void appendLines(const std::string& text, ImU32 color) -{ - std::size_t start = 0; - while (start <= text.size()) { - std::size_t nl = text.find('\n', start); - std::string line = text.substr(start, nl == std::string::npos ? std::string::npos : nl - start); - while (!line.empty() && (line.back() == '\r')) line.pop_back(); - s_lines.push_back({std::move(line), color}); - if (nl == std::string::npos) break; - start = nl + 1; - } -} - -std::string lowerFirstToken(const std::string& cmd) -{ - std::size_t b = cmd.find_first_not_of(" \t"); - if (b == std::string::npos) return {}; - std::size_t e = cmd.find_first_of(" \t", b); - std::string tok = cmd.substr(b, e == std::string::npos ? std::string::npos : e - b); - std::transform(tok.begin(), tok.end(), tok.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - return tok; -} - -// InputText history navigation (Up/Down) — reuses the generic console history helpers. -int inputCallback(ImGuiInputTextCallbackData* data) -{ - if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) { - const bool up = (data->EventKey == ImGuiKey_UpArrow); - s_historyIndex = NavigateConsoleHistoryIndex(s_historyIndex, s_history.size(), up); - const std::string entry = ConsoleHistoryEntry(s_history, s_historyIndex); - data->DeleteChars(0, data->BufTextLen); - if (!entry.empty()) data->InsertChars(0, entry.c_str()); - } - return 0; -} - -} // namespace - -void RenderLiteConsoleTab(App* app) -{ - if (!app) return; - wallet::LiteWalletController* lw = app->liteWallet(); - - // ── Header ────────────────────────────────────────────────────────────────── - Type().text(TypeStyle::H6, TR("lite_console_title")); - Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("lite_console_intro")); - ImGui::Spacing(); - - // ── Live status (read straight from the controller — always meaningful) ─────── - { - const char* connText; - ImU32 connCol; - if (!lw) { - connText = "Lite backend not linked in this build (rebuild with --lite-backend)"; - connCol = Error(); - } else if (lw->walletOpen()) { - connText = "Connected"; - connCol = Success(); - } else if (lw->openInProgress()) { - connText = "Connecting\xE2\x80\xA6"; // ellipsis - connCol = Warning(); - } else { - connText = "Disconnected"; - connCol = Error(); - } - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_console_status")); - ImGui::SameLine(); - Type().textColored(TypeStyle::Body2, connCol, connText); - - if (lw && lw->walletOpen()) { - const SyncInfo& sync = app->state().sync; - char buf[96]; - if (sync.syncing && !sync.isSynced()) { - double vp = sync.verification_progress; - if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0; - std::snprintf(buf, sizeof(buf), "Syncing %.1f%% (block %d / %d)", - vp * 100.0, sync.blocks, sync.headers); - } else { - std::snprintf(buf, sizeof(buf), "Synced (block %d)", sync.blocks); - } - Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf); - } - const std::string& openErr = app->liteOpenError(); - if (!openErr.empty() && (!lw || !lw->walletOpen())) { - Type().textColored(TypeStyle::Caption, Error(), - (std::string("Last error: ") + openErr).c_str()); - } - } - ImGui::Spacing(); - - // ── Ingest new automatic diagnostics events into the unified buffer ─────────── - auto& diag = wallet::LiteDiagnostics::instance(); - const std::uint64_t gen = diag.generation(); - if (gen != s_diagGen) { - const auto snap = diag.snapshot(); - // First pass dumps the whole ring; later passes append only the delta (generation counts - // total events ever added; the ring is bounded, so cap the delta at the snapshot size). - std::size_t startIdx = 0; - if (s_diagGen != static_cast(-1)) { - const std::uint64_t added = gen - s_diagGen; - startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast(added); - } - for (std::size_t i = startIdx; i < snap.size(); ++i) - s_lines.push_back({snap[i], logColor(snap[i])}); - if (gen != s_diagGen && !snap.empty()) s_scrollToBottom = true; - s_diagGen = gen; - } - - // ── Drain a completed interactive command ──────────────────────────────────── - if (lw) { - wallet::LiteConsoleResult res; - if (lw->takeConsoleResult(res)) { - appendLines(res.response.empty() ? "(no output)" : res.response, - res.ok ? OnSurface() : Error()); - s_scrollToBottom = true; - } - } - - // ── Toolbar ───────────────────────────────────────────────────────────────── - if (ImGui::Button(TR("lite_console_clear"))) { - s_lines.clear(); - s_diagGen = diag.generation(); // don't re-dump the whole ring on the next frame - } - ImGui::SameLine(); - if (ImGui::Button(TR("lite_console_copy"))) { - std::string all; - for (const auto& l : s_lines) { all += l.text; all.push_back('\n'); } - ImGui::SetClipboardText(all.c_str()); - } - ImGui::SameLine(); - ImGui::Checkbox(TR("lite_console_autoscroll"), &s_autoScroll); - ImGui::Spacing(); - - // ── Output log (terminal-styled scroll region) ─────────────────────────────── - const float inputRowH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingSm(); - ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 90)); - ImGui::BeginChild("##LiteConsoleLog", ImVec2(0, -inputRowH), true, - ImGuiWindowFlags_HorizontalScrollbar); - ImGui::PushFont(Type().caption()); - if (s_lines.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceDisabled()); - ImGui::TextUnformatted(TR("lite_console_empty")); - ImGui::PopStyleColor(); - } else { - for (const auto& line : s_lines) { - ImGui::PushStyleColor(ImGuiCol_Text, line.color); - ImGui::TextUnformatted(line.text.c_str()); // not format-interpreted — safe for any content - ImGui::PopStyleColor(); - } - } - if (s_scrollToBottom && s_autoScroll) ImGui::SetScrollHereY(1.0f); - else if (s_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) ImGui::SetScrollHereY(1.0f); - s_scrollToBottom = false; - ImGui::PopFont(); - ImGui::EndChild(); - ImGui::PopStyleColor(); - - // ── Command input row ──────────────────────────────────────────────────────── - const bool busy = lw && lw->consoleCommandInProgress(); - auto submit = [&]() { - std::string cmd = s_input; - // trim - const std::size_t b = cmd.find_first_not_of(" \t"); - if (b == std::string::npos) { s_input[0] = '\0'; return; } - cmd = cmd.substr(b); - while (!cmd.empty() && (cmd.back() == ' ' || cmd.back() == '\t')) cmd.pop_back(); - - s_lines.push_back({"> " + cmd, Primary()}); - AppendConsoleHistory(s_history, cmd); - s_historyIndex = -1; - - const std::string first = lowerFirstToken(cmd); - if (first == "clear" || first == "cls") { - // The backend's `clear` wipes wallet tx history — NOT the screen. Intercept it as a - // view-clear (what the user expects) so a stray "clear" can't destroy history. - s_lines.clear(); - s_diagGen = diag.generation(); - } else if (first == "quit" || first == "exit") { - appendLines(TR("lite_console_quit_note"), OnSurfaceMedium()); - } else if (!lw) { - appendLines("Lite backend not linked in this build.", Error()); - } else if (!lw->runConsoleCommand(cmd)) { - appendLines(TR("lite_console_busy"), Warning()); - } - s_input[0] = '\0'; - s_scrollToBottom = true; - s_focusInput = true; // keep focus for the next command - }; - - ImGui::PushFont(Type().caption()); - Type().textColored(TypeStyle::Caption, busy ? Warning() : Primary(), - busy ? TR("lite_console_running") : "\xE2\x80\xBA"); // ‹running…› / "›" - ImGui::SameLine(); - ImGui::SetNextItemWidth(-FLT_MIN); - if (busy) ImGui::BeginDisabled(); - const ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | - ImGuiInputTextFlags_CallbackHistory; - if (s_focusInput) { ImGui::SetKeyboardFocusHere(); s_focusInput = false; } - if (ImGui::InputTextWithHint("##LiteConsoleInput", TR("lite_console_input_hint"), - s_input, sizeof(s_input), flags, inputCallback)) { - submit(); - } - if (busy) ImGui::EndDisabled(); - ImGui::PopFont(); -} - -} // namespace ui -} // namespace dragonx diff --git a/src/ui/windows/lite_console_tab.h b/src/ui/windows/lite_console_tab.h deleted file mode 100644 index 5aa7024..0000000 --- a/src/ui/windows/lite_console_tab.h +++ /dev/null @@ -1,18 +0,0 @@ -// DragonX Wallet - ImGui Edition -// Copyright 2024-2026 The Hush Developers -// Released under the GPLv3 -// -// Lite-wallet "Console" tab: a read-only, terminal-styled view of the lite diagnostics log -// (server connection attempts, wallet open/create/restore, sync milestones, errors). Lite -// builds only. See src/ui/windows/lite_console_tab.cpp. - -#pragma once - -namespace dragonx { -class App; -namespace ui { - -void RenderLiteConsoleTab(App* app); - -} // namespace ui -} // namespace dragonx diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 9095661..629311d 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -947,6 +947,8 @@ void I18n::loadBuiltinEnglish() // --- Console Tab --- strings_["console_auto_scroll"] = "Auto-scroll"; strings_["console_available_commands"] = "Available commands:"; + strings_["console_quit_note"] = "'quit'/'exit' aren't needed here — just close the window."; + strings_["lite_console_help_passthrough"] = "Any other input runs as a lite-wallet console command."; strings_["console_capturing_output"] = "Capturing daemon output..."; strings_["console_clear"] = "Clear"; strings_["console_clear_console"] = "Clear Console";