summaryrefslogtreecommitdiff
path: root/internal/tui/eventstream/model.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/eventstream/model.go')
-rw-r--r--internal/tui/eventstream/model.go164
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 {