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>
This commit is contained in:
2026-07-01 18:22:35 -05:00
parent 19e83eb12a
commit 33735b1d52
9 changed files with 219 additions and 175 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<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);
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"), ConsoleTab::COLOR_INFO);
add(TR("console_daemon_started"), ConsoleChannel::Info);
} else if (cur == DState::Stopped && prev == static_cast<int>(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<int>(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<std::size_t>(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<ConsoleStatusLine> LiteConsoleExecutor::statusLines() const

View File

@@ -9,6 +9,7 @@
#pragma once
#include "console_channel.h"
#include "imgui.h"
#include <cstdint>
@@ -23,7 +24,7 @@ namespace dragonx {
class App;
namespace ui {
using ConsoleAddLineFn = std::function<void(const std::string&, ImU32)>;
using ConsoleAddLineFn = std::function<void(const std::string&, ConsoleChannel)>;
struct ConsoleStatusLine {
std::string text;

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
#pragma once
#include "imgui.h"
#include "console_channel.h"
#include <string>
@@ -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

View File

@@ -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<std::mutex> 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<int>(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<char>(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<std::mutex> 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

View File

@@ -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);

View File

@@ -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<dragonx::AddressInfo> poolAddresses;
poolAddresses.push_back({"R-transparent", 0.0, "transparent", true});