Class: Clacky::UI2::OutputBuffer

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/ui2/output_buffer.rb

Overview

OutputBuffer manages the logical sequence of rendered output lines.

It replaces the scattered state that used to live across LayoutManager (@output_buffer + @output_row) and UIController (@progress_message / “last line” assumptions).

Core concepts:

  • Every append returns an id. Callers can later replace(id, …) or remove(id) that exact entry without relying on “the last line”.

  • Each entry tracks whether it has been “committed” to the terminal scrollback (i.e. scrolled off the top of the visible window by a native terminal n). Committed entries are NEVER re-drawn from the buffer again — this is what prevents the classic “scroll up shows a duplicated line” bug.

  • Entries may contain multi-line content (already wrapped). Each entry stores its visual line count so the renderer can compute exact rows to clear when replacing or removing.

The buffer itself does NOT talk to the terminal. It is a pure data structure; a renderer (LayoutManager) consumes it through the snapshot APIs: visible_entries, entry_by_id, tail_lines.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

DEFAULT_MAX_ENTRIES =
2000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_entries: DEFAULT_MAX_ENTRIES) ⇒ OutputBuffer

Returns a new instance of OutputBuffer.



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/clacky/ui2/output_buffer.rb', line 51

def initialize(max_entries: DEFAULT_MAX_ENTRIES)
  @entries       = []   # Array<Entry> in insertion order
  @index         = {}   # id => Entry (fast lookup)
  @next_id       = 1
  @max_entries   = max_entries
  @mutex         = Mutex.new
  # Monotonic counter incremented every time the buffer changes.
  # Renderers can compare this against a saved version to decide
  # whether their cached screen image is still valid.
  @version       = 0
end

Instance Attribute Details

#entriesObject (readonly)

Returns the value of attribute entries.



49
50
51
# File 'lib/clacky/ui2/output_buffer.rb', line 49

def entries
  @entries
end

Instance Method Details

#append(content, kind: :text) ⇒ Integer

Append a new entry. content may be a String (may include n) or an Array<String> of already-split lines.

Parameters:

  • content (String, Array<String>)
  • kind (Symbol) (defaults to: :text)

    :text (default), :progress, :system

Returns:

  • (Integer)

    id of the newly created entry



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/clacky/ui2/output_buffer.rb', line 69

def append(content, kind: :text)
  @mutex.synchronize do
    lines = normalize_lines(content)
    entry = Entry.new(id: next_id!, lines: lines, kind: kind, committed: false)
    @entries << entry
    @index[entry.id] = entry
    trim_if_needed
    bump_version
    entry.id
  end
end

#clearObject

Clear everything. Used by /clear command.



251
252
253
254
255
256
257
# File 'lib/clacky/ui2/output_buffer.rb', line 251

def clear
  @mutex.synchronize do
    @entries.clear
    @index.clear
    bump_version
  end
end

#commit_oldest_lines(line_count) ⇒ Integer

Commit the oldest N entries. Used when the renderer scrolls N lines off the top via native n. It commits full entries greedily: if the N lines span across entry boundaries, all fully-scrolled entries are marked committed, and the partially-scrolled entry (if any) is left uncommitted (it will be handled next time).

Parameters:

  • line_count (Integer)

    Number of visual lines pushed to scrollback

Returns:

  • (Integer)

    Number of entries actually marked committed



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/clacky/ui2/output_buffer.rb', line 153

def commit_oldest_lines(line_count)
  return 0 if line_count <= 0

  @mutex.synchronize do
    remaining = line_count
    committed = 0
    @entries.each do |e|
      break if remaining <= 0
      next if e.committed

      if e.height <= remaining
        e.committed = true
        remaining  -= e.height
        committed  += 1
      else
        # Partial scroll — can't commit this entry yet
        break
      end
    end
    bump_version if committed > 0
    committed
  end
end

#commit_through(id) ⇒ Object

Mark an entry (and every older live entry) as committed to terminal scrollback. Called by the renderer after it has emitted a native n that scrolled the top-of-screen row off into scrollback.

Committing always flows from oldest → newest: if entry X is committed, every entry older than X must also be committed, because they have already scrolled past X on the screen.

Parameters:

  • id (Integer)


131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/clacky/ui2/output_buffer.rb', line 131

def commit_through(id)
  @mutex.synchronize do
    committed_any = false
    @entries.each do |e|
      break if e.id > id
      unless e.committed
        e.committed = true
        committed_any = true
      end
    end
    bump_version if committed_any
  end
end

#entry_by_id(id) ⇒ Entry?

Look up an entry by id.

Parameters:

  • id (Integer)

Returns:



217
218
219
# File 'lib/clacky/ui2/output_buffer.rb', line 217

def entry_by_id(id)
  @mutex.synchronize { @index[id] }
end

#live?(id) ⇒ Boolean

Does this id still refer to a live, editable entry?

Parameters:

  • id (Integer)

Returns:

  • (Boolean)


223
224
225
226
227
228
# File 'lib/clacky/ui2/output_buffer.rb', line 223

def live?(id)
  @mutex.synchronize do
    e = @index[id]
    !!(e && !e.committed)
  end
end

#live_entriesArray<Entry>

Entries that are still live (not committed). These are candidates for re-rendering into the visible output area.

Returns:



181
182
183
# File 'lib/clacky/ui2/output_buffer.rb', line 181

def live_entries
  @mutex.synchronize { @entries.reject(&:committed).dup }
end

#live_line_countObject

Total visual lines across live entries.



241
242
243
# File 'lib/clacky/ui2/output_buffer.rb', line 241

def live_line_count
  @mutex.synchronize { @entries.sum { |e| e.committed ? 0 : e.height } }
end

#live_sizeObject

Number of live entries.



236
237
238
# File 'lib/clacky/ui2/output_buffer.rb', line 236

def live_size
  @mutex.synchronize { @entries.count { |e| !e.committed } }
end

#remove(id) ⇒ Entry?

Remove an entry. Committed entries cannot be removed (they are in terminal scrollback). Returns the removed Entry, or nil if no-op.

Parameters:

  • id (Integer)

Returns:



109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/clacky/ui2/output_buffer.rb', line 109

def remove(id)
  @mutex.synchronize do
    entry = @index[id]
    return nil unless entry
    return nil if entry.committed

    @entries.delete(entry)
    @index.delete(id)
    bump_version
    entry
  end
end

#replace(id, content) ⇒ Integer?

Replace an existing entry’s content. If the id no longer exists (e.g. the entry was trimmed or already committed and recycled), this is a no-op and returns nil.

Replacing a committed entry is silently ignored — committed content lives in terminal scrollback and cannot be edited in place.

Parameters:

  • id (Integer)
  • content (String, Array<String>)

Returns:

  • (Integer, nil)

    Old height if replaced, nil if no-op



91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/clacky/ui2/output_buffer.rb', line 91

def replace(id, content)
  @mutex.synchronize do
    entry = @index[id]
    return nil unless entry
    return nil if entry.committed

    old_height = entry.lines.length
    entry.lines = normalize_lines(content)
    bump_version
    old_height
  end
end

#sizeObject

Total number of entries (committed + live) currently tracked.



231
232
233
# File 'lib/clacky/ui2/output_buffer.rb', line 231

def size
  @mutex.synchronize { @entries.size }
end

#tail_lines(n) ⇒ Array<String>

The last N *visual lines* across live entries, preserving entry boundaries. Returns an Array<String> suitable for row-by-row painting. If the last live entry is taller than n, only its last n lines are returned.

Parameters:

  • n (Integer)

Returns:

  • (Array<String>)


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/clacky/ui2/output_buffer.rb', line 192

def tail_lines(n)
  return [] if n <= 0

  @mutex.synchronize do
    collected = []
    @entries.reverse_each do |e|
      break if collected.length >= n
      next if e.committed

      # Prepend the entry's lines in order
      remaining = n - collected.length
      if e.lines.length <= remaining
        collected = e.lines + collected
      else
        collected = e.lines.last(remaining) + collected
        break
      end
    end
    collected
  end
end

#versionObject

Monotonic version (incremented on every mutation).



246
247
248
# File 'lib/clacky/ui2/output_buffer.rb', line 246

def version
  @version
end