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_no_daemon": "Kein Daemon",
"console_not_connected": "Fehler: Nicht mit Daemon verbunden", "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_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_reference": "RPC-Befehlsreferenz",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Konsolen-Scanline", "console_scanline": "Konsolen-Scanline",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "Sin daemon", "console_no_daemon": "Sin daemon",
"console_not_connected": "Error: No conectado al daemon", "console_not_connected": "Error: No conectado al daemon",
"console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.", "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_reference": "Referencia de Comandos RPC",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Líneas de consola", "console_scanline": "Líneas de consola",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "Pas de daemon", "console_no_daemon": "Pas de daemon",
"console_not_connected": "Erreur : Non connecté au 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_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_reference": "Référence des commandes RPC",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Scanline de la console", "console_scanline": "Scanline de la console",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "デーモンなし", "console_no_daemon": "デーモンなし",
"console_not_connected": "エラー:デーモンに接続されていません", "console_not_connected": "エラー:デーモンに接続されていません",
"console_quit_note": "ここでは 'quit''exit' は不要です — ウィンドウを閉じるだけで構いません。", "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_reference": "RPCコマンドリファレンス",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "コンソールスキャンライン", "console_scanline": "コンソールスキャンライン",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "데몬 없음", "console_no_daemon": "데몬 없음",
"console_not_connected": "오류: 데몬에 연결되지 않았습니다", "console_not_connected": "오류: 데몬에 연결되지 않았습니다",
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.", "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_reference": "RPC 명령어 참조",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "콘솔 스캔라인", "console_scanline": "콘솔 스캔라인",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "Sem daemon", "console_no_daemon": "Sem daemon",
"console_not_connected": "Erro: Não conectado ao 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_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_reference": "Referência de Comandos RPC",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Scanline do console", "console_scanline": "Scanline do console",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "Нет daemon", "console_no_daemon": "Нет daemon",
"console_not_connected": "Ошибка: Не подключено к daemon", "console_not_connected": "Ошибка: Не подключено к daemon",
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.", "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_reference": "Справочник RPC-команд",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Скан-линия консоли", "console_scanline": "Скан-линия консоли",

View File

@@ -229,6 +229,19 @@
"console_no_daemon": "无守护进程", "console_no_daemon": "无守护进程",
"console_not_connected": "错误:未连接到守护进程", "console_not_connected": "错误:未连接到守护进程",
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。", "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_reference": "RPC 命令参考",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "控制台扫描线", "console_scanline": "控制台扫描线",

View File

@@ -14,28 +14,50 @@ constexpr int CountOf(const ConsoleCommandEntry (&)[N])
} }
const ConsoleCommandEntry kControlCommands[] = { const ConsoleCommandEntry kControlCommands[] = {
{"help", "List all commands, or get help for a specified command", "[\"command\"]"}, {"help", "List all commands, or get help for a specified command", "[\"command\"]",
{"getinfo", "Get general info about the node", ""}, "Lists every RPC command. Pass a command name to see the node's detailed help for just that one.",
{"stop", "Stop the daemon", ""}, "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[] = { const ConsoleCommandEntry kNetworkCommands[] = {
{"getnetworkinfo", "Return P2P network state info", ""}, {"getnetworkinfo", "Return P2P network state info", "",
{"getpeerinfo", "Get data about each connected peer", ""}, "The node's networking state: protocol version, which networks are active, listening addresses and relay fee.",
{"getconnectioncount", "Get number of peer connections", ""}, "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", ""}, {"getnettotals", "Get network traffic statistics", ""},
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\""}, {"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", ""}, {"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", ""}, {"ping", "Ping all peers to measure round-trip time", ""},
}; };
const ConsoleCommandEntry kBlockchainCommands[] = { const ConsoleCommandEntry kBlockchainCommands[] = {
{"getblockchaininfo", "Get current blockchain state", ""}, {"getblockchaininfo", "Get current blockchain state", "",
{"getblockcount", "Get number of blocks in longest chain", ""}, "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", ""}, {"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"}, {"getblockhash", "Get block hash at a given height", "height"},
{"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]"}, {"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]"},
{"getdifficulty", "Get proof-of-work difficulty", ""}, {"getdifficulty", "Get proof-of-work difficulty", ""},
@@ -51,8 +73,12 @@ const ConsoleCommandEntry kBlockchainCommands[] = {
}; };
const ConsoleCommandEntry kMiningCommands[] = { const ConsoleCommandEntry kMiningCommands[] = {
{"getmininginfo", "Get mining-related information", ""}, {"getmininginfo", "Get mining-related information", "",
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]"}, "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", ""}, {"getgenerate", "Check if the node is mining", ""},
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]"}, {"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]"},
{"getblocksubsidy", "Get block reward at a given height", "[height]"}, {"getblocksubsidy", "Get block reward at a given height", "[height]"},
@@ -61,39 +87,75 @@ const ConsoleCommandEntry kMiningCommands[] = {
}; };
const ConsoleCommandEntry kWalletCommands[] = { const ConsoleCommandEntry kWalletCommands[] = {
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]"}, {"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]",
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]"}, "Your confirmed transparent (t-address) balance in DRGX. Shielded funds are NOT included \xE2\x80\x94 use z_gettotalbalance for everything.",
{"z_getbalances", "Get all balances (transparent + shielded)", ""}, "getbalance", "balance money funds how much transparent"},
{"getnewaddress", "Generate a new transparent address", ""}, {"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]",
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]"}, "Your complete balance in DRGX \xE2\x80\x94 transparent plus shielded (private) funds together.",
{"listaddresses", "List all transparent addresses", ""}, "z_gettotalbalance", "balance total money funds shielded private how much"},
{"z_listaddresses", "List all z-addresses", ""}, {"z_getbalances", "Get all balances (transparent + shielded)", "",
{"sendtoaddress", "Send to a specific address", "\"address\" amount"}, "Breaks your balance down across each transparent and shielded address the wallet holds.",
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]"}, "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_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]"}, {"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]"}, {"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]"},
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]"}, {"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]"},
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]"}, {"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]"},
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]"}, {"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]"},
{"z_listoperationids", "List all async z operation IDs", ""}, {"z_listoperationids", "List all async z operation IDs", ""},
{"getwalletinfo", "Get wallet state info", ""}, {"getwalletinfo", "Get wallet state info", "",
{"backupwallet", "Back up wallet to a file", "\"destination\""}, "Wallet summary: balances, transaction count, key-pool size and whether the wallet is encrypted and locked.",
{"dumpprivkey", "Dump private key for an address", "\"address\""}, "getwalletinfo", "wallet status info balance encrypted locked"},
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]"}, {"backupwallet", "Back up wallet to a file", "\"destination\"",
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\""}, "Saves a copy of your wallet.dat to the given path. Keep the backup somewhere safe and private.",
{"importwallet", "Import wallet from a dump file", "\"filename\""}, "backupwallet \"/home/you/drgx-backup.dat\"", "backup save wallet copy protect"},
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\""}, {"dumpprivkey", "Dump private key for an address", "\"address\"",
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]"}, "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_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\""},
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]"}, {"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]", "", "", "", true},
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\""}, {"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\"", "", "", "", true},
{"signmessage", "Sign a message with an address key", "\"address\" \"message\""}, {"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"}, {"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", ""}, {"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[] = { const ConsoleCommandEntry kRawTransactionCommands[] = {
@@ -107,13 +169,15 @@ const ConsoleCommandEntry kRawTransactionCommands[] = {
}; };
const ConsoleCommandEntry kUtilityCommands[] = { 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\""}, {"z_validateaddress", "Validate a z-address", "\"zaddr\""},
{"estimatefee", "Estimate fee for a transaction", "nblocks"}, {"estimatefee", "Estimate fee for a transaction", "nblocks"},
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\""}, {"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\""},
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]"}, {"createmultisig", "Create a multisig address", "nrequired [\"key\",...]"},
{"invalidateblock", "Mark a block as invalid", "\"hash\""}, {"invalidateblock", "Mark a block as invalid", "\"hash\"", "", "", "", true},
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\""}, {"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\"", "", "", "", true},
}; };
} // namespace } // namespace

View File

@@ -7,8 +7,14 @@ namespace ui {
struct ConsoleCommandEntry { struct ConsoleCommandEntry {
const char* name; const char* name;
const char* desc; const char* desc; // terse one-line summary (always set)
const char* params; 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 { 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 // Pull passive log lines (daemon/xmrig output, or the lite diagnostics ring) and any
// completed command results from the backend executor. // completed command results from the backend executor.
exec.pollLogLines([this](const std::string& l, ConsoleChannel c) { addLine(l, c); }); 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 { namespace {
// True if any of a command's name/desc/params contains `filterLower` (already lowercased). // True if any of a command's name/desc/params contains `filterLower` (already lowercased).
// An empty filter matches everything. // An empty filter matches everything.
bool consoleCommandMatchesFilter(const char* name, const char* desc, const char* params, static bool lcContains(const char* s, const std::string& needleLower)
const std::string& filterLower)
{ {
if (filterLower.empty()) return true; std::string v(s);
auto contains = [&filterLower](const char* s) { std::transform(v.begin(), v.end(), v.begin(), ::tolower);
std::string v(s); return v.find(needleLower) != std::string::npos;
std::transform(v.begin(), v.end(), v.begin(), ::tolower); }
return v.find(filterLower) != std::string::npos; static bool lcStartsWith(const char* s, const std::string& needleLower)
}; {
return contains(name) || contains(desc) || contains(params); 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]. // Search relevance of a command for a lowercased query. -1 = no match; higher = better. Keywords
void drawConsoleCommandParams(const char* params) // 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; if (q.empty()) return 0;
const char* p = params; if (lcStartsWith(cmd.name, q)) return 100;
bool first = true; if (lcContains(cmd.name, q)) return 60;
while (*p) { if (cmd.keywords[0] && lcContains(cmd.keywords, q)) return 40;
const char* bracketStart = strchr(p, '['); if (lcContains(cmd.desc, q)) return 25;
if (bracketStart) { if (cmd.details[0] && lcContains(cmd.details, q)) return 15;
// Draw the required part before the bracket. if (lcContains(cmd.params, q)) return 10;
if (bracketStart > p) { return -1;
if (!first) ImGui::SameLine(0, 0); }
std::string req(p, bracketStart);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", req.c_str()); // Split a parameter template into top-level tokens, respecting quote/bracket nesting so a space
first = false; // inside "..." or [{...}] doesn't split (e.g. `"address" [{"a":1}]` -> {`"address"`, `[{"a":1}]`}).
} std::vector<std::string> splitParamTemplate(const char* params)
const char* bracketEnd = strchr(bracketStart, ']'); {
if (bracketEnd) { std::vector<std::string> out;
if (!first) ImGui::SameLine(0, 0); std::string tok;
std::string opt(bracketStart, bracketEnd + 1); int depth = 0;
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", opt.c_str()); char q = 0;
first = false; for (const char* p = params; *p; ++p) {
p = bracketEnd + 1; char c = *p;
} else { if (q) { tok += c; if (c == q) q = 0; continue; }
if (!first) ImGui::SameLine(0, 0); if (c == '"' || c == '\'') { q = c; tok += c; continue; }
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", bracketStart); if (c == '[' || c == '{') { depth++; tok += c; continue; }
first = false; if (c == ']' || c == '}') { if (depth > 0) depth--; tok += c; continue; }
break; if (c == ' ' && depth == 0) { if (!tok.empty()) { out.push_back(tok); tok.clear(); } continue; }
} tok += c;
} else {
if (!first) ImGui::SameLine(0, 0);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", p);
first = false;
break;
}
} }
if (!tok.empty()) out.push_back(tok);
return out;
} }
// Translate a command-category name from the static reference tables (English) for display. The // Translate a command-category name from the static reference tables (English) for display. The
@@ -1504,154 +1510,276 @@ const char* consoleCategoryLabel(const char* name)
} }
} // namespace } // 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() void ConsoleTab::renderCommandsPopup()
{ {
using namespace material; using namespace material;
float dp = Layout::dpiScale();
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; material::OverlayDialogSpec ov;
ov.title = TR("console_rpc_reference"); ov.p_open = &show_commands_popup_; ov.title = TR("console_rpc_reference");
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading ov.p_open = &show_commands_popup_;
ov.cardWidth = popW; ov.cardBottomViewportRatio = 0.94f; // keep authored width ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
// FIXED height (fills most of the viewport) instead of auto-height: the command list's ov.cardWidth = 960.0f; // wide enough for two panes
// fill-height child reads GetContentRegionAvail() inside the dialog child, which is only stable ov.cardHeight = ImGui::GetMainViewport()->Size.y * 0.74f / dp; // fixed; both panes fill it
// when the child is fixed-height. Auto-height made it self-referential and it slid content ov.idSuffix = "cmdref";
// off-screen after a monitor move (same class of bug as the Wallets modal). if (!material::BeginOverlayDialog(ov)) return;
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;
// Search filter — auto-focus on open so the user can type immediately (like the console input). // 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); ImGui::SetNextItemWidth(-1);
if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere(); 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())); 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(); const auto& categories = consoleCommandCategories();
std::string filter(command_search_); // Flat display order of (cat,idx): ranked when searching, category order when browsing. Drives
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower); // keyboard nav + auto-selection; the browse view still renders grouped headers below.
std::vector<std::pair<int, int>> order;
ImGui::BeginChild("CmdListScroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - Layout::spacingXs()), if (searching) {
false); std::vector<std::pair<int, std::pair<int, int>>> scored; // (score, (cat,idx))
for (int c = 0; c < (int)categories.size(); c++)
ImGui::PushFont(Type().caption()); for (int i = 0; i < categories[c].count; i++) {
int s = consoleCommandRank(categories[c].commands[i], q);
// cmd-min-width is authored in logical px; popupInnerW (GetContentRegionAvail) is physical/DPI- if (s >= 0) scored.push_back({s, {c, i}});
// 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++;
}
} }
} std::stable_sort(scored.begin(), scored.end(),
if (matchCount == 0) { catIdx++; continue; } [](const auto& a, const auto& b) { return a.first > b.first; });
for (auto& s : scored) order.push_back(s.second);
// Default-open only the first category (Control); collapse the rest } else {
ImGuiTreeNodeFlags headerFlags = (catIdx == 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0; for (int c = 0; c < (int)categories.size(); c++)
// When filtering, open all matching categories for (int i = 0; i < categories[c].count; i++) order.push_back({c, i});
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);
} 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();
}
ImGui::Spacing();
}
} }
ImGui::PopFont(); // 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);
}
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();
};
// 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 {
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::EndChild(); ImGui::EndChild();
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::SameLine(0, gap);
// Close button (the filter is also reset when the modal next opens). // 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()));
if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) { if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) {
command_search_[0] = '\0'; command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false; show_commands_popup_ = false;
} }

View File

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

View File

@@ -1301,6 +1301,19 @@ void I18n::loadBuiltinEnglish()
strings_["console_cat_wallet"] = "Wallet"; strings_["console_cat_wallet"] = "Wallet";
strings_["console_cat_raw_transactions"] = "Raw Transactions"; strings_["console_cat_raw_transactions"] = "Raw Transactions";
strings_["console_cat_utility"] = "Utility"; 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 --- // --- Export All Keys Dialog ---
strings_["export_keys_btn"] = "Export Keys"; strings_["export_keys_btn"] = "Export Keys";