feat(console): redesign the RPC reference as a novice-friendly command explorer

Replace the flat, terse command-reference table with a guided two-pane
explorer built for users who don't know the RPC commands:

- Search by name OR task: keyword synonyms let "balance" find getbalance,
  "send money" find sendtoaddress; results are ranked (name-prefix > name >
  keyword > desc).
- Two panes (mirrors the portfolio editor): a category-grouped master list
  (mono names, a warning dot on consequential commands) + a rich detail pane
  with a plain-language explanation, a per-parameter type breakdown (string /
  number / json, optional dimmed), and a concrete example.
- Insert into console (fills the input with a template to review + run) and,
  for no-parameter commands, Insert & run — with a confirm for destructive
  ones. Deferred via pending_submit_ so the modal needs no executor.
- Keyboard: type to filter, Up/Down to move, Enter to insert; auto-focus and
  auto-select the top result.

Data model: ConsoleCommandEntry gains details/example/keywords/destructive
(C++17 defaults, so the ~60 un-enriched entries are untouched); ~20 common
commands enriched and 18 consequential ones flagged. Command docs stay English
(technical); the UI chrome + 7 category names are translated into all 8
languages (CJK subset rebuilt, +3 glyphs).

Verified: two-pane renders on dark + light skins and at font_scale 1.5; detail
pane shows explanation/params/example; danger dots + safety badge on
destructive commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 11:07:49 -05:00
co-authored by Claude Opus 4.8
parent ab4dd370c0
commit c9e9c9f979
14 changed files with 536 additions and 214 deletions
+300 -172
View File
@@ -284,6 +284,14 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
}
}
// Run a command the RPC-reference modal's "Insert & run" queued (the modal has no executor, so it
// defers to here). Done before drain so the echoed "> cmd" line surfaces this frame.
if (!pending_submit_.empty()) {
std::string cmd;
cmd.swap(pending_submit_);
submitConsoleCommand(exec, cmd);
}
// 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, ConsoleChannel c) { addLine(l, c); });
@@ -1439,54 +1447,52 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
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)
static bool lcContains(const char* s, const std::string& needleLower)
{
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);
std::string v(s);
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
return v.find(needleLower) != std::string::npos;
}
static bool lcStartsWith(const char* s, const std::string& needleLower)
{
std::string v(s);
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
return v.rfind(needleLower, 0) == 0;
}
// Draw a command's parameter string into the current table cell, dimming optional [params].
void drawConsoleCommandParams(const char* params)
// Search relevance of a command for a lowercased query. -1 = no match; higher = better. Keywords
// let novices find a command by intent ("balance" -> getbalance) even without the exact name.
int consoleCommandRank(const ConsoleCommandEntry& cmd, const std::string& q)
{
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;
}
if (q.empty()) return 0;
if (lcStartsWith(cmd.name, q)) return 100;
if (lcContains(cmd.name, q)) return 60;
if (cmd.keywords[0] && lcContains(cmd.keywords, q)) return 40;
if (lcContains(cmd.desc, q)) return 25;
if (cmd.details[0] && lcContains(cmd.details, q)) return 15;
if (lcContains(cmd.params, q)) return 10;
return -1;
}
// Split a parameter template into top-level tokens, respecting quote/bracket nesting so a space
// inside "..." or [{...}] doesn't split (e.g. `"address" [{"a":1}]` -> {`"address"`, `[{"a":1}]`}).
std::vector<std::string> splitParamTemplate(const char* params)
{
std::vector<std::string> out;
std::string tok;
int depth = 0;
char q = 0;
for (const char* p = params; *p; ++p) {
char c = *p;
if (q) { tok += c; if (c == q) q = 0; continue; }
if (c == '"' || c == '\'') { q = c; tok += c; continue; }
if (c == '[' || c == '{') { depth++; tok += c; continue; }
if (c == ']' || c == '}') { if (depth > 0) depth--; tok += c; continue; }
if (c == ' ' && depth == 0) { if (!tok.empty()) { out.push_back(tok); tok.clear(); } continue; }
tok += c;
}
if (!tok.empty()) out.push_back(tok);
return out;
}
// Translate a command-category name from the static reference tables (English) for display. The
@@ -1504,157 +1510,279 @@ const char* consoleCategoryLabel(const char* name)
}
} // namespace
void ConsoleTab::insertCommandToInput(const ConsoleCommandEntry& cmd)
{
// Fill the console input with the command (+ its param template) and close the modal — the user
// reviews/edits it and presses Enter to run.
if (cmd.params[0] != '\0')
snprintf(input_buffer_, sizeof(input_buffer_), "%s %s", cmd.name, cmd.params);
else {
strncpy(input_buffer_, cmd.name, sizeof(input_buffer_) - 1);
input_buffer_[sizeof(input_buffer_) - 1] = '\0';
}
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel)
{
using namespace material;
float dp = Layout::dpiScale();
ImFont* mono = Type().mono();
// Heading: command name, then category + a safety badge for consequential commands.
ImGui::PushFont(Type().subtitle1());
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(IM_COL32(120, 190, 255, 255)), "%s", cmd.name);
ImGui::PopFont();
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", catLabel);
if (cmd.destructive) {
ImGui::SameLine(0, Layout::spacingMd());
ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning());
ImGui::PushFont(Type().iconSmall());
ImGui::TextColored(warn, ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(warn, "%s", TR("console_ref_destructive"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Plain-language explanation (falls back to the terse summary when not enriched).
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurface()), "%s",
cmd.details[0] ? cmd.details : cmd.desc);
ImGui::PopTextWrapPos();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Parameters — each token with a rough type tag; optional ([...]) tokens are dimmed.
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_parameters"));
std::vector<std::string> toks = splitParamTemplate(cmd.params);
if (toks.empty()) {
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", TR("console_ref_no_params"));
} else {
ImGui::Indent(Layout::spacingMd());
for (const std::string& t : toks) {
bool optional = (!t.empty() && t.front() == '[');
char c0 = t.empty() ? 0 : t[0];
if (optional && t.size() >= 2) c0 = t[1];
const char* type = (c0 == '"' || c0 == '\'') ? "string"
: (c0 == '{' || c0 == '[') ? "json" : "number";
ImGui::PushFont(mono);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(optional ? OnSurfaceMedium() : OnSurface()),
"%s", t.c_str());
ImGui::PopFont();
ImGui::SameLine();
if (optional)
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
" %s \xC2\xB7 %s", type, TR("console_ref_optional"));
else
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), " %s", type);
}
ImGui::Unindent(Layout::spacingMd());
}
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Example (curated commands only).
if (cmd.example[0]) {
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_example"));
ImGui::PushFont(mono);
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(IM_COL32(150, 200, 150, 255)), "%s", cmd.example);
ImGui::PopTextWrapPos();
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
}
// Actions (or the destructive run confirmation).
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (run_confirm_cmd_ == &cmd) {
ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning());
ImGui::PushFont(Type().iconSmall());
ImGui::TextColored(warn, ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(warn, TR("console_ref_run_confirm"), cmd.name);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float cbw = 120.0f * dp;
if (material::TactileButton(TR("console_ref_cancel"), ImVec2(cbw, 0))) run_confirm_cmd_ = nullptr;
ImGui::SameLine();
if (material::TactileButton(TR("console_ref_run"), ImVec2(cbw, 0))) {
pending_submit_ = cmd.name;
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
} else {
if (material::TactileButton(TR("console_ref_insert"), ImVec2(200.0f * dp, 0)))
insertCommandToInput(cmd);
// "Insert & run" only for commands that take no parameters — running a template with
// placeholders ("address", amount) would just error.
if (cmd.params[0] == '\0') {
ImGui::SameLine();
if (material::TactileButton(TR("console_ref_insert_run"), ImVec2(140.0f * dp, 0))) {
if (cmd.destructive) {
run_confirm_cmd_ = &cmd;
} else {
pending_submit_ = cmd.name;
command_search_[0] = '\0';
show_commands_popup_ = false;
}
}
}
}
}
void ConsoleTab::renderCommandsPopup()
{
using namespace material;
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
material::OverlayDialogSpec ov;
ov.title = TR("console_rpc_reference"); ov.p_open = &show_commands_popup_;
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = popW; ov.cardBottomViewportRatio = 0.94f; // keep authored width
// FIXED height (fills most of the viewport) instead of auto-height: the command list's
// fill-height child reads GetContentRegionAvail() inside the dialog child, which is only stable
// when the child is fixed-height. Auto-height made it self-referential and it slid content
// off-screen after a monitor move (same class of bug as the Wallets modal).
ov.cardHeight = ImGui::GetMainViewport()->Size.y * 0.84f / Layout::dpiScale();
if (!material::BeginOverlayDialog(ov)) {
return;
}
// Esc dismisses — this overlay is a plain Begin/BeginChild, not an ImGui popup, so it has no
// built-in Esc handling (matches the other editor overlays).
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) show_commands_popup_ = false;
float dp = Layout::dpiScale();
// Search filter — auto-focus on open so the user can type immediately (like the console input).
material::OverlayDialogSpec ov;
ov.title = TR("console_rpc_reference");
ov.p_open = &show_commands_popup_;
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = 960.0f; // wide enough for two panes
ov.cardHeight = ImGui::GetMainViewport()->Size.y * 0.74f / dp; // fixed; both panes fill it
ov.idSuffix = "cmdref";
if (!material::BeginOverlayDialog(ov)) return;
// Esc dismisses (a first Esc cancels a pending run-confirm). Not built into the overlay.
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
if (run_confirm_cmd_) run_confirm_cmd_ = nullptr;
else show_commands_popup_ = false;
}
// Search box — auto-focus on open so the user can type immediately.
ImGui::SetNextItemWidth(-1);
if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), command_search_, sizeof(command_search_));
bool searchChanged = ImGui::InputTextWithHint("##CmdSearch", TR("console_ref_search_hint"),
command_search_, sizeof(command_search_));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
std::string q(command_search_);
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
const bool searching = !q.empty();
const auto& categories = consoleCommandCategories();
std::string filter(command_search_);
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
ImGui::BeginChild("CmdListScroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - Layout::spacingXs()),
false);
ImGui::PushFont(Type().caption());
// cmd-min-width is authored in logical px; popupInnerW (GetContentRegionAvail) is physical/DPI-
// scaled, so scale the breakpoint to match — otherwise the 2-vs-3 column choice trips at the
// wrong width on HiDPI.
float cmdMinWidth = schema::UI().drawElement("tabs.console", "cmd-min-width").sizeOr(500.0f) * Layout::dpiScale();
float popupInnerW = ImGui::GetContentRegionAvail().x;
bool showParams = popupInnerW >= cmdMinWidth;
int catIdx = 0;
for (const auto& cat : categories) {
// Count matching commands in this category
int matchCount = 0;
if (filter.empty()) {
matchCount = cat.count;
} else {
for (int i = 0; i < cat.count; i++) {
if (consoleCommandMatchesFilter(cat.commands[i].name, cat.commands[i].desc,
cat.commands[i].params, filter)) {
matchCount++;
}
// Flat display order of (cat,idx): ranked when searching, category order when browsing. Drives
// keyboard nav + auto-selection; the browse view still renders grouped headers below.
std::vector<std::pair<int, int>> order;
if (searching) {
std::vector<std::pair<int, std::pair<int, int>>> scored; // (score, (cat,idx))
for (int c = 0; c < (int)categories.size(); c++)
for (int i = 0; i < categories[c].count; i++) {
int s = consoleCommandRank(categories[c].commands[i], q);
if (s >= 0) scored.push_back({s, {c, i}});
}
std::stable_sort(scored.begin(), scored.end(),
[](const auto& a, const auto& b) { return a.first > b.first; });
for (auto& s : scored) order.push_back(s.second);
} else {
for (int c = 0; c < (int)categories.size(); c++)
for (int i = 0; i < categories[c].count; i++) order.push_back({c, i});
}
// Keep a valid selection: reset to the top when the query changes or the current selection falls
// out of the visible set, so the detail pane is always populated.
int selPos = -1;
for (int k = 0; k < (int)order.size(); k++)
if (order[k].first == cmd_sel_cat_ && order[k].second == cmd_sel_idx_) { selPos = k; break; }
if (searchChanged || selPos < 0) {
if (!order.empty()) { cmd_sel_cat_ = order[0].first; cmd_sel_idx_ = order[0].second; selPos = 0; }
else { cmd_sel_cat_ = cmd_sel_idx_ = -1; }
}
// Keyboard nav: Up/Down move the selection, Enter inserts (disabled while a run-confirm shows).
if (!run_confirm_cmd_ && !order.empty() && selPos >= 0) {
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && selPos + 1 < (int)order.size()) selPos++;
else if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && selPos > 0) selPos--;
cmd_sel_cat_ = order[selPos].first;
cmd_sel_idx_ = order[selPos].second;
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter))
insertCommandToInput(categories[cmd_sel_cat_].commands[cmd_sel_idx_]);
}
// Two-pane body sized above the footer.
float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingXs();
float bodyH = std::max(120.0f, ImGui::GetContentRegionAvail().y - footerH);
float contentW = ImGui::GetContentRegionAvail().x;
float gap = Layout::spacingLg();
float masterW = std::min(std::max(contentW * 0.34f, 280.0f * dp), 380.0f * dp);
float detailW = contentW - masterW - gap;
ImFont* mono = Type().mono();
ImU32 nameCol = IM_COL32(100, 180, 255, 255);
float rowH = std::max(20.0f * dp, mono->LegacySize + 6.0f * dp);
// One master row: mono command name + a warning dot for consequential commands. Click selects
// (drives the detail pane); double-click inserts.
auto drawRow = [&](int c, int i) {
const ConsoleCommandEntry& cmd = categories[c].commands[i];
bool sel = (cmd_sel_cat_ == c && cmd_sel_idx_ == i);
ImGui::PushID(c * 1000 + i);
ImVec2 rmn = ImGui::GetCursorScreenPos();
if (ImGui::Selectable("##cmdrow", sel, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH)))
{ cmd_sel_cat_ = c; cmd_sel_idx_ = i; }
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) insertCommandToInput(cmd);
}
if (matchCount == 0) { catIdx++; continue; }
ImDrawList* dl = ImGui::GetWindowDrawList();
float tx = rmn.x + Layout::spacingSm();
if (cmd.destructive) {
float r = 3.0f * dp;
dl->AddCircleFilled(ImVec2(tx + r, rmn.y + rowH * 0.5f), r, Warning());
tx += r * 2.0f + Layout::spacingXs();
}
dl->AddText(mono, mono->LegacySize, ImVec2(tx, rmn.y + (rowH - mono->LegacySize) * 0.5f),
sel ? OnSurface() : nameCol, cmd.name);
ImGui::PopID();
};
// Default-open only the first category (Control); collapse the rest
ImGuiTreeNodeFlags headerFlags = (catIdx == 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0;
// When filtering, open all matching categories
if (!filter.empty()) headerFlags = ImGuiTreeNodeFlags_DefaultOpen;
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
// Show match count badge when filtering
const char* catLabel = consoleCategoryLabel(cat.name);
char headerLabel[128];
if (!filter.empty()) {
snprintf(headerLabel, sizeof(headerLabel), "%s (%d)", catLabel, matchCount);
// MASTER pane.
ImGui::BeginChild("##cmdMaster", ImVec2(masterW, bodyH), true);
{
if (order.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_ref_no_match"));
} else if (searching) {
for (auto& p : order) drawRow(p.first, p.second);
} else {
snprintf(headerLabel, sizeof(headerLabel), "%s", catLabel);
}
bool open = ImGui::CollapsingHeader(headerLabel, headerFlags);
ImGui::PopStyleColor();
catIdx++;
if (open) {
float nameColW = schema::UI().drawElement("tabs.console", "cmd-name-col-width").size * Layout::hScale();
float paramsColW = schema::UI().drawElement("tabs.console", "cmd-params-col-width").size * Layout::hScale();
int numCols = showParams ? 3 : 2;
char tableId[24];
snprintf(tableId, sizeof(tableId), "##cmds%d", catIdx); // unique id per category table
if (ImGui::BeginTable(tableId, numCols, ImGuiTableFlags_None)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, nameColW);
if (showParams)
ImGui::TableSetupColumn("Parameters", ImGuiTableColumnFlags_WidthFixed, paramsColW);
ImGui::TableSetupColumn("Desc", ImGuiTableColumnFlags_WidthStretch);
for (int i = 0; i < cat.count; i++) {
const auto& cmd = cat.commands[i];
if (!consoleCommandMatchesFilter(cmd.name, cmd.desc, cmd.params, filter))
continue;
ImGui::TableNextRow();
ImGui::TableNextColumn();
// Link-like blue for the command name. Readable on all skins: the RPC-reference
// modal card is a dark glass panel regardless of theme, so no light-floor is needed.
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(IM_COL32(100, 180, 255, 255)));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.4f, 0.6f, 0.3f));
char selId[128];
snprintf(selId, sizeof(selId), "%s##cmdRef", cmd.name);
if (ImGui::Selectable(selId, false)) {
if (cmd.params[0] != '\0') {
snprintf(input_buffer_, sizeof(input_buffer_), "%s %s", cmd.name, cmd.params);
} else {
strncpy(input_buffer_, cmd.name, sizeof(input_buffer_) - 1);
input_buffer_[sizeof(input_buffer_) - 1] = '\0';
}
// This overlay isn't an ImGui popup, so CloseCurrentPopup() is a no-op here —
// close it the way the framework honors, revealing the inserted command.
show_commands_popup_ = false;
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (cmd.params[0] != '\0')
material::Tooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
else
material::Tooltip(TR("console_click_insert"), cmd.name);
}
ImGui::PopStyleColor(3);
if (showParams) {
ImGui::TableNextColumn();
drawConsoleCommandParams(cmd.params);
}
ImGui::TableNextColumn();
ImGui::TextColored(
ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
"%s", cmd.desc);
}
ImGui::EndTable();
for (int c = 0; c < (int)categories.size(); c++) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
bool open = ImGui::CollapsingHeader(consoleCategoryLabel(categories[c].name),
ImGuiTreeNodeFlags_DefaultOpen);
ImGui::PopStyleColor();
if (open)
for (int i = 0; i < categories[c].count; i++) drawRow(c, i);
}
ImGui::Spacing();
}
}
ImGui::PopFont();
ImGui::EndChild();
ImGui::SameLine(0, gap);
// DETAIL pane.
ImGui::BeginChild("##cmdDetail", ImVec2(detailW, bodyH), true);
if (cmd_sel_cat_ >= 0 && cmd_sel_cat_ < (int)categories.size() &&
cmd_sel_idx_ >= 0 && cmd_sel_idx_ < categories[cmd_sel_cat_].count) {
renderCommandDetail(categories[cmd_sel_cat_].commands[cmd_sel_idx_],
consoleCategoryLabel(categories[cmd_sel_cat_].name));
} else {
ImVec2 av = ImGui::GetContentRegionAvail();
ImGui::SetCursorPosY(av.y * 0.4f);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_ref_select_hint"));
}
ImGui::EndChild();
// Footer.
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Close button (the filter is also reset when the modal next opens).
if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) {
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
material::EndOverlayDialog();
}