Files
ObsidianDragon/src/ui/windows/console_command_executor.cpp
DanS 33735b1d52 refactor(console): phase 2 — channel-as-semantic key; decouple executor from UI colors
Make ConsoleChannel the canonical semantic classification of every console
line. Producers (command executor, app log forwarders, result formatter) tag
each line with a channel; the UI derives both the text color and the left
accent-bar color from it at draw time, and the output filter keys off it.

This replaces the prior scheme of storing an ImU32 color per line and then
reverse-engineering the source from color-equality plus a "[daemon] "/"[xmrig] "
/"[app] "/"[rpc] " text prefix. Consequences:

- ConsoleLine now stores {text, ConsoleChannel} — no per-line color.
- addLine()/addRpcTraceLine() take a channel; the redundant text prefixes are
  gone (the accent bar carries the origin).
- The executor's ConsoleAddLineFn hands a channel, not an ImU32, so the backend
  no longer depends on ConsoleTab::COLOR_* — FullNode/Lite executors emit clean
  text + channel.
- The theme-remap loop that rewrote stored colors on light/dark flip is deleted;
  colors are resolved per-channel each frame (refreshColors on flip only).
- ConsoleOutputFilter drops its color fields; consoleLinePassesFilter() keys off
  the channel. Test updated to the channel-based API.
- Added a Warning channel (amber severity peer of Error/Success) so the
  notification + logger warning forwarders keep their color.

Full-node + lite build clean; ctest green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:22:35 -05:00

314 lines
12 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "console_command_executor.h"
#include "console_channel.h"
#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("", ConsoleChannel::None);
add(TR("console_starting_node"), ConsoleChannel::Info);
add(TR("console_capturing_output"), ConsoleChannel::Info);
add("", ConsoleChannel::None);
} else if (cur == DState::Running && prev != static_cast<int>(DState::Running)) {
add(TR("console_daemon_started"), ConsoleChannel::Info);
} else if (cur == DState::Stopped && prev == static_cast<int>(DState::Running)) {
add("", ConsoleChannel::None);
add(TR("console_daemon_stopped"), ConsoleChannel::Info);
} else if (cur == DState::Error) {
add(std::string(TR("console_daemon_error")) + d->getLastError() + " ===", ConsoleChannel::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"), ConsoleChannel::Info);
else if (!now && last_rpc_connected_) add(TR("console_disconnected"), ConsoleChannel::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;
ConsoleChannel c = ConsoleChannel::Daemon;
if (line.find("[ERROR]") != std::string::npos ||
line.find("error:") != std::string::npos ||
line.find("Error:") != std::string::npos)
c = ConsoleChannel::Error;
add(line, c); // channel conveys the source; no "[daemon] " prefix
}
}
}
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;
ConsoleChannel c = ConsoleChannel::Xmrig;
if (line.find("error") != std::string::npos ||
line.find("ERROR") != std::string::npos ||
line.find("failed") != std::string::npos)
c = ConsoleChannel::Error;
else if (line.find("accepted") != std::string::npos)
c = ConsoleChannel::Success;
add(line, c); // channel conveys the source; no "[xmrig] " prefix
}
}
} 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"), ConsoleChannel::Info);
add(TR("console_help_clear"), ConsoleChannel::None);
add(TR("console_help_help"), ConsoleChannel::None);
add("", ConsoleChannel::None);
add(TR("console_common_rpc"), ConsoleChannel::Info);
add(TR("console_help_getinfo"), ConsoleChannel::None);
add(TR("console_help_getbalance"), ConsoleChannel::None);
add(TR("console_help_gettotalbalance"), ConsoleChannel::None);
add(TR("console_help_getblockcount"), ConsoleChannel::None);
add(TR("console_help_getpeerinfo"), ConsoleChannel::None);
add(TR("console_help_setgenerate"), ConsoleChannel::None);
add(TR("console_help_getmininginfo"), ConsoleChannel::None);
add(TR("console_help_stop"), ConsoleChannel::None);
add("", ConsoleChannel::None);
add(TR("console_click_commands"), ConsoleChannel::Info);
add(TR("console_tab_completion"), ConsoleChannel::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 {
// Classify a lite diagnostics line into a channel for at-a-glance scanning.
ConsoleChannel liteLogChannel(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 ConsoleChannel::Error;
if (has(": connected") || has("opened") || has("wallet ready") || has("Ready"))
return ConsoleChannel::Success;
return ConsoleChannel::App; // lite diagnostics / wallet log
}
} // 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], liteLogChannel(snap[i]));
diag_gen_ = gen;
}
void LiteConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
{
add(TR("console_available_commands"), ConsoleChannel::Info);
add(TR("console_help_clear"), ConsoleChannel::None);
add(TR("console_help_help"), ConsoleChannel::None);
add(TR("lite_console_help_passthrough"), ConsoleChannel::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