feat(console): parameter builder in the command explorer (phase 3)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1495,6 +1495,49 @@ std::vector<std::string> 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<ConsoleParamSpec> parseParamSpecs(const char* params)
|
||||
{
|
||||
std::vector<ConsoleParamSpec> 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<ConsoleParamSpec> specs = parseParamSpecs(cmd.params);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_parameters"));
|
||||
std::vector<std::string> 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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user