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
parent ab4dd370c0
commit c9e9c9f979
14 changed files with 536 additions and 214 deletions

Binary file not shown.

View File

@@ -229,6 +229,19 @@
"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_cancel": "Abbrechen",
"console_ref_destructive": "Folgenreich",
"console_ref_example": "Beispiel",
"console_ref_insert": "In Konsole einfügen",
"console_ref_insert_run": "Einfügen & ausführen",
"console_ref_no_match": "Keine Befehle gefunden.",
"console_ref_no_params": "Erwartet keine Parameter.",
"console_ref_optional": "optional",
"console_ref_parameters": "Parameter",
"console_ref_run": "Ausführen",
"console_ref_run_confirm": "%s jetzt ausführen? Dies ist ein folgenreicher Befehl.",
"console_ref_search_hint": "Nach Name oder Aufgabe suchen…",
"console_ref_select_hint": "Wählen Sie einen Befehl, um zu sehen, was er tut.",
"console_rpc_reference": "RPC-Befehlsreferenz",
"console_rpc_trace": "RPC",
"console_scanline": "Konsolen-Scanline",

View File

@@ -229,6 +229,19 @@
"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_cancel": "Cancelar",
"console_ref_destructive": "Delicado",
"console_ref_example": "Ejemplo",
"console_ref_insert": "Insertar en la consola",
"console_ref_insert_run": "Insertar y ejecutar",
"console_ref_no_match": "Ningún comando coincide.",
"console_ref_no_params": "No requiere parámetros.",
"console_ref_optional": "opcional",
"console_ref_parameters": "Parámetros",
"console_ref_run": "Ejecutar",
"console_ref_run_confirm": "¿Ejecutar %s ahora? Es un comando con consecuencias.",
"console_ref_search_hint": "Buscar por nombre o tarea…",
"console_ref_select_hint": "Selecciona un comando para ver qué hace.",
"console_rpc_reference": "Referencia de Comandos RPC",
"console_rpc_trace": "RPC",
"console_scanline": "Líneas de consola",

View File

@@ -229,6 +229,19 @@
"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_cancel": "Annuler",
"console_ref_destructive": "Sensible",
"console_ref_example": "Exemple",
"console_ref_insert": "Insérer dans la console",
"console_ref_insert_run": "Insérer et exécuter",
"console_ref_no_match": "Aucune commande ne correspond.",
"console_ref_no_params": "Ne prend aucun paramètre.",
"console_ref_optional": "optionnel",
"console_ref_parameters": "Paramètres",
"console_ref_run": "Exécuter",
"console_ref_run_confirm": "Exécuter %s maintenant ? C'est une commande à conséquences.",
"console_ref_search_hint": "Rechercher par nom ou tâche…",
"console_ref_select_hint": "Sélectionnez une commande pour voir ce qu'elle fait.",
"console_rpc_reference": "Référence des commandes RPC",
"console_rpc_trace": "RPC",
"console_scanline": "Scanline de la console",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "デーモンなし",
"console_not_connected": "エラー:デーモンに接続されていません",
"console_quit_note": "ここでは 'quit''exit' は不要です — ウィンドウを閉じるだけで構いません。",
"console_ref_cancel": "キャンセル",
"console_ref_destructive": "要注意",
"console_ref_example": "例",
"console_ref_insert": "コンソールに挿入",
"console_ref_insert_run": "挿入して実行",
"console_ref_no_match": "一致するコマンドはありません。",
"console_ref_no_params": "パラメーターは不要です。",
"console_ref_optional": "任意",
"console_ref_parameters": "パラメーター",
"console_ref_run": "実行",
"console_ref_run_confirm": "%s を今すぐ実行しますか? 影響の大きいコマンドです。",
"console_ref_search_hint": "名前または用途で検索…",
"console_ref_select_hint": "コマンドを選ぶと内容が表示されます。",
"console_rpc_reference": "RPCコマンドリファレンス",
"console_rpc_trace": "RPC",
"console_scanline": "コンソールスキャンライン",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "데몬 없음",
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
"console_ref_cancel": "취소",
"console_ref_destructive": "주의",
"console_ref_example": "예시",
"console_ref_insert": "콘솔에 삽입",
"console_ref_insert_run": "삽입 후 실행",
"console_ref_no_match": "일치하는 명령이 없습니다.",
"console_ref_no_params": "매개변수가 없습니다.",
"console_ref_optional": "선택",
"console_ref_parameters": "매개변수",
"console_ref_run": "실행",
"console_ref_run_confirm": "%s 을(를) 지금 실행할까요? 영향이 큰 명령입니다.",
"console_ref_search_hint": "이름 또는 용도로 검색…",
"console_ref_select_hint": "명령을 선택하면 설명이 표시됩니다.",
"console_rpc_reference": "RPC 명령어 참조",
"console_rpc_trace": "RPC",
"console_scanline": "콘솔 스캔라인",

View File

@@ -229,6 +229,19 @@
"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_cancel": "Cancelar",
"console_ref_destructive": "Sensível",
"console_ref_example": "Exemplo",
"console_ref_insert": "Inserir no console",
"console_ref_insert_run": "Inserir e executar",
"console_ref_no_match": "Nenhum comando corresponde.",
"console_ref_no_params": "Não requer parâmetros.",
"console_ref_optional": "opcional",
"console_ref_parameters": "Parâmetros",
"console_ref_run": "Executar",
"console_ref_run_confirm": "Executar %s agora? Este é um comando com consequências.",
"console_ref_search_hint": "Pesquisar por nome ou tarefa…",
"console_ref_select_hint": "Selecione um comando para ver o que ele faz.",
"console_rpc_reference": "Referência de Comandos RPC",
"console_rpc_trace": "RPC",
"console_scanline": "Scanline do console",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "Нет daemon",
"console_not_connected": "Ошибка: Не подключено к daemon",
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
"console_ref_cancel": "Отмена",
"console_ref_destructive": "Осторожно",
"console_ref_example": "Пример",
"console_ref_insert": "Вставить в консоль",
"console_ref_insert_run": "Вставить и выполнить",
"console_ref_no_match": "Нет подходящих команд.",
"console_ref_no_params": "Не требует параметров.",
"console_ref_optional": "необязательно",
"console_ref_parameters": "Параметры",
"console_ref_run": "Выполнить",
"console_ref_run_confirm": "Выполнить %s сейчас? Это ответственная команда.",
"console_ref_search_hint": "Поиск по названию или задаче…",
"console_ref_select_hint": "Выберите команду, чтобы увидеть, что она делает.",
"console_rpc_reference": "Справочник RPC-команд",
"console_rpc_trace": "RPC",
"console_scanline": "Скан-линия консоли",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "无守护进程",
"console_not_connected": "错误:未连接到守护进程",
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
"console_ref_cancel": "取消",
"console_ref_destructive": "谨慎",
"console_ref_example": "示例",
"console_ref_insert": "插入到控制台",
"console_ref_insert_run": "插入并运行",
"console_ref_no_match": "没有匹配的命令。",
"console_ref_no_params": "无需参数。",
"console_ref_optional": "可选",
"console_ref_parameters": "参数",
"console_ref_run": "运行",
"console_ref_run_confirm": "立即运行 %s这是一个有重大影响的命令。",
"console_ref_search_hint": "按名称或用途搜索…",
"console_ref_select_hint": "选择一个命令以查看其功能。",
"console_rpc_reference": "RPC 命令参考",
"console_rpc_trace": "RPC",
"console_scanline": "控制台扫描线",

View File

@@ -14,28 +14,50 @@ constexpr int CountOf(const ConsoleCommandEntry (&)[N])
}
const ConsoleCommandEntry kControlCommands[] = {
{"help", "List all commands, or get help for a specified command", "[\"command\"]"},
{"getinfo", "Get general info about the node", ""},
{"stop", "Stop the daemon", ""},
{"help", "List all commands, or get help for a specified command", "[\"command\"]",
"Lists every RPC command. Pass a command name to see the node's detailed help for just that one.",
"help \"getinfo\"", "commands list documentation what available"},
{"getinfo", "Get general info about the node", "",
"A quick snapshot of the node: version, block height, connection count, difficulty and wallet balance.",
"getinfo", "status version summary overview node health"},
{"stop", "Stop the daemon", "",
"Shuts the DragonX node down cleanly. The wallet stays disconnected until the node is started again.",
"stop", "quit shutdown exit close halt turn off", true},
};
const ConsoleCommandEntry kNetworkCommands[] = {
{"getnetworkinfo", "Return P2P network state info", ""},
{"getpeerinfo", "Get data about each connected peer", ""},
{"getconnectioncount", "Get number of peer connections", ""},
{"getnetworkinfo", "Return P2P network state info", "",
"The node's networking state: protocol version, which networks are active, listening addresses and relay fee.",
"getnetworkinfo", "network p2p connection status internet"},
{"getpeerinfo", "Get data about each connected peer", "",
"Detailed data for every peer you are connected to \xE2\x80\x94 address, version, ping time and bytes exchanged.",
"getpeerinfo", "peers connections nodes who connected"},
{"getconnectioncount", "Get number of peer connections", "",
"How many peers the node is currently connected to. Zero usually means the node is still starting or offline.",
"getconnectioncount", "peers connections count how many"},
{"getnettotals", "Get network traffic statistics", ""},
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\""},
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]"},
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]",
"Bans or unbans an IP or subnet from connecting. bantime is in seconds (0 = the default 24h); absolute treats it as a Unix timestamp.",
"setban \"192.168.0.6\" \"add\" 86400", "ban block ip peer firewall reject", true},
{"listbanned", "List all banned IPs/subnets", ""},
{"clearbanned", "Clear all banned IPs", ""},
{"clearbanned", "Clear all banned IPs", "",
"Removes every entry from the ban list, letting all previously-banned peers connect again.",
"clearbanned", "unban reset bans allow clear", true},
{"ping", "Ping all peers to measure round-trip time", ""},
};
const ConsoleCommandEntry kBlockchainCommands[] = {
{"getblockchaininfo", "Get current blockchain state", ""},
{"getblockcount", "Get number of blocks in longest chain", ""},
{"getblockchaininfo", "Get current blockchain state", "",
"State of the chain: height, best block hash, difficulty, verification progress and how far the node has synced.",
"getblockchaininfo", "chain sync height status blockchain progress"},
{"getblockcount", "Get number of blocks in longest chain", "",
"The height of the longest chain \xE2\x80\x94 how many blocks the node has. Compare with the network to check sync.",
"getblockcount", "height blocks how many chain length synced"},
{"getbestblockhash", "Get hash of the tip block", ""},
{"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]"},
{"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]",
"Returns a block by its hash or height. verbosity 0 = raw hex, 1 = decoded header + txids, 2 = full transactions.",
"getblock \"0000000000abc123\" 1", "block details transactions header"},
{"getblockhash", "Get block hash at a given height", "height"},
{"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]"},
{"getdifficulty", "Get proof-of-work difficulty", ""},
@@ -51,8 +73,12 @@ const ConsoleCommandEntry kBlockchainCommands[] = {
};
const ConsoleCommandEntry kMiningCommands[] = {
{"getmininginfo", "Get mining-related information", ""},
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]"},
{"getmininginfo", "Get mining-related information", "",
"Mining status: current difficulty, estimated network hash rate and whether this node is generating blocks.",
"getmininginfo", "mining hashrate difficulty generate"},
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]",
"Turns built-in CPU mining on or off. Pass true or false, and optionally the number of threads (-1 = all cores).",
"setgenerate true 4", "mine mining generate cpu start stop", true},
{"getgenerate", "Check if the node is mining", ""},
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]"},
{"getblocksubsidy", "Get block reward at a given height", "[height]"},
@@ -61,39 +87,75 @@ const ConsoleCommandEntry kMiningCommands[] = {
};
const ConsoleCommandEntry kWalletCommands[] = {
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]"},
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]"},
{"z_getbalances", "Get all balances (transparent + shielded)", ""},
{"getnewaddress", "Generate a new transparent address", ""},
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]"},
{"listaddresses", "List all transparent addresses", ""},
{"z_listaddresses", "List all z-addresses", ""},
{"sendtoaddress", "Send to a specific address", "\"address\" amount"},
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]"},
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]",
"Your confirmed transparent (t-address) balance in DRGX. Shielded funds are NOT included \xE2\x80\x94 use z_gettotalbalance for everything.",
"getbalance", "balance money funds how much transparent"},
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]",
"Your complete balance in DRGX \xE2\x80\x94 transparent plus shielded (private) funds together.",
"z_gettotalbalance", "balance total money funds shielded private how much"},
{"z_getbalances", "Get all balances (transparent + shielded)", "",
"Breaks your balance down across each transparent and shielded address the wallet holds.",
"z_getbalances", "balance breakdown addresses funds per address"},
{"getnewaddress", "Generate a new transparent address", "",
"Creates a fresh transparent (t-) address you can share to receive DRGX.",
"getnewaddress", "receive new address deposit transparent create get paid"},
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]",
"Creates a fresh shielded (z-) address for receiving DRGX privately.",
"z_getnewaddress", "receive new address private shielded create"},
{"listaddresses", "List all transparent addresses", "",
"Lists the transparent (t-) addresses in your wallet.",
"listaddresses", "addresses list my accounts transparent"},
{"z_listaddresses", "List all z-addresses", "",
"Lists the shielded (z-) addresses in your wallet.",
"z_listaddresses", "addresses list my shielded private"},
{"sendtoaddress", "Send to a specific address", "\"address\" amount",
"Sends DRGX to a transparent address. amount is in DRGX. Double-check the address \xE2\x80\x94 sent transactions cannot be reversed.",
"sendtoaddress \"RyourRecipientAddr\" 1.5", "send pay transfer money spend", true},
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]",
"Sends DRGX from one address to one or more recipients, with shielded (private) support. Amounts are in DRGX.",
"z_sendmany \"RfromAddr\" [{\"address\":\"zs1toAddr\",\"amount\":1.0}]", "send pay private shielded transfer money", true},
{"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]"},
{"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]"},
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]"},
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]",
"Your most recent wallet transactions, newest first \xE2\x80\x94 amounts, addresses and confirmations.",
"listtransactions", "transactions history recent payments received sent"},
{"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]"},
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]"},
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]"},
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]"},
{"z_listoperationids", "List all async z operation IDs", ""},
{"getwalletinfo", "Get wallet state info", ""},
{"backupwallet", "Back up wallet to a file", "\"destination\""},
{"dumpprivkey", "Dump private key for an address", "\"address\""},
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]"},
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\""},
{"importwallet", "Import wallet from a dump file", "\"filename\""},
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\""},
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]"},
{"getwalletinfo", "Get wallet state info", "",
"Wallet summary: balances, transaction count, key-pool size and whether the wallet is encrypted and locked.",
"getwalletinfo", "wallet status info balance encrypted locked"},
{"backupwallet", "Back up wallet to a file", "\"destination\"",
"Saves a copy of your wallet.dat to the given path. Keep the backup somewhere safe and private.",
"backupwallet \"/home/you/drgx-backup.dat\"", "backup save wallet copy protect"},
{"dumpprivkey", "Dump private key for an address", "\"address\"",
"Reveals the private key for an address. Anyone with this key can spend from the address \xE2\x80\x94 never share it.",
"dumpprivkey \"RyourAddr\"", "private key export reveal secret spend", true},
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]",
"Imports a private key so the wallet can spend from its address, then rescans the chain for its history (can be slow).",
"importprivkey \"Uxxxxxxxxxxxx\"", "import private key restore recover add", true},
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\"", "", "", "", true},
{"importwallet", "Import wallet from a dump file", "\"filename\"", "", "", "", true},
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\"",
"Reveals the spending key for a shielded address. Anyone with it can spend your private funds \xE2\x80\x94 keep it secret.",
"z_exportkey \"zs1yourAddr\"", "private key export shielded secret spend", true},
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]", "", "", "", true},
{"z_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\""},
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]"},
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\""},
{"signmessage", "Sign a message with an address key", "\"address\" \"message\""},
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]", "", "", "", true},
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\"", "", "", "", true},
{"signmessage", "Sign a message with an address key", "\"address\" \"message\"",
"Signs a message with the private key of one of your addresses, proving you control that address.",
"signmessage \"RyourAddr\" \"hello world\"", "sign message prove ownership"},
{"settxfee", "Set the transaction fee per kB", "amount"},
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout"},
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout",
"Unlocks an encrypted wallet for the given number of seconds so you can send funds. Your passphrase is typed in plain text \xE2\x80\x94 be sure no one is watching.",
"walletpassphrase \"your passphrase\" 60", "unlock passphrase password encrypted open", true},
{"walletlock", "Lock the wallet", ""},
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\""},
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\"",
"Encrypts the wallet with a passphrase. The node shuts down afterward and you will need the passphrase to send. Encryption cannot be undone \xE2\x80\x94 keep the passphrase safe.",
"encryptwallet \"a strong passphrase\"", "encrypt password protect secure lock", true},
};
const ConsoleCommandEntry kRawTransactionCommands[] = {
@@ -107,13 +169,15 @@ const ConsoleCommandEntry kRawTransactionCommands[] = {
};
const ConsoleCommandEntry kUtilityCommands[] = {
{"validateaddress", "Validate a transparent address", "\"address\""},
{"validateaddress", "Validate a transparent address", "\"address\"",
"Checks whether a transparent address is well-formed and whether it belongs to your wallet.",
"validateaddress \"RsomeAddr\"", "validate check address valid mine"},
{"z_validateaddress", "Validate a z-address", "\"zaddr\""},
{"estimatefee", "Estimate fee for a transaction", "nblocks"},
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\""},
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]"},
{"invalidateblock", "Mark a block as invalid", "\"hash\""},
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\""},
{"invalidateblock", "Mark a block as invalid", "\"hash\"", "", "", "", true},
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\"", "", "", "", true},
};
} // namespace

View File

@@ -7,8 +7,14 @@ namespace ui {
struct ConsoleCommandEntry {
const char* name;
const char* desc;
const char* params;
const char* desc; // terse one-line summary (always set)
const char* params; // parameter template, e.g. "\"address\" amount [comment]"
// Optional novice-facing enrichment (C++17 aggregate defaults — entries that omit these keep
// the empty/false fallbacks, and the command explorer falls back to `desc`):
const char* details = ""; // longer plain-language explanation
const char* example = ""; // one concrete, runnable example line
const char* keywords = ""; // space-separated search synonyms ("balance money funds")
bool destructive = false; // consequential/sensitive -> safety badge + run confirmation
};
struct ConsoleCommandCategory {

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();
}

View File

@@ -25,6 +25,7 @@ namespace ui {
class ConsoleCommandExecutor;
struct ConsoleLogFilterCaps;
struct ConsoleCommandEntry;
/**
* @brief Console tab — a rich terminal shared by the full-node and lite variants.
@@ -115,6 +116,8 @@ private:
void addFormattedResult(const std::string& result, bool is_error);
void renderStatusHeader(ConsoleCommandExecutor& exec);
void renderCommandsPopup();
void renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel); // right pane
void insertCommandToInput(const ConsoleCommandEntry& cmd); // fill input + close the modal
// renderToolbar() draws the top bar; these are its sub-steps:
void renderToolbar(ConsoleCommandExecutor& exec);
@@ -183,9 +186,13 @@ private:
// consumed by the renderer + hit-testing.
mutable ConsoleLayout layout_;
// Commands popup
// Commands popup (RPC command explorer)
bool show_commands_popup_ = false;
char command_search_[128] = {0}; // RPC-reference search filter (cleared when the modal opens)
int cmd_sel_cat_ = -1; // detail-pane selection: category index into consoleCommandCategories()
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
};
} // namespace ui

View File

@@ -1301,6 +1301,19 @@ void I18n::loadBuiltinEnglish()
strings_["console_cat_wallet"] = "Wallet";
strings_["console_cat_raw_transactions"] = "Raw Transactions";
strings_["console_cat_utility"] = "Utility";
strings_["console_ref_search_hint"] = "Search by name or task\xE2\x80\xA6";
strings_["console_ref_parameters"] = "Parameters";
strings_["console_ref_no_params"] = "Takes no parameters.";
strings_["console_ref_optional"] = "optional";
strings_["console_ref_example"] = "Example";
strings_["console_ref_destructive"] = "Consequential";
strings_["console_ref_run_confirm"] = "Run %s now? This is a consequential command.";
strings_["console_ref_cancel"] = "Cancel";
strings_["console_ref_run"] = "Run";
strings_["console_ref_insert"] = "Insert into console";
strings_["console_ref_insert_run"] = "Insert & run";
strings_["console_ref_select_hint"] = "Select a command to see what it does.";
strings_["console_ref_no_match"] = "No commands match.";
// --- Export All Keys Dialog ---
strings_["export_keys_btn"] = "Export Keys";