From 33735b1d5210d9802199c6d837433eacde63e987 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 1 Jul 2026 18:22:35 -0500 Subject: [PATCH] =?UTF-8?q?refactor(console):=20phase=202=20=E2=80=94=20ch?= =?UTF-8?q?annel-as-semantic=20key;=20decouple=20executor=20from=20UI=20co?= =?UTF-8?q?lors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/app.cpp | 24 +-- src/ui/windows/console_channel.h | 35 +++++ src/ui/windows/console_command_executor.cpp | 88 +++++------ src/ui/windows/console_command_executor.h | 3 +- src/ui/windows/console_output_model.cpp | 12 +- src/ui/windows/console_output_model.h | 16 +- src/ui/windows/console_tab.cpp | 165 ++++++++++---------- src/ui/windows/console_tab.h | 19 +-- tests/test_phase4.cpp | 32 ++-- 9 files changed, 219 insertions(+), 175 deletions(-) create mode 100644 src/ui/windows/console_channel.h diff --git a/src/app.cpp b/src/app.cpp index ab29e91..f941644 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -396,37 +396,39 @@ bool App::init() worker_->start(); network_refresh_.markDue(services::NetworkRefreshService::Timer::Price); - // Forward error/warning notifications to the console tab - // Use ConsoleTab colors so the Errors filter works correctly + // Forward error/warning notifications to the console tab. The channel is the semantic + // key — the Errors filter keys off ConsoleChannel::Error, and the color/accent bar are + // derived from the channel at draw time. ui::Notifications::instance().setConsoleCallback( [this](const std::string& msg, bool is_error) { - ImU32 color = is_error ? ui::ConsoleTab::COLOR_ERROR : ui::material::Warning(); - console_tab_.addLine(msg, color); + console_tab_.addLine(msg, is_error ? ui::ConsoleChannel::Error + : ui::ConsoleChannel::Warning); }); // Forward all app-level log messages (DEBUG_LOGF, LOGF, etc.) to the // console tab so they are visible in the UI, not just in the log file. util::Logger::instance().setCallback( [this](const std::string& msg) { - // Classify by content: errors in red, warnings in warning color, - // everything else in the default info color. - ImU32 color = ui::ConsoleTab::COLOR_INFO; + // Classify by content into a severity channel: errors, warnings, else the App + // channel (the routine wallet log stream, controlled by the App messages filter). + ui::ConsoleChannel channel = ui::ConsoleChannel::App; if (msg.find("[ERROR]") != std::string::npos || msg.find("error") != std::string::npos || msg.find("Error") != std::string::npos || msg.find("failed") != std::string::npos || msg.find("Failed") != std::string::npos) { - color = ui::ConsoleTab::COLOR_ERROR; + channel = ui::ConsoleChannel::Error; } else if (msg.find("[WARN]") != std::string::npos || msg.find("warn") != std::string::npos) { - color = ui::material::Warning(); + channel = ui::ConsoleChannel::Warning; } - // Strip trailing newline so console tab lines look clean + // Strip trailing newline so console tab lines look clean. The App channel + // supplies the accent bar; no "[app] " text prefix needed. std::string trimmed = msg; while (!trimmed.empty() && (trimmed.back() == '\n' || trimmed.back() == '\r')) trimmed.pop_back(); if (!trimmed.empty()) - console_tab_.addLine("[app] " + trimmed, color); + console_tab_.addLine(trimmed, channel); }); // Check for first-run wizard — also re-run if blockchain data is missing diff --git a/src/ui/windows/console_channel.h b/src/ui/windows/console_channel.h new file mode 100644 index 0000000..c705274 --- /dev/null +++ b/src/ui/windows/console_channel.h @@ -0,0 +1,35 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 +// +// ConsoleChannel — the semantic classification of a console line. It is the canonical +// key: producers (the command executor, app log forwarders, the 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 older +// approach of storing an ImU32 color and reverse-engineering the source from color +// equality + a "[daemon] " text prefix. + +#pragma once + +namespace dragonx { +namespace ui { + +enum class ConsoleChannel { + None = 0, // plain result / default text + Command, // the "> cmd" echo + Info, // informational / help text + Success, // positive events (connected, accepted, ready) + Warning, // non-fatal warnings (amber) + Error, // errors + Rpc, // RPC trace lines + Daemon, // dragonxd log output + Xmrig, // miner log output + App, // app / wallet / lite-diagnostics log + JsonKey, // result JSON: object key + JsonString, // result JSON: string value + JsonNumber, // result JSON: number / bool / null + JsonBrace, // result JSON: braces / brackets +}; + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/console_command_executor.cpp b/src/ui/windows/console_command_executor.cpp index 0e3b253..aefb863 100644 --- a/src/ui/windows/console_command_executor.cpp +++ b/src/ui/windows/console_command_executor.cpp @@ -3,7 +3,7 @@ // Released under the GPLv3 #include "console_command_executor.h" -#include "console_tab.h" // ConsoleTab::COLOR_* statics +#include "console_channel.h" #include "console_input_model.h" // BuildConsoleRpcCall #include "../../app.h" @@ -103,17 +103,17 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) 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); + 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(DState::Running)) { - add(TR("console_daemon_started"), ConsoleTab::COLOR_INFO); + add(TR("console_daemon_started"), ConsoleChannel::Info); } else if (cur == DState::Stopped && prev == static_cast(DState::Running)) { - add("", ConsoleTab::COLOR_RESULT); - add(TR("console_daemon_stopped"), ConsoleTab::COLOR_INFO); + add("", ConsoleChannel::None); + add(TR("console_daemon_stopped"), ConsoleChannel::Info); } else if (cur == DState::Error) { - add(std::string(TR("console_daemon_error")) + d->getLastError() + " ===", ConsoleTab::COLOR_ERROR); + add(std::string(TR("console_daemon_error")) + d->getLastError() + " ===", ConsoleChannel::Error); } last_daemon_state_ = static_cast(cur); } @@ -121,8 +121,8 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) 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); + 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; } @@ -133,12 +133,12 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) std::string line; while (std::getline(ss, line)) { if (line.empty()) continue; - ImU32 c = ConsoleTab::COLOR_DAEMON; + ConsoleChannel c = ConsoleChannel::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); + c = ConsoleChannel::Error; + add(line, c); // channel conveys the source; no "[daemon] " prefix } } } @@ -151,14 +151,14 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) std::string line; while (std::getline(ss, line)) { if (line.empty()) continue; - ImU32 c = ConsoleTab::COLOR_DAEMON; + ConsoleChannel c = ConsoleChannel::Xmrig; if (line.find("error") != std::string::npos || line.find("ERROR") != std::string::npos || line.find("failed") != std::string::npos) - c = ConsoleTab::COLOR_ERROR; + c = ConsoleChannel::Error; else if (line.find("accepted") != std::string::npos) - c = ConsoleTab::COLOR_INFO; - add("[xmrig] " + line, c); + c = ConsoleChannel::Success; + add(line, c); // channel conveys the source; no "[xmrig] " prefix } } } else { @@ -168,22 +168,22 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) 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); + 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 @@ -207,16 +207,16 @@ ConsoleStatusLine FullNodeConsoleExecutor::toolbarStatus() const // ============================================================================ namespace { -// Colour error/success lines from the lite diagnostics stream for at-a-glance scanning. -ImU32 liteLogColor(const std::string& line) +// 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 Error(); + return ConsoleChannel::Error; if (has(": connected") || has("opened") || has("wallet ready") || has("Ready")) - return Success(); - return OnSurfaceMedium(); + return ConsoleChannel::Success; + return ConsoleChannel::App; // lite diagnostics / wallet log } } // namespace @@ -263,16 +263,16 @@ void LiteConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) 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])); + add(snap[i], liteLogChannel(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); + 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 LiteConsoleExecutor::statusLines() const diff --git a/src/ui/windows/console_command_executor.h b/src/ui/windows/console_command_executor.h index 83289d1..1cf640c 100644 --- a/src/ui/windows/console_command_executor.h +++ b/src/ui/windows/console_command_executor.h @@ -9,6 +9,7 @@ #pragma once +#include "console_channel.h" #include "imgui.h" #include @@ -23,7 +24,7 @@ namespace dragonx { class App; namespace ui { -using ConsoleAddLineFn = std::function; +using ConsoleAddLineFn = std::function; struct ConsoleStatusLine { std::string text; diff --git a/src/ui/windows/console_output_model.cpp b/src/ui/windows/console_output_model.cpp index 53d531c..ba2eaaf 100644 --- a/src/ui/windows/console_output_model.cpp +++ b/src/ui/windows/console_output_model.cpp @@ -16,14 +16,14 @@ std::string lowerCopy(std::string value) } bool consoleLinePassesFilter(const std::string& lineText, - ImU32 lineColor, + ConsoleChannel channel, const ConsoleOutputFilter& filter) { - if (!filter.daemonMessagesEnabled && lineColor == filter.daemonColor) return false; - if (!filter.rpcTraceEnabled && lineColor == filter.rpcTraceColor) return false; - // "[app] ..." lines share COLOR_INFO with other info text, so match them by prefix. - if (!filter.appMessagesEnabled && lineText.rfind("[app] ", 0) == 0) return false; - if (filter.errorsOnly && lineColor != filter.errorColor) return false; + if (!filter.daemonMessagesEnabled && + (channel == ConsoleChannel::Daemon || channel == ConsoleChannel::Xmrig)) return false; + if (!filter.rpcTraceEnabled && channel == ConsoleChannel::Rpc) return false; + if (!filter.appMessagesEnabled && channel == ConsoleChannel::App) return false; + if (filter.errorsOnly && channel != ConsoleChannel::Error) return false; if (!filter.text.empty()) { std::string needle = lowerCopy(filter.text); std::string haystack = lowerCopy(lineText); diff --git a/src/ui/windows/console_output_model.h b/src/ui/windows/console_output_model.h index 431d13b..485a93f 100644 --- a/src/ui/windows/console_output_model.h +++ b/src/ui/windows/console_output_model.h @@ -1,6 +1,6 @@ #pragma once -#include "imgui.h" +#include "console_channel.h" #include @@ -9,17 +9,15 @@ namespace ui { struct ConsoleOutputFilter { std::string text; - bool daemonMessagesEnabled = true; - bool errorsOnly = false; - bool rpcTraceEnabled = false; - bool appMessagesEnabled = true; // "[app] ..." wallet log lines (matched by prefix, not color) - ImU32 daemonColor = 0; - ImU32 errorColor = 0; - ImU32 rpcTraceColor = 0; + bool daemonMessagesEnabled = true; // Daemon + Xmrig (node log) channels + bool errorsOnly = false; // keep only the Error channel + bool rpcTraceEnabled = false; // Rpc channel + bool appMessagesEnabled = true; // App channel }; +// Whether a line with the given channel + text passes the active filter. bool consoleLinePassesFilter(const std::string& lineText, - ImU32 lineColor, + ConsoleChannel channel, const ConsoleOutputFilter& filter); } // namespace ui diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index b21051e..bdace00 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -72,6 +72,22 @@ std::string rpcTraceTimestamp() return buffer; } +// Result-body channels (plain command output + JSON syntax roles) — the lines that carry +// no left accent bar and get the JSON indent guides. +bool isResultBodyChannel(ConsoleChannel ch) +{ + switch (ch) { + case ConsoleChannel::None: + case ConsoleChannel::JsonKey: + case ConsoleChannel::JsonString: + case ConsoleChannel::JsonNumber: + case ConsoleChannel::JsonBrace: + return true; + default: + return false; + } +} + } // namespace void ConsoleTab::refreshColors() @@ -112,6 +128,29 @@ void ConsoleTab::refreshColors() } } +ImU32 ConsoleTab::channelTextColor(ConsoleChannel channel) const +{ + using namespace material; + switch (channel) { + case ConsoleChannel::Command: return COLOR_COMMAND; + case ConsoleChannel::Info: return COLOR_INFO; + case ConsoleChannel::Success: return WithAlpha(Success(), 255); + case ConsoleChannel::Warning: return Warning(); + case ConsoleChannel::Error: return COLOR_ERROR; + case ConsoleChannel::Rpc: return COLOR_RPC; + case ConsoleChannel::Daemon: return COLOR_DAEMON; + case ConsoleChannel::Xmrig: return COLOR_DAEMON; + case ConsoleChannel::App: return COLOR_INFO; + // JSON syntax roles — highlight against the plain-result body. + case ConsoleChannel::JsonKey: return WithAlpha(Secondary(), 255); + case ConsoleChannel::JsonString: return WithAlpha(Success(), 255); + case ConsoleChannel::JsonNumber: return WithAlpha(Warning(), 255); + case ConsoleChannel::JsonBrace: return IM_COL32(200, 200, 200, 150); + case ConsoleChannel::None: + default: return COLOR_RESULT; + } +} + ConsoleTab::ConsoleTab() { { @@ -132,9 +171,9 @@ ConsoleTab::ConsoleTab() refreshColors(); // Add welcome message - addLine(TR("console_welcome"), COLOR_INFO); - addLine(TR("console_type_help"), COLOR_INFO); - addLine("", COLOR_RESULT); + addLine(TR("console_welcome"), ConsoleChannel::Info); + addLine(TR("console_type_help"), ConsoleChannel::Info); + addLine("", ConsoleChannel::None); } ConsoleTab::~ConsoleTab() @@ -149,35 +188,20 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec) { using namespace material; - // Refresh console colors when dark/light theme changes + // Refresh the console theme colors on a dark/light switch. Line colors are derived + // from each line's channel at draw time, so no per-line remap is needed. { static bool s_lastDark = IsDarkTheme(); bool nowDark = IsDarkTheme(); if (nowDark != s_lastDark) { - // Save old colors to remap existing lines - ImU32 oldCmd = COLOR_COMMAND, oldRes = COLOR_RESULT; - ImU32 oldErr = COLOR_ERROR, oldDmn = COLOR_DAEMON; - ImU32 oldInf = COLOR_INFO, oldRpc = COLOR_RPC; refreshColors(); - // Remap stored line colors from old to new - { - std::lock_guard lock(lines_mutex_); - for (auto& line : lines_) { - if (line.color == oldCmd) line.color = COLOR_COMMAND; - else if (line.color == oldRes) line.color = COLOR_RESULT; - else if (line.color == oldErr) line.color = COLOR_ERROR; - else if (line.color == oldDmn) line.color = COLOR_DAEMON; - else if (line.color == oldInf) line.color = COLOR_INFO; - else if (line.color == oldRpc) line.color = COLOR_RPC; - } - } s_lastDark = nowDark; } } // 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); }); + exec.pollLogLines([this](const std::string& l, ConsoleChannel c) { addLine(l, c); }); { std::string result; bool isError = false; @@ -579,8 +603,7 @@ void ConsoleTab::renderOutput() // Build filtered line index list BEFORE mouse handling (so screenToTextPos works) ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled, s_errors_only_enabled, s_rpc_trace_enabled, - s_app_messages_enabled, - COLOR_DAEMON, COLOR_ERROR, COLOR_RPC}; + s_app_messages_enabled}; bool has_text_filter = !outputFilter.text.empty(); // Lowercased needle for in-line match highlighting. std::string filter_lower; @@ -595,7 +618,7 @@ void ConsoleTab::renderOutput() visible_indices_.clear(); if (has_filter) { for (int i = 0; i < static_cast(lines_.size()); i++) { - if (!consoleLinePassesFilter(lines_[i].text, lines_[i].color, outputFilter)) continue; + if (!consoleLinePassesFilter(lines_[i].text, lines_[i].channel, outputFilter)) continue; visible_indices_.push_back(i); } } else { @@ -745,17 +768,19 @@ void ConsoleTab::renderOutput() // Left-edge channel accent bar, drawn in the padX margin so it never overlaps // the text or the selection highlight. - if (line.channel != CH_NONE) { + if (line.channel != ConsoleChannel::None) { ImU32 barCol = 0; switch (line.channel) { - case CH_COMMAND: barCol = Primary(); break; - case CH_ERROR: barCol = Error(); break; - case CH_RPC: barCol = Secondary(); break; - case CH_DAEMON: barCol = IM_COL32(90, 130, 190, 210); break; // node log — blue - case CH_XMRIG: barCol = Warning(); break; // mining — amber - case CH_APP: barCol = IM_COL32(120, 190, 160, 210); break; // app — teal - case CH_INFO: barCol = OnSurfaceDisabled(); break; - default: break; + case ConsoleChannel::Command: barCol = Primary(); break; + case ConsoleChannel::Error: barCol = Error(); break; + case ConsoleChannel::Warning: barCol = Warning(); break; + case ConsoleChannel::Success: barCol = Success(); break; + case ConsoleChannel::Rpc: barCol = Secondary(); break; + case ConsoleChannel::Daemon: barCol = IM_COL32(90, 130, 190, 210); break; // node log — blue + case ConsoleChannel::Xmrig: barCol = Warning(); break; // mining — amber + case ConsoleChannel::App: barCol = IM_COL32(120, 190, 160, 210); break; // app — teal + case ConsoleChannel::Info: barCol = OnSurfaceDisabled(); break; + default: break; // result / JSON roles get no bar } if (barCol != 0) { float barW = 3.0f * Layout::hScale(); @@ -767,8 +792,8 @@ void ConsoleTab::renderOutput() } // JSON indent guides — faint vertical lines per 2-space nesting level, on result - // (non-channel) lines only. Draw-only, like the channel bar. - if (line.channel == CH_NONE) { + // body lines only (plain result + JSON syntax roles). Draw-only, like the bar. + if (isResultBodyChannel(line.channel)) { size_t leading = 0; while (leading < line.text.size() && line.text[leading] == ' ') ++leading; if (leading >= 2) { @@ -846,7 +871,7 @@ void ConsoleTab::renderOutput() if (seg.byteStart < seg.byteEnd) { dl->AddText(font, fontSize, ImVec2(lineOrigin.x, rowY), - line.color, segStart, segEnd); + channelTextColor(line.channel), segStart, segEnd); } } @@ -1094,9 +1119,9 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec) data->InsertChars(0, completion.matches.front().c_str()); } else if (completion.matches.size() > 1) { // Multiple matches — show list in console and complete common prefix - console->addLine(TR("console_completions"), ConsoleTab::COLOR_INFO); + console->addLine(TR("console_completions"), ConsoleChannel::Info); for (const auto& line : FormatConsoleCompletionLines(completion.matches)) { - console->addLine(line, ConsoleTab::COLOR_RESULT); + console->addLine(line, ConsoleChannel::None); } // Complete to longest common prefix @@ -1121,7 +1146,7 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec) while (!cmd.empty() && (cmd.back() == ' ' || cmd.back() == '\t')) cmd.pop_back(); if (!cmd.empty()) { - addLine("> " + cmd, COLOR_COMMAND); + addLine("> " + cmd, ConsoleChannel::Command); AppendConsoleHistory(command_history_, cmd, 100); history_index_ = -1; @@ -1134,7 +1159,7 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec) 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); }; + auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); }; if (first == "clear" || first == "cls") { // View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history). clear(); @@ -1142,9 +1167,9 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec) } else if (first == "help") { exec.printHelp(add); } else if (first == "quit" || first == "exit") { - addLine(TR("console_quit_note"), COLOR_INFO); + addLine(TR("console_quit_note"), ConsoleChannel::Info); } else if (!exec.isReady()) { - addLine(TR("console_not_connected"), COLOR_ERROR); + addLine(TR("console_not_connected"), ConsoleChannel::Error); } else { exec.submit(cmd); } @@ -1358,23 +1383,17 @@ void ConsoleTab::renderCommandsPopup() void ConsoleTab::addFormattedResult(const std::string& result, bool is_error) { - 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, is_error)) { - ImU32 lineCol = COLOR_RESULT; + ConsoleChannel channel = ConsoleChannel::None; 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::Error: channel = ConsoleChannel::Error; break; + case ConsoleResultLineRole::JsonKey: channel = ConsoleChannel::JsonKey; break; + case ConsoleResultLineRole::JsonString: channel = ConsoleChannel::JsonString; break; + case ConsoleResultLineRole::JsonNumber: channel = ConsoleChannel::JsonNumber; break; + case ConsoleResultLineRole::JsonBrace: channel = ConsoleChannel::JsonBrace; break; case ConsoleResultLineRole::Result: break; } - addLine(resultLine.text, lineCol); + addLine(resultLine.text, channel); } } @@ -1387,36 +1406,15 @@ void ConsoleTab::renderStatusHeader(ConsoleCommandExecutor& exec) if (!lines.empty()) ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } -void ConsoleTab::addLine(const std::string& line, ImU32 color) +void ConsoleTab::addLine(const std::string& line, ConsoleChannel channel) { std::lock_guard lock(lines_mutex_); - // Infer the channel (for the left accent bar) from the color, then strip the now - // redundant "[daemon]"/"[xmrig]"/"[app]" text prefix so the bar carries the source. - int channel = CH_NONE; - if (color == COLOR_ERROR) channel = CH_ERROR; - else if (color == COLOR_RPC) channel = CH_RPC; - else if (color == COLOR_COMMAND) channel = CH_COMMAND; - else if (color == COLOR_DAEMON) channel = CH_DAEMON; - else if (color == COLOR_INFO) channel = CH_INFO; + // The channel is the canonical semantic key — text color and the left accent bar are + // both derived from it at draw time (channelTextColor / the bar switch). Sources emit + // clean text (no "[daemon]"/"[xmrig]"/"[app]"/"[rpc]" prefix); the bar carries origin. + lines_.push_back({line, channel}); - std::string text = line; - auto stripPrefix = [&text, &channel](const char* p, int ch) { - const size_t n = std::strlen(p); - if (text.rfind(p, 0) == 0) { - text.erase(0, n); - // Keep an error channel (red accent) for e.g. a "[daemon] error:" line so the - // bar matches the red text, rather than being downgraded to the source channel. - if (channel != CH_ERROR) channel = ch; - } - }; - stripPrefix("[daemon] ", CH_DAEMON); - stripPrefix("[xmrig] ", CH_XMRIG); - stripPrefix("[app] ", CH_APP); - stripPrefix("[rpc] ", CH_RPC); - - lines_.push_back({text, color, channel}); - // Limit buffer size — adjust selection indices when lines are removed // from the front so the highlight stays on the text the user selected. int popped = 0; @@ -1445,7 +1443,8 @@ void ConsoleTab::addLine(const std::string& line, ImU32 color) void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method) { - addLine("[rpc] [" + rpcTraceTimestamp() + "] [" + source + "] " + method, COLOR_RPC); + // The Rpc channel supplies the accent bar + color; no "[rpc] " text prefix needed. + addLine("[" + rpcTraceTimestamp() + "] [" + source + "] " + method, ConsoleChannel::Rpc); } void ConsoleTab::clear() @@ -1456,7 +1455,7 @@ void ConsoleTab::clear() } // 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); + addLine(TR("console_cleared"), ConsoleChannel::Info); } } // namespace ui diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index 75f382e..dc672b2 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -4,6 +4,7 @@ #pragma once +#include "console_channel.h" #include "console_text_layout.h" #include "../layout.h" #include "../../daemon/embedded_daemon.h" @@ -46,7 +47,7 @@ public: /** * @brief Add a line to the console output */ - void addLine(const std::string& line, ImU32 color = IM_COL32(200, 200, 200, 255)); + void addLine(const std::string& line, ConsoleChannel channel = ConsoleChannel::None); void addRpcTraceLine(const std::string& source, const std::string& method); /** @@ -84,17 +85,17 @@ public: static ImU32 COLOR_RPC; private: - // Line source, for the left-edge channel accent bar. Inferred in addLine() from the - // color + any "[daemon]"/"[xmrig]"/"[app]" prefix (which is then stripped). - enum Channel { CH_NONE = 0, CH_COMMAND, CH_INFO, CH_ERROR, CH_RPC, CH_DAEMON, CH_XMRIG, CH_APP }; - + // Each line stores its semantic ConsoleChannel; the text color and left accent-bar + // color are both derived from it at draw time (channelTextColor / channelBarColor). struct ConsoleLine { - std::string text; - ImU32 color; - int channel = CH_NONE; + std::string text; + ConsoleChannel channel = ConsoleChannel::None; }; - // Format + color a completed command result (JSON-aware) into console lines. + // Text color for a channel, resolved from the current theme (COLOR_* + material). + ImU32 channelTextColor(ConsoleChannel channel) const; + + // Format a completed command result (JSON role -> channel) into console lines. void addFormattedResult(const std::string& result, bool is_error); void renderStatusHeader(ConsoleCommandExecutor& exec); void renderToolbar(ConsoleCommandExecutor& exec); diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 7d7f0d8..050843e 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -2354,26 +2354,34 @@ void testRendererHelpers() EXPECT_FALSE(dragonx::ui::IsPoolMiningActive(true, false, true)); EXPECT_TRUE(dragonx::ui::IsPoolMiningActive(false, false, true)); - dragonx::ui::ConsoleOutputFilter filter{"error", false, false, false, true, 10, 20, 30}; - EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("RPC Error", 20, filter)); - EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("daemon line", 10, filter)); + // The output filter keys off the line's semantic ConsoleChannel (not a color / prefix). + using CC = dragonx::ui::ConsoleChannel; + // {text, daemonMessagesEnabled, errorsOnly, rpcTraceEnabled, appMessagesEnabled} + dragonx::ui::ConsoleOutputFilter filter{"error", false, false, false, true}; + EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("RPC Error", CC::Error, filter)); // substring "error" + EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("Daemon line", CC::Daemon, filter)); // no "error" substring filter.text.clear(); - EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("[rpc] History -> listtransactions", 30, filter)); + // Daemon/Xmrig channels hidden while daemonMessagesEnabled=false. + EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("node log", CC::Daemon, filter)); + EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("hashrate 1000", CC::Xmrig, filter)); + // Rpc channel hidden while rpcTraceEnabled=false, shown once enabled. + EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("History -> listtransactions", CC::Rpc, filter)); filter.rpcTraceEnabled = true; - EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("[rpc] History -> listtransactions", 30, filter)); + EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("History -> listtransactions", CC::Rpc, filter)); filter.daemonMessagesEnabled = true; + // Errors-only keeps only the Error channel. filter.errorsOnly = true; - filter.text.clear(); - EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("error line", 20, filter)); - EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("info line", 30, filter)); + EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("error line", CC::Error, filter)); + EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("info line", CC::Info, filter)); + EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("node log", CC::Daemon, filter)); - // "[app] ..." lines are filtered by prefix (they share the info color), not by color. + // App channel is toggled by appMessagesEnabled; other channels are unaffected by it. filter.errorsOnly = false; filter.appMessagesEnabled = false; - EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("[app] Price updated", 30, filter)); - EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("[rpc] trace", 30, filter)); + EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("Price updated", CC::App, filter)); + EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("trace", CC::Rpc, filter)); filter.appMessagesEnabled = true; - EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("[app] Price updated", 30, filter)); + EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("Price updated", CC::App, filter)); std::vector poolAddresses; poolAddresses.push_back({"R-transparent", 0.0, "transparent", true});