From 4416e01f9c23d21e2e84f01e8e4a03635585d911 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 11:32:35 -0500 Subject: [PATCH] feat(console): parameter builder in the command explorer (phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The detail pane's Parameters section is now an editable form: each parameter is a type-hinted input field (string / number / json, optional marked), and a live "Builds" preview shows the command being assembled. String values are auto-quoted; a required field left empty keeps its placeholder and disables "Insert & run"; trailing empty optionals are omitted. Insert and Insert & run now use the assembled command (not just the template), so Insert & run works for parameterized commands once the required fields are filled — destructive commands still confirm first. Param fields reset when the selected command changes or the modal opens. Keyboard nav (Up/Down/Enter) is scoped to the search box so typing in a field doesn't move the selection. A best-effort parseParamSpecs turns the human-readable param templates into fields; JSON-array params (z_sendmany, createrawtransaction) surface as a single "json" field the user pastes into. Adds the console_ref_builds label + 8 translations. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/lang/de.json | 1 + res/lang/es.json | 1 + res/lang/fr.json | 1 + res/lang/ja.json | 1 + res/lang/ko.json | 1 + res/lang/pt.json | 1 + res/lang/ru.json | 1 + res/lang/zh.json | 1 + src/ui/windows/console_tab.cpp | 177 +++++++++++++++++++++++++-------- src/ui/windows/console_tab.h | 2 + src/util/i18n.cpp | 1 + 11 files changed, 144 insertions(+), 44 deletions(-) diff --git a/res/lang/de.json b/res/lang/de.json index ccc7d88..ccb2a08 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -229,6 +229,7 @@ "console_no_daemon": "Kein Daemon", "console_not_connected": "Fehler: Nicht mit Daemon verbunden", "console_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.", + "console_ref_builds": "Ergibt", "console_ref_cancel": "Abbrechen", "console_ref_destructive": "Folgenreich", "console_ref_example": "Beispiel", diff --git a/res/lang/es.json b/res/lang/es.json index 25f856c..ca19149 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -229,6 +229,7 @@ "console_no_daemon": "Sin daemon", "console_not_connected": "Error: No conectado al daemon", "console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.", + "console_ref_builds": "Genera", "console_ref_cancel": "Cancelar", "console_ref_destructive": "Delicado", "console_ref_example": "Ejemplo", diff --git a/res/lang/fr.json b/res/lang/fr.json index 805ebf1..3e5177d 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -229,6 +229,7 @@ "console_no_daemon": "Pas de daemon", "console_not_connected": "Erreur : Non connecté au daemon", "console_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.", + "console_ref_builds": "Génère", "console_ref_cancel": "Annuler", "console_ref_destructive": "Sensible", "console_ref_example": "Exemple", diff --git a/res/lang/ja.json b/res/lang/ja.json index 50364dd..409a44f 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -229,6 +229,7 @@ "console_no_daemon": "デーモンなし", "console_not_connected": "エラー:デーモンに接続されていません", "console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。", + "console_ref_builds": "生成", "console_ref_cancel": "キャンセル", "console_ref_destructive": "要注意", "console_ref_example": "例", diff --git a/res/lang/ko.json b/res/lang/ko.json index 40fb139..992e01b 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -229,6 +229,7 @@ "console_no_daemon": "데몬 없음", "console_not_connected": "오류: 데몬에 연결되지 않았습니다", "console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.", + "console_ref_builds": "생성", "console_ref_cancel": "취소", "console_ref_destructive": "주의", "console_ref_example": "예시", diff --git a/res/lang/pt.json b/res/lang/pt.json index cbd2aad..9c98fd6 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -229,6 +229,7 @@ "console_no_daemon": "Sem daemon", "console_not_connected": "Erro: Não conectado ao daemon", "console_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.", + "console_ref_builds": "Gera", "console_ref_cancel": "Cancelar", "console_ref_destructive": "Sensível", "console_ref_example": "Exemplo", diff --git a/res/lang/ru.json b/res/lang/ru.json index 66f8940..f97746d 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -229,6 +229,7 @@ "console_no_daemon": "Нет daemon", "console_not_connected": "Ошибка: Не подключено к daemon", "console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.", + "console_ref_builds": "Формирует", "console_ref_cancel": "Отмена", "console_ref_destructive": "Осторожно", "console_ref_example": "Пример", diff --git a/res/lang/zh.json b/res/lang/zh.json index cdecf49..52c01be 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -229,6 +229,7 @@ "console_no_daemon": "无守护进程", "console_not_connected": "错误:未连接到守护进程", "console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。", + "console_ref_builds": "生成", "console_ref_cancel": "取消", "console_ref_destructive": "谨慎", "console_ref_example": "示例", diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 301715f..27f34bd 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -1495,6 +1495,49 @@ std::vector splitParamTemplate(const char* params) return out; } +// A parsed parameter for the builder form. type in {string, number, json}; `raw` is the original +// template token (used as a placeholder when a required field is left empty). +struct ConsoleParamSpec { + std::string label; + std::string type; + bool optional = false; + std::string raw; +}; + +// Best-effort parse of a param template into fillable fields. The templates are human-readable, not +// a strict schema, so heuristics: [word] / ["word"] = an optional scalar; a [ / { with JSON content +// (a brace or comma) = a JSON field the user pastes; otherwise a required string/number. +std::vector parseParamSpecs(const char* params) +{ + std::vector out; + for (const std::string& t : splitParamTemplate(params)) { + ConsoleParamSpec p; + p.raw = t; + std::string inner = t; + if (inner.size() >= 2 && inner.front() == '[' && inner.back() == ']') { + std::string body = inner.substr(1, inner.size() - 2); + bool looksJson = body.find('{') != std::string::npos || + body.find(',') != std::string::npos || + (!body.empty() && body.front() == '['); + if (!looksJson) { p.optional = true; inner = body; } // optional scalar wrapper + } + char c0 = inner.empty() ? 0 : inner.front(); + if (c0 == '"' || c0 == '\'') { + p.type = "string"; + if (inner.size() >= 2 && inner.back() == c0) inner = inner.substr(1, inner.size() - 2); + p.label = inner; + } else if (c0 == '{' || c0 == '[') { + p.type = "json"; + p.label = "json"; + } else { + p.type = "number"; + p.label = inner; + } + out.push_back(p); + } + return out; +} + // Translate a command-category name from the static reference tables (English) for display. The // per-command descriptions stay in English (technical RPC docs); only the 7 category labels are i18n'd. const char* consoleCategoryLabel(const char* name) @@ -1554,35 +1597,72 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* ImGui::PopTextWrapPos(); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); - // Parameters — each token with a rough type tag; optional ([...]) tokens are dimmed. + // Parameters — an editable form that assembles the command. Reset the fields whenever the + // selected command changes so values don't carry across commands. + if (cmd_param_owner_ != &cmd) { + for (auto& b : cmd_param_bufs_) b[0] = '\0'; + cmd_param_owner_ = &cmd; + } + std::vector specs = parseParamSpecs(cmd.params); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_parameters")); - std::vector toks = splitParamTemplate(cmd.params); - if (toks.empty()) { + if (specs.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"; + float labelW = 110.0f * dp; + ImGui::Indent(Layout::spacingSm()); + for (size_t k = 0; k < specs.size() && k < 6; k++) { + const ConsoleParamSpec& s = specs[k]; + ImGui::PushID((int)k); ImGui::PushFont(mono); - ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(optional ? OnSurfaceMedium() : OnSurface()), - "%s", t.c_str()); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(s.optional ? OnSurfaceMedium() : OnSurface()), + "%s", s.label.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::SameLine(labelW); + ImGui::SetNextItemWidth(-1); + std::string hint = s.optional + ? (s.type + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : s.type; + ImGui::InputTextWithHint("##pv", hint.c_str(), cmd_param_bufs_[k], sizeof(cmd_param_bufs_[k])); + ImGui::PopID(); } - ImGui::Unindent(Layout::spacingMd()); + ImGui::Unindent(Layout::spacingSm()); + } + + // Assemble the command from the filled fields. A required field left empty keeps its placeholder + // token (so Insert still shows what's needed) and marks the command incomplete (Insert & run + // disabled). String values are auto-quoted; trailing empty optionals are omitted. + auto trimStr = [](const std::string& s) -> std::string { + size_t a = s.find_first_not_of(" \t"); + if (a == std::string::npos) return std::string(); + return s.substr(a, s.find_last_not_of(" \t") - a + 1); + }; + bool complete = specs.size() <= 6; + std::string built = cmd.name; + for (size_t k = 0, stop = 0; k < specs.size() && k < 6 && !stop; k++) { + std::string val = trimStr(cmd_param_bufs_[k]); + if (val.empty()) { + if (specs[k].optional) stop = 1; // omit this + trailing optionals + else { built += " " + specs[k].raw; complete = false; } // required placeholder + } else { + if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' && + val.front() != '[' && val.front() != '{') + val = "\"" + val + "\""; + built += " " + val; + } + } + + // Live "Builds" preview when the command takes parameters. + if (!specs.empty()) { + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_builds")); + ImGui::PushFont(mono); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(IM_COL32(150, 200, 150, 255)), "%s", built.c_str()); + ImGui::PopTextWrapPos(); + ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); - // Example (curated commands only). + // Example (curated commands only) — reference alongside the builder. if (cmd.example[0]) { ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_example")); ImGui::PushFont(mono); @@ -1593,7 +1673,7 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* ImGui::Dummy(ImVec2(0, Layout::spacingMd())); } - // Actions (or the destructive run confirmation). + // Actions (or the destructive run confirmation) — Insert/run use the assembled command. ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (run_confirm_cmd_ == &cmd) { ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning()); @@ -1607,28 +1687,32 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* 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; + pending_submit_ = built; 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; - } + if (material::TactileButton(TR("console_ref_insert"), ImVec2(200.0f * dp, 0))) { + snprintf(input_buffer_, sizeof(input_buffer_), "%s", built.c_str()); + command_search_[0] = '\0'; + run_confirm_cmd_ = nullptr; + show_commands_popup_ = false; + } + // Insert & run: enabled once every required field is filled (a template with unfilled + // placeholders would just error). + ImGui::SameLine(); + ImGui::BeginDisabled(!complete); + if (material::TactileButton(TR("console_ref_insert_run"), ImVec2(140.0f * dp, 0))) { + if (cmd.destructive) { + run_confirm_cmd_ = &cmd; + } else { + pending_submit_ = built; + command_search_[0] = '\0'; + show_commands_popup_ = false; } } + ImGui::EndDisabled(); } } @@ -1652,11 +1736,15 @@ void ConsoleTab::renderCommandsPopup() else show_commands_popup_ = false; } - // Search box — auto-focus on open so the user can type immediately. + // Search box — auto-focus on open so the user can type immediately. Enter inserts the selected + // command's template; reset the param-builder fields on open. + if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); cmd_param_owner_ = nullptr; } ImGui::SetNextItemWidth(-1); - if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere(); - bool searchChanged = ImGui::InputTextWithHint("##CmdSearch", TR("console_ref_search_hint"), - command_search_, sizeof(command_search_)); + bool searchEnter = ImGui::InputTextWithHint("##CmdSearch", TR("console_ref_search_hint"), + command_search_, sizeof(command_search_), + ImGuiInputTextFlags_EnterReturnsTrue); + bool searchChanged = ImGui::IsItemEdited(); + bool searchActive = ImGui::IsItemActive(); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); std::string q(command_search_); @@ -1693,15 +1781,16 @@ void ConsoleTab::renderCommandsPopup() 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) { + // Keyboard nav from the SEARCH box only (so typing in a param field doesn't move the selection): + // Up/Down move, Enter inserts the selected command's template. Disabled while a run-confirm shows. + if (!run_confirm_cmd_ && searchActive && !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_]); } + if (searchEnter && !run_confirm_cmd_ && cmd_sel_cat_ >= 0) + insertCommandToInput(categories[cmd_sel_cat_].commands[cmd_sel_idx_]); // Two-pane body sized above the footer. float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingXs(); diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index 5c2f56a..4cf40f3 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -193,6 +193,8 @@ private: int cmd_sel_idx_ = -1; // detail-pane selection: command index within that category std::string pending_submit_; // command to run next frame (deferred so the modal needs no executor) const ConsoleCommandEntry* run_confirm_cmd_ = nullptr; // destructive "Insert & run" awaiting confirmation + char cmd_param_bufs_[6][256] = {{0}}; // parameter-builder input fields + const ConsoleCommandEntry* cmd_param_owner_ = nullptr; // command the param buffers currently hold }; } // namespace ui diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index ebb29fb..6fae6d6 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1306,6 +1306,7 @@ void I18n::loadBuiltinEnglish() strings_["console_ref_no_params"] = "Takes no parameters."; strings_["console_ref_optional"] = "optional"; strings_["console_ref_example"] = "Example"; + strings_["console_ref_builds"] = "Builds"; strings_["console_ref_destructive"] = "Consequential"; strings_["console_ref_run_confirm"] = "Run %s now? This is a consequential command."; strings_["console_ref_cancel"] = "Cancel";