Files
ObsidianDragon/src/ui/windows/lite_console_tab.cpp
dan_s 3d60de49fe fix(lite): always-populated Console (live status) + single-instance log
The Console could look empty if the wallet produced few events. Make it useful
in every state and remove a cross-platform footgun:

- Add a live status header read straight from the controller (connected /
  connecting / disconnected, sync %, and the last open error) — independent of the
  diagnostics event log, so the Console always shows the current connection +
  wallet-open state even when the log is sparse.
- Move LiteDiagnostics::instance() into a single .cpp so there is exactly one
  instance across the binary, rather than relying on the linker folding an
  inline-function static across translation units (a known fragility, especially
  on mingw/Windows — the most likely cause of a stuck-empty event log there).

Verified the writer and reader share one instance on Linux; builds clean for
full-node, lite, and Windows cross-compile; tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:28:28 -05:00

149 lines
5.8 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_console_tab.h"
#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 <cstdint>
#include <cstdio>
#include <string>
#include <vector>
namespace dragonx {
namespace ui {
using namespace material;
namespace {
// Re-snapshot the (mutex-guarded) log only when it actually changes, not every frame.
std::vector<std::string> s_lines;
std::uint64_t s_cachedGeneration = static_cast<std::uint64_t>(-1);
bool s_autoScroll = true;
// 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 lineColor(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();
}
} // namespace
void RenderLiteConsoleTab(App* app)
{
if (!app) return;
// ── 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, even if the
// event log below is empty) ─────────────────────────────────────────────────
{
const wallet::LiteWalletController* lw = app->liteWallet();
const char* connText;
ImU32 connCol;
if (!lw) {
connText = "Lite backend unavailable";
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);
}
// Surface the last open failure (e.g. server unreachable) prominently.
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();
// Snapshot the event log (only when it changed).
auto& diag = wallet::LiteDiagnostics::instance();
const std::uint64_t gen = diag.generation();
if (gen != s_cachedGeneration) {
s_lines = diag.snapshot();
s_cachedGeneration = gen;
}
// ── Toolbar ─────────────────────────────────────────────────────────────────
if (ImGui::Button(TR("lite_console_clear"))) diag.clear();
ImGui::SameLine();
if (ImGui::Button(TR("lite_console_copy"))) {
std::string all;
for (const auto& l : s_lines) { all += l; all.push_back('\n'); }
ImGui::SetClipboardText(all.c_str());
}
ImGui::SameLine();
ImGui::Checkbox(TR("lite_console_autoscroll"), &s_autoScroll);
ImGui::Spacing();
// ── Event log (terminal-styled scroll region) ───────────────────────────────
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 90));
ImGui::BeginChild("##LiteConsoleLog", ImVec2(0, 0), 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, lineColor(line));
ImGui::TextUnformatted(line.c_str()); // not format-interpreted — safe for any content
ImGui::PopStyleColor();
}
}
// Keep pinned to the newest line only while the user is already at the bottom.
if (s_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
ImGui::SetScrollHereY(1.0f);
ImGui::PopFont();
ImGui::EndChild();
ImGui::PopStyleColor();
}
} // namespace ui
} // namespace dragonx