diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
| commit | 1561987330cb898f5ff64383a9c78e7e6559f118 (patch) | |
| tree | 69a823e8f98dce572566c97e6879c11c9d591bda /internal/tui/eventstream/model.go | |
| parent | 96225fb6159212a8851043a08d781aba721b4e78 (diff) | |
| parent | 110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff) | |
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/tui/eventstream/model.go')
| -rw-r--r-- | internal/tui/eventstream/model.go | 164 |
1 files changed, 116 insertions, 48 deletions
diff --git a/internal/tui/eventstream/model.go b/internal/tui/eventstream/model.go index d9c4ee3..12aff4d 100644 --- a/internal/tui/eventstream/model.go +++ b/internal/tui/eventstream/model.go @@ -6,7 +6,8 @@ import ( "strconv" "strings" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" ) const ( @@ -23,8 +24,14 @@ const ( streamColumnCount ) +// Source is the minimal stream buffer contract needed by the stream model. +type Source interface { + Len() int + Snapshot() []StreamEvent +} + type Model struct { - source *RingBuffer + source Source allEvents []StreamEvent filtered []StreamEvent @@ -53,11 +60,13 @@ type Model struct { pendingOpenPath string statusMessage string exportDir string + isDark bool width int height int showFooter bool + viewport viewport.Model } type fdTraceViewState struct { @@ -68,8 +77,8 @@ type fdTraceViewState struct { offset int } -func NewModel(source *RingBuffer) Model { - return Model{ +func NewModel(source Source) Model { + m := Model{ source: source, filterModal: NewFilterModal(), exportModal: NewExportModal(), @@ -79,7 +88,25 @@ func NewModel(source *RingBuffer) Model { selectedCol: 0, exportDir: ".", showFooter: true, + isDark: true, + viewport: newStreamViewport(), } + m.SetDarkMode(true) + return m +} + +func newStreamViewport() viewport.Model { + vp := viewport.New() + keyMap := viewport.DefaultKeyMap() + keyMap.Down.SetKeys("down", "j") + keyMap.Up.SetKeys("up", "k") + keyMap.Left.SetKeys("left", "h") + keyMap.Right.SetKeys("right", "l") + keyMap.PageDown.SetKeys("pgdown", "pgdn", "pagedown") + keyMap.PageUp.SetKeys("pgup", "pageup") + vp.KeyMap = keyMap + vp.SoftWrap = true + return vp } // SetViewport updates the render/scroll viewport dimensions used for @@ -87,9 +114,11 @@ func NewModel(source *RingBuffer) Model { func (m *Model) SetViewport(width, height int) { if width > 0 { m.width = width + m.viewport.SetWidth(width) } if height > 0 { m.height = height + m.viewport.SetHeight(m.visibleRows()) } } @@ -99,11 +128,19 @@ func (m *Model) SetFooterVisible(visible bool) { } // SetSource updates the backing ring buffer and refreshes visible rows. -func (m *Model) SetSource(source *RingBuffer) { +func (m *Model) SetSource(source Source) { m.source = source m.Refresh() } +// SetDarkMode updates stream modal text input styles for the active theme. +func (m *Model) SetDarkMode(isDark bool) { + m.isDark = isDark + m.filterModal = m.filterModal.SetDarkMode(isDark) + m.exportModal = m.exportModal.SetDarkMode(isDark) + m.searchModal = m.searchModal.SetDarkMode(isDark) +} + // FilterModalVisible reports whether the filter modal is currently open. func (m Model) FilterModalVisible() bool { return m.filterModal.Visible() @@ -284,7 +321,8 @@ func (m *Model) HandleKey(keyStr string) bool { m.moveSelectionTo(len(m.filtered) - 1) } else { m.autoScroll = true - m.scrollOffset = m.maxScrollOffset() + m.viewport.GotoBottom() + m.scrollOffset = clamp(m.viewport.YOffset(), 0, m.maxScrollOffset()) } return true case "g": @@ -292,6 +330,7 @@ func (m *Model) HandleKey(keyStr string) bool { m.moveSelectionTo(0) } else { m.autoScroll = false + m.viewport.GotoTop() m.scrollOffset = 0 } return true @@ -305,14 +344,14 @@ func (m *Model) HandleKey(keyStr string) bool { if m.paused { m.moveSelectionBy(1) } else { - m.scrollByLines(1) + m.handleViewportUpdate(keyMsgFromString("down")) } return true case "k", "up": if m.paused { m.moveSelectionBy(-1) } else { - m.scrollByLines(-1) + m.handleViewportUpdate(keyMsgFromString("up")) } return true case "left", "h": @@ -320,25 +359,25 @@ func (m *Model) HandleKey(keyStr string) bool { m.moveSelectedColBy(-1) return true } - return false + return m.handleViewportUpdate(keyMsgFromString("left")) case "right", "l": if m.paused { m.moveSelectedColBy(1) return true } - return false + return m.handleViewportUpdate(keyMsgFromString("right")) case "pgdown", "pgdn", "pagedown": if m.paused { m.moveSelectionBy(m.pageStep()) } else { - m.scrollByLines(m.pageStep()) + m.handleViewportUpdate(keyMsgFromString("pgdown")) } return true case "pgup", "pageup": if m.paused { m.moveSelectionBy(-m.pageStep()) } else { - m.scrollByLines(-m.pageStep()) + m.handleViewportUpdate(keyMsgFromString("pgup")) } return true case "esc": @@ -353,8 +392,12 @@ func (m *Model) HandleKey(keyStr string) bool { // HandleTeaKey handles stream keys based on Bubble Tea key message types first, // then falls back to string matching for rune-driven shortcuts. -func (m *Model) HandleTeaKey(msg tea.KeyMsg) bool { - switch msg.Type { +func (m *Model) HandleTeaKey(msg tea.KeyPressMsg) bool { + if m.handleViewportUpdate(msg) { + return true + } + + switch msg.Code { case tea.KeyLeft: return m.HandleKey("left") case tea.KeyRight: @@ -373,14 +416,45 @@ func (m *Model) HandleTeaKey(msg tea.KeyMsg) bool { return m.HandleKey("esc") case tea.KeyEnter: return m.HandleKey("enter") - case tea.KeyRunes: - if len(msg.Runes) == 1 { - return m.HandleKey(string(msg.Runes[0])) + default: + if msg.Text != "" { + runes := []rune(msg.Text) + if len(runes) == 1 { + return m.HandleKey(msg.Text) + } } } return m.HandleKey(msg.String()) } +func (m *Model) handleViewportUpdate(msg tea.KeyPressMsg) bool { + if m.paused || m.fdTraceView.visible || m.filterModal.Visible() || m.exportModal.Visible() || m.searchModal.Visible() { + return false + } + + switch msg.String() { + case "down", "j", "up", "k", "left", "h", "right", "l", "pgup", "pageup", "pgdown", "pgdn", "pagedown": + default: + return false + } + + switch msg.String() { + case "pgup", "pageup": + m.viewport.ScrollUp(m.pageStep()) + case "pgdown", "pgdn", "pagedown": + m.viewport.ScrollDown(m.pageStep()) + default: + vp, cmd := m.viewport.Update(msg) + _ = cmd + m.viewport = vp + } + m.scrollOffset = clamp(m.viewport.YOffset(), 0, m.maxScrollOffset()) + if m.scrollOffset < m.maxScrollOffset() { + m.autoScroll = false + } + return true +} + func (m *Model) View(width, height int) string { if width <= 0 { width = 100 @@ -390,13 +464,16 @@ func (m *Model) View(width, height int) string { } m.width = width m.height = height + m.viewport.SetWidth(width) + m.viewport.SetHeight(m.visibleRows()) if m.fdTraceView.visible { return m.viewFDTrace(width) } rows := m.visibleRows() - start := clamp(m.scrollOffset, 0, m.maxScrollOffset()) + start := clamp(m.viewport.YOffset(), 0, m.maxScrollOffset()) + m.scrollOffset = start end := start + rows if end > len(m.filtered) { end = len(m.filtered) @@ -464,6 +541,8 @@ func (m *Model) Refresh() { m.allEvents = []StreamEvent{} m.filtered = []StreamEvent{} m.scrollOffset = 0 + m.viewport.SetContentLines(nil) + m.viewport.SetYOffset(0) return } @@ -476,6 +555,8 @@ func (m *Model) applyFilter() { m.filtered = []StreamEvent{} m.scrollOffset = 0 m.selectedIdx = -1 + m.viewport.SetContentLines(nil) + m.viewport.SetYOffset(0) return } @@ -487,12 +568,18 @@ func (m *Model) applyFilter() { } } m.filtered = filtered + m.viewport.SetWidth(m.width) + m.viewport.SetHeight(m.visibleRows()) + lines := make([]string, len(m.filtered)) + m.viewport.SetContentLines(lines) max := m.maxScrollOffset() if m.autoScroll { - m.scrollOffset = max + m.viewport.GotoBottom() + m.scrollOffset = clamp(m.viewport.YOffset(), 0, max) } else { m.scrollOffset = clamp(m.scrollOffset, 0, max) + m.viewport.SetYOffset(m.scrollOffset) } m.clampSelection() if m.paused { @@ -529,26 +616,6 @@ func (m *Model) pageStep() int { return rows - 1 } -func (m *Model) scrollByLines(delta int) { - if delta == 0 { - return - } - max := m.maxScrollOffset() - next := m.scrollOffset + delta - if next < 0 { - next = 0 - } - if next > max { - next = max - } - if next != m.scrollOffset { - m.scrollOffset = next - } - if m.scrollOffset < max { - m.autoScroll = false - } -} - func (m *Model) openFDTraceView() bool { if m.fdTraceView.visible || m.selectedIdx < 0 || m.selectedIdx >= len(m.filtered) { return false @@ -646,6 +713,7 @@ func (m *Model) centerSelection() { mid := m.visibleRows() / 2 target := m.selectedIdx - mid m.scrollOffset = clamp(target, 0, m.maxScrollOffset()) + m.viewport.SetYOffset(m.scrollOffset) } func (m *Model) ensureSelection() { @@ -807,26 +875,26 @@ func (m *Model) clampSelection() { m.selectedIdx = clamp(m.selectedIdx, 0, len(m.filtered)-1) } -func keyMsgFromString(keyStr string) tea.KeyMsg { +func keyMsgFromString(keyStr string) tea.KeyPressMsg { switch keyStr { case "esc": - return tea.KeyMsg{Type: tea.KeyEsc} + return tea.KeyPressMsg{Code: tea.KeyEsc} case "enter": - return tea.KeyMsg{Type: tea.KeyEnter} + return tea.KeyPressMsg{Code: tea.KeyEnter} case "tab": - return tea.KeyMsg{Type: tea.KeyTab} + return tea.KeyPressMsg{Code: tea.KeyTab} case "up": - return tea.KeyMsg{Type: tea.KeyUp} + return tea.KeyPressMsg{Code: tea.KeyUp} case "down": - return tea.KeyMsg{Type: tea.KeyDown} + return tea.KeyPressMsg{Code: tea.KeyDown} case " ", "space": - return tea.KeyMsg{Type: tea.KeySpace} + return tea.KeyPressMsg{Code: tea.KeySpace, Text: " "} } if keyStr == "" { - return tea.KeyMsg{} + return tea.KeyPressMsg{} } runes := []rune(keyStr) - return tea.KeyMsg{Type: tea.KeyRunes, Runes: runes} + return tea.KeyPressMsg{Code: runes[0], Text: keyStr} } func rowNumber(start, total int) int { |
