refactor(console): phase 5b — decompose toolbar/input/commands-popup methods

Finish the thin-view pass on the remaining large render methods (pure code
motion, no behavior change):

- renderToolbar (225 -> 99 lines): extracted drawToolbarStatus(), the
  drawLogFilterToggles() checkbox group, drawFilterInput(), and drawZoomControls().
- renderInput: factored the command echo + built-in interception + submit into
  submitConsoleCommand(exec, cmd) -> bool, leaving renderInput to own just the
  glass panel + InputText + completion/history callback.
- renderCommandsPopup (194 -> 135 lines): removed the duplicated lowercase-and-
  find filter logic (match-count loop and per-row filter now share
  consoleCommandMatchesFilter()) and extracted the [optional]-param dimming loop
  into drawConsoleCommandParams().

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 19:25:12 -05:00
parent 57291ef28c
commit 1c248d727a
2 changed files with 213 additions and 182 deletions

View File

@@ -366,30 +366,8 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + Layout::spacingMd());
// Backend status with colored dot (daemon state / lite connection).
{
ConsoleStatusLine st = exec.toolbarStatus();
if (!st.text.empty()) {
ImVec2 cp = ImGui::GetCursorScreenPos();
float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale();
float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f;
float dotX = cp.x + dotR + 2;
drawToolbarStatus(exec);
if (st.pulse) {
float a = schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-fast").size);
ImU32 pCol = (st.color & 0x00FFFFFF) | ((ImU32)(255 * a) << 24);
dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, pCol);
} else {
dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, st.color);
}
ImGui::Dummy(ImVec2(dotR * 2 + 6, 0));
ImGui::SameLine();
Type().textColored(TypeStyle::Caption, st.color, st.text.c_str());
} else {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon"));
}
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
@@ -406,49 +384,8 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
// Log filters (daemon/errors/rpc-trace/app) apply only to a daemon log source.
if (exec.hasLogFilters()) {
// Daemon messages toggle
ImGui::Checkbox(TR("console_daemon"), &s_daemon_messages_enabled);
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_daemon_output"));
drawLogFilterToggles();
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
// Errors-only toggle
ImGui::Checkbox(TR("console_errors"), &s_errors_only_enabled);
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_errors_only"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
// App RPC trace toggle — captures method/source only, never results or params
if (ImGui::Checkbox(TR("console_rpc_trace"), &s_rpc_trace_enabled)) {
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
}
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_rpc_trace"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
// App messages toggle — "[app] ..." wallet log lines
ImGui::Checkbox(TR("console_app"), &s_app_messages_enabled);
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_app_output"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
} // exec.hasLogFilters()
// Clear button
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
@@ -501,6 +438,90 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
ImGui::SameLine();
// Output filter input
drawFilterInput();
// Zoom +/- buttons (right side of toolbar)
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
drawZoomControls();
}
void ConsoleTab::drawToolbarStatus(ConsoleCommandExecutor& exec)
{
using namespace material;
ImDrawList* dl = ImGui::GetWindowDrawList();
ConsoleStatusLine st = exec.toolbarStatus();
if (!st.text.empty()) {
ImVec2 cp = ImGui::GetCursorScreenPos();
float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale();
float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f;
float dotX = cp.x + dotR + 2;
if (st.pulse) {
float a = schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-fast").size);
ImU32 pCol = (st.color & 0x00FFFFFF) | ((ImU32)(255 * a) << 24);
dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, pCol);
} else {
dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, st.color);
}
ImGui::Dummy(ImVec2(dotR * 2 + 6, 0));
ImGui::SameLine();
Type().textColored(TypeStyle::Caption, st.color, st.text.c_str());
} else {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon"));
}
}
void ConsoleTab::drawLogFilterToggles()
{
// Daemon messages toggle
ImGui::Checkbox(TR("console_daemon"), &s_daemon_messages_enabled);
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_daemon_output"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
// Errors-only toggle
ImGui::Checkbox(TR("console_errors"), &s_errors_only_enabled);
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_errors_only"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
// App RPC trace toggle — captures method/source only, never results or params
if (ImGui::Checkbox(TR("console_rpc_trace"), &s_rpc_trace_enabled)) {
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
}
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_rpc_trace"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
// App messages toggle — "[app] ..." wallet log lines
ImGui::Checkbox(TR("console_app"), &s_app_messages_enabled);
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("console_show_app_output"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
}
void ConsoleTab::drawFilterInput()
{
using namespace material;
float zoomBtnSpace = ImGui::GetFrameHeight() * 2.0f + Layout::spacingSm() * 3.0f;
float filterAvail = ImGui::GetContentRegionAvail().x - zoomBtnSpace;
float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
@@ -512,31 +533,29 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
Type().textColored(TypeStyle::Caption,
filter_match_count_ > 0 ? OnSurfaceMedium() : Error(), mc.c_str());
}
}
// Zoom +/- buttons (right side of toolbar)
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
{
auto& S = schema::UI();
float zoomStep = S.drawElement("tabs.console", "zoom-step").sizeOr(0.1f);
float zoomMin = S.drawElement("tabs.console", "zoom-min").sizeOr(0.5f);
float zoomMax = S.drawElement("tabs.console", "zoom-max").sizeOr(3.0f);
float btnSz = ImGui::GetFrameHeight();
void ConsoleTab::drawZoomControls()
{
using namespace material;
auto& S = schema::UI();
float zoomStep = S.drawElement("tabs.console", "zoom-step").sizeOr(0.1f);
float zoomMin = S.drawElement("tabs.console", "zoom-min").sizeOr(0.5f);
float zoomMax = S.drawElement("tabs.console", "zoom-max").sizeOr(3.0f);
float btnSz = ImGui::GetFrameHeight();
if (TactileButton(ICON_MD_REMOVE, ImVec2(btnSz, btnSz), Type().iconMed())) {
s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
}
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
}
ImGui::SameLine();
if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
}
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
}
if (TactileButton(ICON_MD_REMOVE, ImVec2(btnSz, btnSz), Type().iconMed())) {
s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
}
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
}
ImGui::SameLine();
if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
}
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
}
}
@@ -1108,34 +1127,7 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec)
if (tb == std::string::npos) cmd.clear(); else cmd = cmd.substr(tb);
while (!cmd.empty() && (cmd.back() == ' ' || cmd.back() == '\t')) cmd.pop_back();
if (!cmd.empty()) {
addLine("> " + cmd, ConsoleChannel::Command);
AppendConsoleHistory(command_history_, cmd, 100);
history_index_ = -1;
// First token, lowercased, for built-in interception.
std::string first;
{
size_t fb = cmd.find_first_not_of(" \t");
size_t fe = cmd.find_first_of(" \t", fb);
first = cmd.substr(fb, fe == std::string::npos ? std::string::npos : fe - fb);
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, ConsoleChannel c) { addLine(l, c); };
if (first == "clear" || first == "cls") {
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
clear();
selection_.clear();
} else if (first == "help") {
exec.printHelp(add);
} else if (first == "quit" || first == "exit") {
addLine(TR("console_quit_note"), ConsoleChannel::Info);
} else if (!exec.isReady()) {
addLine(TR("console_not_connected"), ConsoleChannel::Error);
} else {
exec.submit(cmd);
}
if (submitConsoleCommand(exec, cmd)) {
input_buffer_[0] = '\0';
reclaim_focus = true;
}
@@ -1150,6 +1142,94 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec)
}
}
bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::string& cmd)
{
if (cmd.empty()) return false;
addLine("> " + cmd, ConsoleChannel::Command);
AppendConsoleHistory(command_history_, cmd, 100);
history_index_ = -1;
// First token, lowercased, for built-in interception.
std::string first;
{
size_t fb = cmd.find_first_not_of(" \t");
size_t fe = cmd.find_first_of(" \t", fb);
first = cmd.substr(fb, fe == std::string::npos ? std::string::npos : fe - fb);
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, ConsoleChannel c) { addLine(l, c); };
if (first == "clear" || first == "cls") {
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
clear();
selection_.clear();
} else if (first == "help") {
exec.printHelp(add);
} else if (first == "quit" || first == "exit") {
addLine(TR("console_quit_note"), ConsoleChannel::Info);
} else if (!exec.isReady()) {
addLine(TR("console_not_connected"), ConsoleChannel::Error);
} else {
exec.submit(cmd);
}
return true;
}
namespace {
// True if any of a command's name/desc/params contains `filterLower` (already lowercased).
// An empty filter matches everything.
bool consoleCommandMatchesFilter(const char* name, const char* desc, const char* params,
const std::string& filterLower)
{
if (filterLower.empty()) return true;
auto contains = [&filterLower](const char* s) {
std::string v(s);
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
return v.find(filterLower) != std::string::npos;
};
return contains(name) || contains(desc) || contains(params);
}
// Draw a command's parameter string into the current table cell, dimming optional [params].
void drawConsoleCommandParams(const char* params)
{
using namespace material;
const char* p = params;
bool first = true;
while (*p) {
const char* bracketStart = strchr(p, '[');
if (bracketStart) {
// Draw the required part before the bracket.
if (bracketStart > p) {
if (!first) ImGui::SameLine(0, 0);
std::string req(p, bracketStart);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", req.c_str());
first = false;
}
const char* bracketEnd = strchr(bracketStart, ']');
if (bracketEnd) {
if (!first) ImGui::SameLine(0, 0);
std::string opt(bracketStart, bracketEnd + 1);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", opt.c_str());
first = false;
p = bracketEnd + 1;
} else {
if (!first) ImGui::SameLine(0, 0);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", bracketStart);
first = false;
break;
}
} else {
if (!first) ImGui::SameLine(0, 0);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", p);
first = false;
break;
}
}
}
} // namespace
void ConsoleTab::renderCommandsPopup()
{
using namespace material;
@@ -1190,15 +1270,8 @@ void ConsoleTab::renderCommandsPopup()
matchCount = cat.count;
} else {
for (int i = 0; i < cat.count; i++) {
std::string name(cat.commands[i].name);
std::string desc(cat.commands[i].desc);
std::string params(cat.commands[i].params);
std::transform(name.begin(), name.end(), name.begin(), ::tolower);
std::transform(desc.begin(), desc.end(), desc.begin(), ::tolower);
std::transform(params.begin(), params.end(), params.begin(), ::tolower);
if (name.find(filter) != std::string::npos ||
desc.find(filter) != std::string::npos ||
params.find(filter) != std::string::npos) {
if (consoleCommandMatchesFilter(cat.commands[i].name, cat.commands[i].desc,
cat.commands[i].params, filter)) {
matchCount++;
}
}
@@ -1234,20 +1307,9 @@ void ConsoleTab::renderCommandsPopup()
for (int i = 0; i < cat.count; i++) {
const auto& cmd = cat.commands[i];
std::string nameLower(cmd.name);
std::string descLower(cmd.desc);
std::string paramsLower(cmd.params);
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
std::transform(descLower.begin(), descLower.end(), descLower.begin(), ::tolower);
std::transform(paramsLower.begin(), paramsLower.end(), paramsLower.begin(), ::tolower);
if (!filter.empty() &&
nameLower.find(filter) == std::string::npos &&
descLower.find(filter) == std::string::npos &&
paramsLower.find(filter) == std::string::npos) {
if (!consoleCommandMatchesFilter(cmd.name, cmd.desc, cmd.params, filter))
continue;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushStyleColor(ImGuiCol_Text,
@@ -1275,48 +1337,7 @@ void ConsoleTab::renderCommandsPopup()
ImGui::PopStyleColor(3);
if (showParams) {
ImGui::TableNextColumn();
// Style optional params [param] in dimmed text
const char* p = cmd.params;
bool first = true;
while (*p) {
const char* bracketStart = strchr(p, '[');
if (bracketStart) {
// Draw required part before bracket
if (bracketStart > p) {
if (!first) ImGui::SameLine(0, 0);
std::string req(p, bracketStart);
ImGui::TextColored(
ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
"%s", req.c_str());
first = false;
}
// Find matching ]
const char* bracketEnd = strchr(bracketStart, ']');
if (bracketEnd) {
if (!first) ImGui::SameLine(0, 0);
std::string opt(bracketStart, bracketEnd + 1);
ImGui::TextColored(
ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
"%s", opt.c_str());
first = false;
p = bracketEnd + 1;
} else {
if (!first) ImGui::SameLine(0, 0);
ImGui::TextColored(
ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
"%s", bracketStart);
first = false;
break;
}
} else {
if (!first) ImGui::SameLine(0, 0);
ImGui::TextColored(
ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
"%s", p);
first = false;
break;
}
}
drawConsoleCommandParams(cmd.params);
}
ImGui::TableNextColumn();
ImGui::TextColored(

View File

@@ -100,10 +100,20 @@ private:
// 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);
void renderInput(ConsoleCommandExecutor& exec);
void renderCommandsPopup();
// renderToolbar() draws the top bar; these are its sub-steps:
void renderToolbar(ConsoleCommandExecutor& exec);
void drawToolbarStatus(ConsoleCommandExecutor& exec); // backend status dot + label
void drawLogFilterToggles(); // daemon/errors/rpc/app checkboxes
void drawFilterInput(); // text filter + match count
void drawZoomControls(); // zoom -/+ buttons
// renderInput() draws the command line; dispatch is factored out:
void renderInput(ConsoleCommandExecutor& exec);
// Echo + handle a submitted command line (built-ins + submit). True if non-empty/handled.
bool submitConsoleCommand(ConsoleCommandExecutor& exec, const std::string& cmd);
// renderOutput() orchestrates the scrollable output area; these are its sub-steps:
void renderOutput();
void computeVisibleLines(bool& hasTextFilter, std::string& filterLower); // -> visible_indices_