Commit 92cd9b0
Eric Bower
·
2026-01-16 15:47:14 -0500 EST
parent 62cd4d9
feat(tui.logs): allow logs to be manually scrolled
2 files changed,
+63,
-8
+4,
-0
| ... | ... | @@ -39,6 +39,10 @@ func (m *TextInput) FocusOut() (vxfw.Command, error) { | |
| 39 | 39 | return vxfw.RedrawCmd{}, nil | |
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | + | func (m *TextInput) Focused() bool { | |
| 43 | + | return m.focus | |
| 44 | + | } | |
| 45 | + | ||
| 42 | 46 | func (m *TextInput) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) { | |
| 43 | 47 | return nil, nil | |
| 44 | 48 | } |
+59,
-8
| ... | ... | @@ -31,6 +31,7 @@ type LogsPage struct { | |
| 31 | 31 | logs []*LogLine | |
| 32 | 32 | ctx context.Context | |
| 33 | 33 | done context.CancelFunc | |
| 34 | + | focus string | |
| 34 | 35 | } | |
| 35 | 36 | ||
| 36 | 37 | func NewLogsPage(shrd *SharedModel) *LogsPage { |
| ... | ... | @@ -38,12 +39,16 @@ func NewLogsPage(shrd *SharedModel) *LogsPage { | |
| 38 | 39 | shared: shrd, | |
| 39 | 40 | input: NewTextInput("filter logs"), | |
| 40 | 41 | } | |
| 41 | - | page.list = &list.Dynamic{Builder: page.getWidget, DisableEventHandlers: true} | |
| 42 | + | page.list = &list.Dynamic{Builder: page.getWidget, DrawCursor: true} | |
| 42 | 43 | return page | |
| 43 | 44 | } | |
| 44 | 45 | ||
| 45 | 46 | func (m *LogsPage) Footer() []Shortcut { | |
| 46 | - | return []Shortcut{} | |
| 47 | + | return []Shortcut{ | |
| 48 | + | {Shortcut: "tab", Text: "toggle focus"}, | |
| 49 | + | {Shortcut: "↑↓", Text: "scroll"}, | |
| 50 | + | {Shortcut: "G", Text: "bottom"}, | |
| 51 | + | } | |
| 47 | 52 | } | |
| 48 | 53 | ||
| 49 | 54 | func (m *LogsPage) filterLogLine(match string, ll *LogLine) bool { |
| ... | ... | @@ -73,19 +78,28 @@ func (m *LogsPage) filterLogs() { | |
| 73 | 78 | filtered = append(filtered, idx) | |
| 74 | 79 | } | |
| 75 | 80 | } | |
| 81 | + | ||
| 82 | + | // Check if cursor is at the last position before filtering | |
| 83 | + | isAtBottom := len(m.filtered) == 0 || int(m.list.Cursor()) == len(m.filtered)-1 | |
| 84 | + | ||
| 76 | 85 | m.filtered = filtered | |
| 77 | 86 | ||
| 78 | - | // scroll to bottom | |
| 79 | - | if len(m.filtered) > 0 { | |
| 87 | + | // scroll to bottom only if we were already at the bottom | |
| 88 | + | if isAtBottom && len(m.filtered) > 0 { | |
| 80 | 89 | m.list.SetCursor(uint(len(m.filtered) - 1)) | |
| 81 | 90 | } | |
| 82 | 91 | } | |
| 83 | 92 | ||
| 84 | 93 | func (m *LogsPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) { | |
| 85 | - | switch ev.(type) { | |
| 94 | + | switch msg := ev.(type) { | |
| 86 | 95 | case vaxis.Key: | |
| 87 | - | m.filterLogs() | |
| 88 | - | return vxfw.RedrawCmd{}, nil | |
| 96 | + | if msg.Matches(vaxis.KeyTab) { | |
| 97 | + | return nil, nil | |
| 98 | + | } | |
| 99 | + | if m.focus == "input" { | |
| 100 | + | m.filterLogs() | |
| 101 | + | return vxfw.RedrawCmd{}, nil | |
| 102 | + | } | |
| 89 | 103 | } | |
| 90 | 104 | return nil, nil | |
| 91 | 105 | } |
| ... | ... | @@ -96,9 +110,35 @@ func (m *LogsPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Comm | |
| 96 | 110 | go func() { | |
| 97 | 111 | _ = m.connectToLogs() | |
| 98 | 112 | }() | |
| 113 | + | m.focus = "input" | |
| 99 | 114 | return m.input.FocusIn() | |
| 100 | 115 | case PageOut: | |
| 101 | 116 | m.done() | |
| 117 | + | case vaxis.Key: | |
| 118 | + | if msg.Matches(vaxis.KeyTab) { | |
| 119 | + | if m.focus == "input" { | |
| 120 | + | m.focus = "list" | |
| 121 | + | m.filterLogs() | |
| 122 | + | cmd, _ := m.input.FocusOut() | |
| 123 | + | return vxfw.BatchCmd([]vxfw.Command{ | |
| 124 | + | vxfw.FocusWidgetCmd(m.list), | |
| 125 | + | cmd, | |
| 126 | + | }), nil | |
| 127 | + | } | |
| 128 | + | m.focus = "input" | |
| 129 | + | cmd, _ := m.input.FocusIn() | |
| 130 | + | return vxfw.BatchCmd([]vxfw.Command{ | |
| 131 | + | cmd, | |
| 132 | + | vxfw.RedrawCmd{}, | |
| 133 | + | }), nil | |
| 134 | + | } | |
| 135 | + | if msg.Matches('G') { | |
| 136 | + | // Scroll to bottom | |
| 137 | + | if len(m.filtered) > 0 { | |
| 138 | + | m.list.SetCursor(uint(len(m.filtered) - 1)) | |
| 139 | + | return vxfw.RedrawCmd{}, nil | |
| 140 | + | } | |
| 141 | + | } | |
| 102 | 142 | case LogLineLoaded: | |
| 103 | 143 | ll := NewLogLine(msg.Line) | |
| 104 | 144 | m.logs = append(m.logs, ll) |
| ... | ... | @@ -116,7 +156,10 @@ func (m *LogsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) { | |
| 116 | 156 | txtSurf, _ := txt.Draw(ctx) | |
| 117 | 157 | root.AddChild(0, 0, txtSurf) | |
| 118 | 158 | } else { | |
| 119 | - | listSurf, _ := m.list.Draw(createDrawCtx(ctx, ctx.Max.Height-4)) | |
| 159 | + | listPane := NewBorder(m.list) | |
| 160 | + | listPane.Label = "logs" | |
| 161 | + | m.focusBorder(listPane) | |
| 162 | + | listSurf, _ := listPane.Draw(createDrawCtx(ctx, ctx.Max.Height-4)) | |
| 120 | 163 | root.AddChild(0, 0, listSurf) | |
| 121 | 164 | } | |
| 122 | 165 |
| ... | ... | @@ -126,6 +169,14 @@ func (m *LogsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) { | |
| 126 | 169 | return root, nil | |
| 127 | 170 | } | |
| 128 | 171 | ||
| 172 | + | func (m *LogsPage) focusBorder(border *Border) { | |
| 173 | + | if m.focus == "list" { | |
| 174 | + | border.Style = vaxis.Style{Foreground: oj} | |
| 175 | + | } else { | |
| 176 | + | border.Style = vaxis.Style{Foreground: purp} | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | ||
| 129 | 180 | func (m *LogsPage) getWidget(i uint, cursor uint) vxfw.Widget { | |
| 130 | 181 | if len(m.filtered) == 0 { | |
| 131 | 182 | return nil |