repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

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
M pkg/tui/input.go
+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 }
M pkg/tui/logs.go
+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