// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "lite_console_tab.h" #include "console_input_model.h" // reuse the generic command-history helpers #include "../../app.h" #include "../../data/wallet_state.h" #include "../../util/i18n.h" #include "../../wallet/lite_diagnostics.h" #include "../../wallet/lite_wallet_controller.h" #include "../layout.h" #include "../material/type.h" #include "../material/colors.h" #include "imgui.h" #include #include #include #include #include #include #include #include namespace dragonx { namespace ui { using namespace material; namespace { struct ConsoleLine { std::string text; ImU32 color; }; // Unified, in-memory console buffer: it interleaves the automatic diagnostics log (lifecycle / // connection / sync events) with the user's interactive commands and their responses, the same // way the full-node Console interleaves the daemon log with RPC I/O. NOTE: this buffer can hold // secret command output (e.g. `seed`/`export`) and is NEVER persisted to LiteDiagnostics. std::vector s_lines; std::uint64_t s_diagGen = static_cast(-1); // last consumed diagnostics generation char s_input[512] = ""; std::vector s_history; int s_historyIndex = -1; bool s_autoScroll = true; bool s_scrollToBottom = false; bool s_focusInput = false; // Colour error/success lines for at-a-glance scanning (substring match on the messages the // controller emits). Anything else renders in the muted default colour. ImU32 logColor(const std::string& line) { const auto has = [&line](const char* s) { return line.find(s) != std::string::npos; }; if (has("failed") || has(" unreachable") || has("blocked") || has("could not") || has("Error") || has("error")) return Error(); if (has(": connected") || has("opened") || has("wallet ready") || has("Ready")) return Success(); return OnSurfaceMedium(); } // Append text as one or more lines (splitting on '\n' so multi-line JSON responses render row by row). void appendLines(const std::string& text, ImU32 color) { std::size_t start = 0; while (start <= text.size()) { std::size_t nl = text.find('\n', start); std::string line = text.substr(start, nl == std::string::npos ? std::string::npos : nl - start); while (!line.empty() && (line.back() == '\r')) line.pop_back(); s_lines.push_back({std::move(line), color}); if (nl == std::string::npos) break; start = nl + 1; } } std::string lowerFirstToken(const std::string& cmd) { std::size_t b = cmd.find_first_not_of(" \t"); if (b == std::string::npos) return {}; std::size_t e = cmd.find_first_of(" \t", b); std::string tok = cmd.substr(b, e == std::string::npos ? std::string::npos : e - b); std::transform(tok.begin(), tok.end(), tok.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return tok; } // InputText history navigation (Up/Down) — reuses the generic console history helpers. int inputCallback(ImGuiInputTextCallbackData* data) { if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) { const bool up = (data->EventKey == ImGuiKey_UpArrow); s_historyIndex = NavigateConsoleHistoryIndex(s_historyIndex, s_history.size(), up); const std::string entry = ConsoleHistoryEntry(s_history, s_historyIndex); data->DeleteChars(0, data->BufTextLen); if (!entry.empty()) data->InsertChars(0, entry.c_str()); } return 0; } } // namespace void RenderLiteConsoleTab(App* app) { if (!app) return; wallet::LiteWalletController* lw = app->liteWallet(); // ── Header ────────────────────────────────────────────────────────────────── Type().text(TypeStyle::H6, TR("lite_console_title")); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("lite_console_intro")); ImGui::Spacing(); // ── Live status (read straight from the controller — always meaningful) ─────── { const char* connText; ImU32 connCol; if (!lw) { connText = "Lite backend not linked in this build (rebuild with --lite-backend)"; connCol = Error(); } else if (lw->walletOpen()) { connText = "Connected"; connCol = Success(); } else if (lw->openInProgress()) { connText = "Connecting\xE2\x80\xA6"; // ellipsis connCol = Warning(); } else { connText = "Disconnected"; connCol = Error(); } Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_console_status")); ImGui::SameLine(); Type().textColored(TypeStyle::Body2, connCol, connText); if (lw && lw->walletOpen()) { const SyncInfo& sync = app->state().sync; char buf[96]; if (sync.syncing && !sync.isSynced()) { double vp = sync.verification_progress; if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0; std::snprintf(buf, sizeof(buf), "Syncing %.1f%% (block %d / %d)", vp * 100.0, sync.blocks, sync.headers); } else { std::snprintf(buf, sizeof(buf), "Synced (block %d)", sync.blocks); } Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf); } const std::string& openErr = app->liteOpenError(); if (!openErr.empty() && (!lw || !lw->walletOpen())) { Type().textColored(TypeStyle::Caption, Error(), (std::string("Last error: ") + openErr).c_str()); } } ImGui::Spacing(); // ── Ingest new automatic diagnostics events into the unified buffer ─────────── auto& diag = wallet::LiteDiagnostics::instance(); const std::uint64_t gen = diag.generation(); if (gen != s_diagGen) { const auto snap = diag.snapshot(); // First pass dumps the whole ring; later passes append only the delta (generation counts // total events ever added; the ring is bounded, so cap the delta at the snapshot size). std::size_t startIdx = 0; if (s_diagGen != static_cast(-1)) { const std::uint64_t added = gen - s_diagGen; startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast(added); } for (std::size_t i = startIdx; i < snap.size(); ++i) s_lines.push_back({snap[i], logColor(snap[i])}); if (gen != s_diagGen && !snap.empty()) s_scrollToBottom = true; s_diagGen = gen; } // ── Drain a completed interactive command ──────────────────────────────────── if (lw) { wallet::LiteConsoleResult res; if (lw->takeConsoleResult(res)) { appendLines(res.response.empty() ? "(no output)" : res.response, res.ok ? OnSurface() : Error()); s_scrollToBottom = true; } } // ── Toolbar ───────────────────────────────────────────────────────────────── if (ImGui::Button(TR("lite_console_clear"))) { s_lines.clear(); s_diagGen = diag.generation(); // don't re-dump the whole ring on the next frame } ImGui::SameLine(); if (ImGui::Button(TR("lite_console_copy"))) { std::string all; for (const auto& l : s_lines) { all += l.text; all.push_back('\n'); } ImGui::SetClipboardText(all.c_str()); } ImGui::SameLine(); ImGui::Checkbox(TR("lite_console_autoscroll"), &s_autoScroll); ImGui::Spacing(); // ── Output log (terminal-styled scroll region) ─────────────────────────────── const float inputRowH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingSm(); ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 90)); ImGui::BeginChild("##LiteConsoleLog", ImVec2(0, -inputRowH), true, ImGuiWindowFlags_HorizontalScrollbar); ImGui::PushFont(Type().caption()); if (s_lines.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceDisabled()); ImGui::TextUnformatted(TR("lite_console_empty")); ImGui::PopStyleColor(); } else { for (const auto& line : s_lines) { ImGui::PushStyleColor(ImGuiCol_Text, line.color); ImGui::TextUnformatted(line.text.c_str()); // not format-interpreted — safe for any content ImGui::PopStyleColor(); } } if (s_scrollToBottom && s_autoScroll) ImGui::SetScrollHereY(1.0f); else if (s_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) ImGui::SetScrollHereY(1.0f); s_scrollToBottom = false; ImGui::PopFont(); ImGui::EndChild(); ImGui::PopStyleColor(); // ── Command input row ──────────────────────────────────────────────────────── const bool busy = lw && lw->consoleCommandInProgress(); auto submit = [&]() { std::string cmd = s_input; // trim const std::size_t b = cmd.find_first_not_of(" \t"); if (b == std::string::npos) { s_input[0] = '\0'; return; } cmd = cmd.substr(b); while (!cmd.empty() && (cmd.back() == ' ' || cmd.back() == '\t')) cmd.pop_back(); s_lines.push_back({"> " + cmd, Primary()}); AppendConsoleHistory(s_history, cmd); s_historyIndex = -1; const std::string first = lowerFirstToken(cmd); if (first == "clear" || first == "cls") { // The backend's `clear` wipes wallet tx history — NOT the screen. Intercept it as a // view-clear (what the user expects) so a stray "clear" can't destroy history. s_lines.clear(); s_diagGen = diag.generation(); } else if (first == "quit" || first == "exit") { appendLines(TR("lite_console_quit_note"), OnSurfaceMedium()); } else if (!lw) { appendLines("Lite backend not linked in this build.", Error()); } else if (!lw->runConsoleCommand(cmd)) { appendLines(TR("lite_console_busy"), Warning()); } s_input[0] = '\0'; s_scrollToBottom = true; s_focusInput = true; // keep focus for the next command }; ImGui::PushFont(Type().caption()); Type().textColored(TypeStyle::Caption, busy ? Warning() : Primary(), busy ? TR("lite_console_running") : "\xE2\x80\xBA"); // ‹running…› / "›" ImGui::SameLine(); ImGui::SetNextItemWidth(-FLT_MIN); if (busy) ImGui::BeginDisabled(); const ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackHistory; if (s_focusInput) { ImGui::SetKeyboardFocusHere(); s_focusInput = false; } if (ImGui::InputTextWithHint("##LiteConsoleInput", TR("lite_console_input_hint"), s_input, sizeof(s_input), flags, inputCallback)) { submit(); } if (busy) ImGui::EndDisabled(); ImGui::PopFont(); } } // namespace ui } // namespace dragonx