From c9e9c9f9798d9a0646b2bc07b34d3cd692f1ed40 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 11:07:49 -0500 Subject: [PATCH] feat(console): redesign the RPC reference as a novice-friendly command explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 619300 -> 622296 bytes res/lang/de.json | 13 + res/lang/es.json | 13 + res/lang/fr.json | 13 + res/lang/ja.json | 13 + res/lang/ko.json | 13 + res/lang/pt.json | 13 + res/lang/ru.json | 13 + res/lang/zh.json | 13 + src/ui/windows/console_command_reference.cpp | 142 ++++-- src/ui/windows/console_command_reference.h | 10 +- src/ui/windows/console_tab.cpp | 472 ++++++++++++------- src/ui/windows/console_tab.h | 9 +- src/util/i18n.cpp | 13 + 14 files changed, 536 insertions(+), 214 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 8d271988be34917ca7688f0c857248d3660a8526..0564b4ab7aae5ea657f43fe6d79e1b0dbd7a15d2 100644 GIT binary patch delta 21414 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 ZL|I8t%4Fw_uKF`4OU0e1lJzFa{|AkAzkvV% delta 18795 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 S0k#CkVD&tG!n82ur~Mz6UtN{} diff --git a/res/lang/de.json b/res/lang/de.json index 21a529b..ccc7d88 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -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", diff --git a/res/lang/es.json b/res/lang/es.json index 8ab8215..25f856c 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -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", diff --git a/res/lang/fr.json b/res/lang/fr.json index 8f096a7..805ebf1 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -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", diff --git a/res/lang/ja.json b/res/lang/ja.json index 859b1a9..50364dd 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -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": "コンソールスキャンライン", diff --git a/res/lang/ko.json b/res/lang/ko.json index 976156f..40fb139 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -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": "콘솔 스캔라인", diff --git a/res/lang/pt.json b/res/lang/pt.json index fd3f988..cbd2aad 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -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", diff --git a/res/lang/ru.json b/res/lang/ru.json index 1d36dae..66f8940 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -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": "Скан-линия консоли", diff --git a/res/lang/zh.json b/res/lang/zh.json index 11c761b..cdecf49 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -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": "控制台扫描线", diff --git a/src/ui/windows/console_command_reference.cpp b/src/ui/windows/console_command_reference.cpp index 9b0892f..42ea65b 100644 --- a/src/ui/windows/console_command_reference.cpp +++ b/src/ui/windows/console_command_reference.cpp @@ -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 diff --git a/src/ui/windows/console_command_reference.h b/src/ui/windows/console_command_reference.h index e5fd520..47ad00c 100644 --- a/src/ui/windows/console_command_reference.h +++ b/src/ui/windows/console_command_reference.h @@ -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 { diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 0d85732..301715f 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -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 splitParamTemplate(const char* params) +{ + std::vector 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 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> order; + if (searching) { + std::vector>> 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(); } diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index 321b884..5c2f56a 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -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 diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index d8dd986..ebb29fb 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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";