refactor(console): unify lite + full-node console behind a pluggable executor
The two consoles were separate implementations (the rich full-node ConsoleTab and a
minimal RenderLiteConsoleTab). Introduce ConsoleCommandExecutor to abstract the only
real differences — command execution + log source — so both variants share the one rich
terminal (the lite console now gains selection, zoom, copy, and JSON-colored output).
- console_command_executor.{h,cpp}: interface + FullNodeConsoleExecutor (dragonxd log
ingestion + RPC command execution, result queue) and LiteConsoleExecutor (lite
diagnostics ring + backend runConsoleCommand/takeConsoleResult, connection/sync status).
- ConsoleTab::render() now takes a ConsoleCommandExecutor&; log ingestion, command
submission, and command results flow through it. The RPC command-reference popup and
the daemon/errors/rpc-trace/app filter toggles are gated on the executor's capabilities;
the toolbar status dot + a status header come from the executor.
- Safety preserved: `clear`/`cls` is intercepted as a view-only clear in the shared UI so
it is NEVER forwarded (the lite backend's `clear` wipes tx history); command output is
not persisted to diagnostics.
- App owns the executor (created per variant) and routes both NavPages to console_tab_;
lite_console_tab.{h,cpp} deleted.
Needs runtime verification of BOTH consoles (full-node RPC + lite: clear, seed/export
secrecy, sync status, command results).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
41
src/app.cpp
41
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<ui::LiteConsoleExecutor>(this);
|
||||
else
|
||||
console_exec_ = std::make_unique<ui::FullNodeConsoleExecutor>(this);
|
||||
}
|
||||
console_tab_.render(*console_exec_);
|
||||
break;
|
||||
case ui::NavPage::Settings:
|
||||
ui::RenderSettingsPage(this);
|
||||
|
||||
@@ -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<ui::ConsoleCommandExecutor> console_exec_;
|
||||
|
||||
// Pending payment from URI
|
||||
bool pending_payment_valid_ = false;
|
||||
|
||||
313
src/ui/windows/console_command_executor.cpp
Normal file
313
src/ui/windows/console_command_executor.cpp
Normal file
@@ -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 <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
|
||||
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<std::mutex> 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<std::mutex> 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<std::mutex> lk(results_mutex_);
|
||||
results_.push_back({result_str, is_error});
|
||||
}
|
||||
}
|
||||
|
||||
bool FullNodeConsoleExecutor::pollResult(std::string& result, bool& isError)
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<int>(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<int>(DState::Running)) {
|
||||
add(TR("console_daemon_started"), ConsoleTab::COLOR_INFO);
|
||||
} else if (cur == DState::Stopped && prev == static_cast<int>(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<int>(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<std::uint64_t>(-1)) {
|
||||
const std::uint64_t added = gen - diag_gen_;
|
||||
startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast<std::size_t>(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<ConsoleStatusLine> LiteConsoleExecutor::statusLines() const
|
||||
{
|
||||
std::vector<ConsoleStatusLine> 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
|
||||
111
src/ui/windows/console_command_executor.h
Normal file
111
src/ui/windows/console_command_executor.h
Normal file
@@ -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 <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
class App;
|
||||
namespace ui {
|
||||
|
||||
using ConsoleAddLineFn = std::function<void(const std::string&, ImU32)>;
|
||||
|
||||
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<ConsoleStatusLine> 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<std::pair<std::string, bool>> 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<ConsoleStatusLine> statusLines() const override;
|
||||
ConsoleStatusLine toolbarStatus() const override;
|
||||
|
||||
private:
|
||||
App* app_;
|
||||
std::uint64_t diag_gen_ = static_cast<std::uint64_t>(-1); // last consumed diagnostics generation
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -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 <imgui.h>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
@@ -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<char>(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<std::mutex> lock(lines_mutex_);
|
||||
@@ -1635,10 +1502,9 @@ void ConsoleTab::clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<ConsoleLine> s_lines;
|
||||
std::uint64_t s_diagGen = static_cast<std::uint64_t>(-1); // last consumed diagnostics generation
|
||||
char s_input[512] = "";
|
||||
std::vector<std::string> 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<char>(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<std::uint64_t>(-1)) {
|
||||
const std::uint64_t added = gen - s_diagGen;
|
||||
startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast<std::size_t>(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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user