-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlog.go
More file actions
316 lines (272 loc) · 8.29 KB
/
log.go
File metadata and controls
316 lines (272 loc) · 8.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package glyph
import (
"bufio"
"io"
"sync"
)
type LogC struct {
reader io.Reader
maxLines int
autoScroll bool
onUpdate func() // called when new lines arrive (for RequestRender)
// layout
grow float32
margin [4]int16
flexGrowPtr *float32
flexGrowCond conditionNode
// styling
style Style
colorize func(string) []Span
// key bindings
declaredBindings []binding
// internal state
layer *Layer
lines []string
mu sync.Mutex
started sync.Once
following bool // true = auto-scroll active, false = user scrolled away
newLineCount int // lines arrived while not following (for "X new lines" indicator)
}
// Log creates a scrollable log viewer that reads lines from an io.Reader.
// The reader is consumed in a background goroutine that exits on EOF/error.
// Lines are buffered with a configurable max (ring buffer); scrolling follows
// new content automatically until the user scrolls away.
func Log(r io.Reader) *LogC {
return &LogC{
reader: r,
maxLines: 10000, // large default buffer
autoScroll: true,
layer: NewLayer(),
following: true, // start following new content
}
}
// MaxLines sets the maximum number of lines to keep in the buffer.
// Oldest lines are dropped when the limit is exceeded. Default is 1000.
func (lv *LogC) MaxLines(n int) *LogC {
lv.maxLines = n
return lv
}
// AutoScroll controls whether the view automatically scrolls to show new lines.
// Default is true. When the user scrolls up, auto-scroll pauses until they
// return to the bottom.
func (lv *LogC) AutoScroll(enabled bool) *LogC {
lv.autoScroll = enabled
return lv
}
func (lv *LogC) FG(c any) *LogC {
if col, ok := c.(Color); ok {
lv.style.FG = col
}
return lv
}
func (lv *LogC) BG(c any) *LogC {
if col, ok := c.(Color); ok {
lv.style.BG = col
}
return lv
}
// Colorize sets a function that transforms each line into styled spans.
// When set, lines are rendered with WriteSpans instead of WriteStringFast.
func (lv *LogC) Colorize(fn func(string) []Span) *LogC {
lv.colorize = fn
return lv
}
// Grow sets the flex grow factor. Accepts float32, float64, int, or *float32 for dynamic values.
func (lv *LogC) Grow(g any) *LogC {
switch val := g.(type) {
case float32:
lv.grow = val
case float64:
lv.grow = float32(val)
case int:
lv.grow = float32(val)
case *float32:
lv.flexGrowPtr = val
case conditionNode:
lv.flexGrowCond = val
}
return lv
}
// Margin sets equal margin on all sides.
func (lv *LogC) Margin(all int16) *LogC {
lv.margin = [4]int16{all, all, all, all}
return lv
}
// MarginVH sets vertical and horizontal margins.
func (lv *LogC) MarginVH(v, h int16) *LogC {
lv.margin = [4]int16{v, h, v, h}
return lv
}
// MarginTRBL sets top, right, bottom, left margins individually.
func (lv *LogC) MarginTRBL(t, r, b, l int16) *LogC {
lv.margin = [4]int16{t, r, b, l}
return lv
}
// Layer returns the underlying layer for manual scroll control.
// Use this to bind key handlers for scrolling (j/k, Page Up/Down, etc.).
func (lv *LogC) Layer() *Layer {
return lv.layer
}
// Ref calls f with this LogC and returns it for chaining.
func (lv *LogC) Ref(f func(*LogC)) *LogC {
f(lv)
return lv
}
// NewLines returns the number of new lines that have arrived while not following.
// Use this to display an indicator like "42 new lines ↓".
func (lv *LogC) NewLines() int {
lv.mu.Lock()
defer lv.mu.Unlock()
return lv.newLineCount
}
// resume syncs the display to current buffer and resets new line count.
func (lv *LogC) resume() {
lv.mu.Lock()
defer lv.mu.Unlock()
lv.following = true
lv.newLineCount = 0
lv.syncToLayer()
lv.layer.ScrollToEnd()
}
// OnUpdate sets a callback to be called when new lines arrive.
// Use this with app.RequestRender to trigger redraws:
//
// Log(reader).OnUpdate(app.RequestRender)
func (lv *LogC) OnUpdate(f func()) *LogC {
lv.onUpdate = f
return lv
}
// BindNav registers key bindings for scrolling down/up by one line.
func (lv *LogC) BindNav(down, up string) *LogC {
lv.declaredBindings = append(lv.declaredBindings,
binding{down, func() { lv.layer.ScrollDown(1) }},
binding{up, func() { lv.following = false; lv.layer.ScrollUp(1) }},
)
return lv
}
// BindPageNav registers key bindings for half-page scrolling.
func (lv *LogC) BindPageNav(down, up string) *LogC {
lv.declaredBindings = append(lv.declaredBindings,
binding{down, func() { lv.layer.HalfPageDown() }},
binding{up, func() { lv.following = false; lv.layer.HalfPageUp() }},
)
return lv
}
// BindFirstLast registers key bindings for jumping to top/bottom.
func (lv *LogC) BindFirstLast(first, last string) *LogC {
lv.declaredBindings = append(lv.declaredBindings,
binding{first, func() { lv.following = false; lv.layer.ScrollToTop() }},
binding{last, func() { lv.resume() }},
)
return lv
}
// BindVimNav wires standard vim-style scroll keys:
// j/k: line, Ctrl-d/u: half-page, g/G: top/bottom
func (lv *LogC) BindVimNav() *LogC {
return lv.BindNav("j", "k").BindPageNav("<C-d>", "<C-u>").BindFirstLast("g", "G")
}
// bindings implements the bindable interface.
func (lv *LogC) bindings() []binding {
return lv.declaredBindings
}
// start begins reading from the reader in a background goroutine.
// Called once via sync.Once when the component is first compiled.
func (lv *LogC) start() {
go lv.readLoop()
}
// readLoop reads lines from the reader and appends them to the buffer.
// Exits when the reader returns EOF or an error.
func (lv *LogC) readLoop() {
scanner := bufio.NewScanner(lv.reader)
// increase scanner buffer for long lines
const maxLineSize = 1024 * 1024 // 1MB
scanner.Buffer(make([]byte, 64*1024), maxLineSize)
for scanner.Scan() {
line := scanner.Text()
lv.mu.Lock()
lv.lines = append(lv.lines, line)
// ring buffer: drop oldest if over limit
if lv.maxLines > 0 && len(lv.lines) > lv.maxLines {
dropped := len(lv.lines) - lv.maxLines
lv.lines = lv.lines[dropped:]
// adjust scroll position to keep viewing same content
if !lv.following {
newScrollY := lv.layer.ScrollY() - dropped
if newScrollY >= 0 {
lv.layer.ScrollTo(newScrollY)
}
// if newScrollY < 0, content is gone but we keep them at 0
// they can still scroll down through remaining buffered content
}
}
// always sync so layer has valid content at any viewport size
lv.syncToLayer()
if lv.following {
// following: scroll to end
if lv.autoScroll {
lv.layer.ScrollToEnd()
}
} else {
// not following: count new lines for indicator
lv.newLineCount++
}
lv.mu.Unlock()
// request redraw
if lv.onUpdate != nil {
lv.onUpdate()
}
}
}
// Refresh re-renders all buffered lines to the layer.
// Call this when external state used by the Colorize callback changes (e.g. theme switch).
func (lv *LogC) Refresh() {
lv.mu.Lock()
defer lv.mu.Unlock()
lv.syncToLayer()
if lv.following {
lv.layer.ScrollToEnd()
}
}
// syncToLayer writes all buffered lines to the layer's buffer.
func (lv *LogC) syncToLayer() {
if len(lv.lines) == 0 {
return
}
// create exact-sized buffer (EnsureSize only grows, which breaks maxScroll after ring buffer truncates)
const bufferWidth = 500
buf := NewBuffer(bufferWidth, len(lv.lines))
if lv.colorize != nil {
for i, line := range lv.lines {
buf.ClearLineWithStyle(i, lv.style)
buf.WriteSpans(0, i, lv.colorize(line), bufferWidth)
}
} else {
for i, line := range lv.lines {
buf.WriteStringFast(0, i, line, lv.style, bufferWidth)
}
}
lv.layer.SetBuffer(buf)
}
// compileLogC compiles the Log component into the template.
// Starts the reader goroutine on first compile and returns a LayerView.
func (t *Template) compileLogC(lv *LogC, parent int16, depth int) int16 {
// collect for later wiring (app not available yet during compile)
if lv.onUpdate == nil {
t.pendingLogs = append(t.pendingLogs, lv)
}
// start reader goroutine (once)
lv.started.Do(lv.start)
// compile as LayerView with the internal layer
var layerView LayerViewC
if lv.flexGrowCond != nil {
layerView = LayerView(lv.layer).Grow(lv.flexGrowCond)
} else if lv.flexGrowPtr != nil {
layerView = LayerView(lv.layer).Grow(lv.flexGrowPtr)
} else {
layerView = LayerView(lv.layer).Grow(lv.grow)
}
if lv.margin != [4]int16{} {
layerView = layerView.MarginTRBL(lv.margin[0], lv.margin[1], lv.margin[2], lv.margin[3])
}
return t.compileLayerViewC(layerView, parent, depth)
}