Files
ObsidianDragon/src/ui/windows/console_model.h
DanS 245d9bf976 feat(console): collapsible JSON in command output
Add fold/unfold of JSON objects and arrays in console command results, built on
the refactored channel + model foundation.

- ComputeConsoleFoldSpans() (console_input_model): pure brace/bracket matcher
  over a block of pretty-printed JSON lines; each opener line gets the offset to
  its matching closer at the same indentation (empty/one-line blocks are not
  foldable). Unit tested (nested objects, arrays, empty blocks, non-JSON, and a
  string value that merely contains a brace).
- ConsoleModelLine gains foldSpan (relative, so it survives front-eviction by the
  line cap) + a collapsed flag; ingest() carries the span and toggleCollapsed()
  flips openers (main thread). addFormattedResult computes the block's spans and
  ingests them atomically.
- computeVisibleLines() skips a collapsed block's interior (opener stays, its
  foldSpan lines through the closer are hidden); folding is bypassed while a
  filter is active so every match stays reachable.
- drawVisibleLines() draws a fold triangle in the free left gutter of opener
  lines (result/JSON channels carry no accent bar there), toggled by a click in
  the gutter cell, and appends a dim " ... }" / " ... ]" summary on collapsed
  openers.

Full-node + lite build clean; ctest green (incl. fold-span + model-fold tests);
source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:11:44 -05:00

88 lines
3.7 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// ConsoleModel — the thread-safe line store behind ConsoleTab.
//
// The console is fed from multiple threads: user commands + drained daemon/lite log lines
// arrive on the main (render) thread, but RPC-trace and app-logger lines can be produced on
// background worker threads. Previously a single mutex guarded both the line container AND
// the UI-only interaction state (selection, scroll, auto-scroll counters), and it was held
// across the whole render pass — yet some of that UI state was still touched outside the
// lock in render(), racing the background producers.
//
// ConsoleModel splits the two concerns. Producers on ANY thread call ingest(), which only
// briefly locks a small pending queue. The main thread calls drain() once per frame to move
// pending lines into the visible deque and apply the line cap. The visible deque is then
// only ever read/written on the main thread, so ConsoleTab needs no lock during render and
// its selection/scroll fields become pure main-thread state.
#pragma once
#include "console_channel.h"
#include <cstddef>
#include <deque>
#include <mutex>
#include <string>
#include <vector>
namespace dragonx {
namespace ui {
struct ConsoleModelLine {
std::string text;
ConsoleChannel channel = ConsoleChannel::None;
// JSON folding: for a line that opens a collapsible block, `foldSpan` is the offset to
// its matching closing-bracket line (>= 2); 0 for non-openers. `collapsed` is the user's
// fold toggle (main-thread UI state).
int foldSpan = 0;
bool collapsed = false;
};
class ConsoleModel {
public:
static constexpr std::size_t kDefaultMaxLines = 10000;
explicit ConsoleModel(std::size_t maxLines = kDefaultMaxLines) : max_lines_(maxLines) {}
// Append a line from ANY thread. Cheap: locks only the pending queue, does not touch
// the visible deque. The line becomes visible after the next drain(). `foldSpan` marks a
// JSON block opener (offset to its matching closer); 0 for ordinary lines.
void ingest(const std::string& text, ConsoleChannel channel, int foldSpan = 0);
struct DrainResult {
std::size_t added = 0; // lines appended to the visible deque this drain
std::size_t popped = 0; // lines evicted from the front by the line cap
};
// Main thread only. Move pending lines into the visible deque, then enforce the cap.
// Returns how many were added and how many were evicted from the front (so the caller
// can shift any selection indices that referenced the evicted lines).
DrainResult drain();
// Main thread only. Clear the visible deque. Does NOT discard queued pending lines —
// new output still appears — matching the console's view-only `clear` semantics.
void clear();
// Main thread only. Toggle a foldable line's collapsed state (no-op if out of range or
// not a block opener). Returns the new collapsed state.
bool toggleCollapsed(std::size_t i);
// Visible-model access — main thread only (no lock; the deque is main-thread-owned).
const std::deque<ConsoleModelLine>& lines() const { return lines_; }
std::size_t size() const { return lines_.size(); }
bool empty() const { return lines_.empty(); }
const ConsoleModelLine& operator[](std::size_t i) const { return lines_[i]; }
const ConsoleModelLine& back() const { return lines_.back(); }
private:
const std::size_t max_lines_;
std::deque<ConsoleModelLine> lines_; // visible model — main thread only
std::vector<ConsoleModelLine> pending_; // guarded by ingest_mutex_
std::mutex ingest_mutex_;
};
} // namespace ui
} // namespace dragonx