- commit
- 92cd9b0
- parent
- 62cd4d9
- author
- Eric Bower
- date
- 2026-01-16 15:47:14 -0500 EST
feat(tui.logs): allow logs to be manually scrolled
2 files changed,
+63,
-8
+4,
-0
1@@ -39,6 +39,10 @@ func (m *TextInput) FocusOut() (vxfw.Command, error) {
2 return vxfw.RedrawCmd{}, nil
3 }
4
5+func (m *TextInput) Focused() bool {
6+ return m.focus
7+}
8+
9 func (m *TextInput) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
10 return nil, nil
11 }
+59,
-8
1@@ -31,6 +31,7 @@ type LogsPage struct {
2 logs []*LogLine
3 ctx context.Context
4 done context.CancelFunc
5+ focus string
6 }
7
8 func NewLogsPage(shrd *SharedModel) *LogsPage {
9@@ -38,12 +39,16 @@ func NewLogsPage(shrd *SharedModel) *LogsPage {
10 shared: shrd,
11 input: NewTextInput("filter logs"),
12 }
13- page.list = &list.Dynamic{Builder: page.getWidget, DisableEventHandlers: true}
14+ page.list = &list.Dynamic{Builder: page.getWidget, DrawCursor: true}
15 return page
16 }
17
18 func (m *LogsPage) Footer() []Shortcut {
19- return []Shortcut{}
20+ return []Shortcut{
21+ {Shortcut: "tab", Text: "toggle focus"},
22+ {Shortcut: "↑↓", Text: "scroll"},
23+ {Shortcut: "G", Text: "bottom"},
24+ }
25 }
26
27 func (m *LogsPage) filterLogLine(match string, ll *LogLine) bool {
28@@ -73,19 +78,28 @@ func (m *LogsPage) filterLogs() {
29 filtered = append(filtered, idx)
30 }
31 }
32+
33+ // Check if cursor is at the last position before filtering
34+ isAtBottom := len(m.filtered) == 0 || int(m.list.Cursor()) == len(m.filtered)-1
35+
36 m.filtered = filtered
37
38- // scroll to bottom
39- if len(m.filtered) > 0 {
40+ // scroll to bottom only if we were already at the bottom
41+ if isAtBottom && len(m.filtered) > 0 {
42 m.list.SetCursor(uint(len(m.filtered) - 1))
43 }
44 }
45
46 func (m *LogsPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
47- switch ev.(type) {
48+ switch msg := ev.(type) {
49 case vaxis.Key:
50- m.filterLogs()
51- return vxfw.RedrawCmd{}, nil
52+ if msg.Matches(vaxis.KeyTab) {
53+ return nil, nil
54+ }
55+ if m.focus == "input" {
56+ m.filterLogs()
57+ return vxfw.RedrawCmd{}, nil
58+ }
59 }
60 return nil, nil
61 }
62@@ -96,9 +110,35 @@ func (m *LogsPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Comm
63 go func() {
64 _ = m.connectToLogs()
65 }()
66+ m.focus = "input"
67 return m.input.FocusIn()
68 case PageOut:
69 m.done()
70+ case vaxis.Key:
71+ if msg.Matches(vaxis.KeyTab) {
72+ if m.focus == "input" {
73+ m.focus = "list"
74+ m.filterLogs()
75+ cmd, _ := m.input.FocusOut()
76+ return vxfw.BatchCmd([]vxfw.Command{
77+ vxfw.FocusWidgetCmd(m.list),
78+ cmd,
79+ }), nil
80+ }
81+ m.focus = "input"
82+ cmd, _ := m.input.FocusIn()
83+ return vxfw.BatchCmd([]vxfw.Command{
84+ cmd,
85+ vxfw.RedrawCmd{},
86+ }), nil
87+ }
88+ if msg.Matches('G') {
89+ // Scroll to bottom
90+ if len(m.filtered) > 0 {
91+ m.list.SetCursor(uint(len(m.filtered) - 1))
92+ return vxfw.RedrawCmd{}, nil
93+ }
94+ }
95 case LogLineLoaded:
96 ll := NewLogLine(msg.Line)
97 m.logs = append(m.logs, ll)
98@@ -116,7 +156,10 @@ func (m *LogsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
99 txtSurf, _ := txt.Draw(ctx)
100 root.AddChild(0, 0, txtSurf)
101 } else {
102- listSurf, _ := m.list.Draw(createDrawCtx(ctx, ctx.Max.Height-4))
103+ listPane := NewBorder(m.list)
104+ listPane.Label = "logs"
105+ m.focusBorder(listPane)
106+ listSurf, _ := listPane.Draw(createDrawCtx(ctx, ctx.Max.Height-4))
107 root.AddChild(0, 0, listSurf)
108 }
109
110@@ -126,6 +169,14 @@ func (m *LogsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
111 return root, nil
112 }
113
114+func (m *LogsPage) focusBorder(border *Border) {
115+ if m.focus == "list" {
116+ border.Style = vaxis.Style{Foreground: oj}
117+ } else {
118+ border.Style = vaxis.Style{Foreground: purp}
119+ }
120+}
121+
122 func (m *LogsPage) getWidget(i uint, cursor uint) vxfw.Widget {
123 if len(m.filtered) == 0 {
124 return nil