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:
24
src/app.cpp
24
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
|
||||
|
||||
35
src/ui/windows/console_channel.h
Normal file
35
src/ui/windows/console_channel.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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});
|
||||
|
||||
Reference in New Issue
Block a user