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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user