summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/tui/dashboard/model.go2
-rw-r--r--internal/tui/eventstream/model.go163
-rw-r--r--internal/tui/eventstream/model_test.go203
-rw-r--r--internal/tui/tui.go4
4 files changed, 47 insertions, 325 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 5c3f690..5b13642 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -522,7 +522,7 @@ func (m Model) ActiveTab() Tab {
// top-level shortcut for the given key press.
func (m Model) BlocksGlobalShortcuts(msg tea.KeyPressMsg) bool {
if m.activeTab == TabStream {
- return m.streamModel.FilterModalVisible() || m.streamModel.ExportModalVisible() || m.streamModel.SearchModalVisible()
+ return m.streamModel.ExportModalVisible() || m.streamModel.SearchModalVisible()
}
if m.activeTab == TabFlame {
return m.flamegraphModel.ConsumesKey(msg)
diff --git a/internal/tui/eventstream/model.go b/internal/tui/eventstream/model.go
index 3dc774b..467c137 100644
--- a/internal/tui/eventstream/model.go
+++ b/internal/tui/eventstream/model.go
@@ -36,31 +36,25 @@ type Model struct {
allEvents []StreamEvent
filtered []StreamEvent
- filter Filter
- filterModal FilterModal
+ filter Filter
paused bool
- // pauseBeforeFilter keeps the pre-modal pause state so opening the filter
- // can pause refresh temporarily without losing the user's prior state.
- pauseBeforeFilter bool
-
- scrollOffset int
- autoScroll bool
- selectedIdx int
- selectedCol int
- fdTraceView fdTraceViewState
- filterStack []Filter
- filterActionStack []string
- exportModal ExportModal
- searchModal SearchModal
- searchPattern string
- searchRegex *regexp.Regexp
- searchDirection SearchDirection
- lastExportPath string
- pendingOpenPath string
- statusMessage string
- exportDir string
- isDark bool
+
+ scrollOffset int
+ autoScroll bool
+ selectedIdx int
+ selectedCol int
+ fdTraceView fdTraceViewState
+ exportModal ExportModal
+ searchModal SearchModal
+ searchPattern string
+ searchRegex *regexp.Regexp
+ searchDirection SearchDirection
+ lastExportPath string
+ pendingOpenPath string
+ statusMessage string
+ exportDir string
+ isDark bool
width int
height int
@@ -80,7 +74,6 @@ type fdTraceViewState struct {
func NewModel(source Source) Model {
m := Model{
source: source,
- filterModal: NewFilterModal(),
exportModal: NewExportModal(),
searchModal: NewSearchModal(),
autoScroll: true,
@@ -145,14 +138,13 @@ func (m *Model) SetFilter(filter Filter) {
// 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()
+ return false
}
// ExportModalVisible reports whether the stream export modal is currently open.
@@ -202,21 +194,6 @@ func (m *Model) HandleKey(keyStr string) bool {
m.statusMessage = "Exported: " + path
return true
}
- if m.filterModal.Visible() {
- wasVisible := m.filterModal.Visible()
- m.filterModal = m.filterModal.Update(keyMsgFromString(keyStr))
- if wasVisible && !m.filterModal.Visible() {
- m.filter = m.filterModal.Filter()
- m.filterStack = nil
- m.filterActionStack = nil
- m.paused = m.pauseBeforeFilter
- m.applyFilter()
- if !m.paused {
- m.Refresh()
- }
- }
- return true
- }
if m.fdTraceView.visible {
switch keyStr {
case "enter", " ", "space":
@@ -260,11 +237,6 @@ func (m *Model) HandleKey(keyStr string) bool {
case "?":
m.openSearch(SearchBackward)
return true
- case "enter":
- if m.paused {
- return m.applyFilterFromSelectedCell()
- }
- return false
case "n":
if m.searchRegex == nil {
return false
@@ -320,11 +292,6 @@ func (m *Model) HandleKey(keyStr string) bool {
m.centerSelection()
}
return true
- case "f", "F":
- m.pauseBeforeFilter = m.paused
- m.paused = true
- m.filterModal = m.filterModal.Open(m.filter)
- return true
case "G":
if m.paused {
m.moveSelectionTo(len(m.filtered) - 1)
@@ -343,12 +310,6 @@ func (m *Model) HandleKey(keyStr string) bool {
m.scrollOffset = 0
}
return true
- case "c":
- m.filter = Filter{}
- m.filterStack = nil
- m.filterActionStack = nil
- m.applyFilter()
- return true
case "j", "down":
if m.paused {
m.moveSelectionBy(1)
@@ -389,11 +350,6 @@ func (m *Model) HandleKey(keyStr string) bool {
m.handleViewportUpdate(keyMsgFromString("pgup"))
}
return true
- case "esc":
- if m.paused {
- return m.popFilter()
- }
- return false
default:
return false
}
@@ -437,7 +393,7 @@ func (m *Model) HandleTeaKey(msg tea.KeyPressMsg) bool {
}
func (m *Model) handleViewportUpdate(msg tea.KeyPressMsg) bool {
- if m.paused || m.fdTraceView.visible || m.filterModal.Visible() || m.exportModal.Visible() || m.searchModal.Visible() {
+ if m.paused || m.fdTraceView.visible || m.exportModal.Visible() || m.searchModal.Visible() {
return false
}
@@ -504,9 +460,6 @@ func (m *Model) View(width, height int) string {
}
base := RenderStreamTable(width, m.paused, len(m.allEvents), len(m.filtered), bufferLen, ringBufferCapacity, m.filter, visible, selectedVisibleIdx, selectedCol)
if !m.showFooter {
- if m.filterModal.Visible() {
- return m.filterModal.View(width, height)
- }
if m.exportModal.Visible() {
return m.exportModal.View(width, height)
}
@@ -518,21 +471,13 @@ func (m *Model) View(width, height int) string {
status := fmt.Sprintf("Row %d/%d", rowNumber(start, len(m.filtered)), len(m.filtered))
if m.paused && m.selectedIdx >= 0 {
- status = fmt.Sprintf("Row %d/%d | Sel %d/%d Col %d/%d | Filters %d", rowNumber(start, len(m.filtered)), len(m.filtered), rowNumber(m.selectedIdx, len(m.filtered)), len(m.filtered), m.selectedCol+1, streamColumnCount, len(m.filterStack))
+ status = fmt.Sprintf("Row %d/%d | Sel %d/%d Col %d/%d", rowNumber(start, len(m.filtered)), len(m.filtered), rowNumber(m.selectedIdx, len(m.filtered)), len(m.filtered), m.selectedCol+1, streamColumnCount)
}
out := base + "\n" + status
- if len(m.filterActionStack) > 0 {
- out += "\n" + "Stack: " + strings.Join(m.filterActionStack, " | ")
- }
if m.statusMessage != "" {
out += "\n" + m.statusMessage
}
- if m.filterModal.Visible() {
- // While editing filters, show a dedicated modal screen to avoid
- // visual mixing with the live stream table underneath.
- return m.filterModal.View(width, height)
- }
if m.exportModal.Visible() {
return m.exportModal.View(width, height)
}
@@ -754,74 +699,6 @@ func (m *Model) moveSelectedColBy(delta int) {
m.selectedCol = clamp(m.selectedCol+delta, 0, streamColumnCount-1)
}
-func (m *Model) applyFilterFromSelectedCell() bool {
- if m.selectedIdx < 0 || m.selectedIdx >= len(m.filtered) {
- return false
- }
- ev := m.filtered[m.selectedIdx]
- targetSeq := ev.Seq
- next := m.filter.Clone()
- action := ""
-
- switch m.selectedCol {
- case streamColGap:
- next.GapNs = &NumericFilter{Op: OpGte, Value: int64(ev.GapNs)}
- action = fmt.Sprintf("gap>=%dns", ev.GapNs)
- case streamColLatency:
- next.LatencyNs = &NumericFilter{Op: OpGte, Value: int64(ev.DurationNs)}
- action = fmt.Sprintf("latency>=%dns", ev.DurationNs)
- case streamColComm:
- next.Comm = &StringFilter{Pattern: ev.Comm}
- action = fmt.Sprintf("comm~%s", ev.Comm)
- case streamColPID:
- next.PID = &NumericFilter{Op: OpEq, Value: int64(ev.PID)}
- action = fmt.Sprintf("pid=%d", ev.PID)
- case streamColTID:
- next.TID = &NumericFilter{Op: OpEq, Value: int64(ev.TID)}
- action = fmt.Sprintf("tid=%d", ev.TID)
- case streamColSyscall:
- next.Syscall = &StringFilter{Pattern: ev.Syscall}
- action = fmt.Sprintf("syscall~%s", ev.Syscall)
- case streamColFD:
- next.FD = &NumericFilter{Op: OpEq, Value: int64(ev.FD)}
- action = fmt.Sprintf("fd=%d", ev.FD)
- case streamColRet:
- next.RetVal = &NumericFilter{Op: OpEq, Value: ev.RetVal}
- action = fmt.Sprintf("ret=%d", ev.RetVal)
- case streamColBytes:
- next.Bytes = &NumericFilter{Op: OpEq, Value: int64(ev.Bytes)}
- action = fmt.Sprintf("bytes=%d", ev.Bytes)
- case streamColFile:
- next.File = &StringFilter{Pattern: ev.FileName}
- action = fmt.Sprintf("file~%s", ev.FileName)
- default:
- return false
- }
-
- m.filterStack = append(m.filterStack, m.filter.Clone())
- m.filterActionStack = append(m.filterActionStack, action)
- m.filter = next
- m.applyFilter()
- m.restoreSelectionBySeq(targetSeq)
- return true
-}
-
-func (m *Model) popFilter() bool {
- if len(m.filterStack) == 0 {
- return false
- }
- targetSeq := m.currentSelectedSeq()
- last := m.filterStack[len(m.filterStack)-1]
- m.filterStack = m.filterStack[:len(m.filterStack)-1]
- if len(m.filterActionStack) > 0 {
- m.filterActionStack = m.filterActionStack[:len(m.filterActionStack)-1]
- }
- m.filter = last.Clone()
- m.applyFilter()
- m.restoreSelectionBySeq(targetSeq)
- return true
-}
-
func (m *Model) currentSelectedSeq() uint64 {
if m.selectedIdx < 0 || m.selectedIdx >= len(m.filtered) {
return 0
diff --git a/internal/tui/eventstream/model_test.go b/internal/tui/eventstream/model_test.go
index 3925d26..9848042 100644
--- a/internal/tui/eventstream/model_test.go
+++ b/internal/tui/eventstream/model_test.go
@@ -185,65 +185,26 @@ func TestModelHandleKeyRouting(t *testing.T) {
if m.HandleKey("x") {
t.Fatalf("unknown key should not be handled")
}
- if !m.HandleKey("f") {
- t.Fatalf("f should be handled")
- }
- if !m.filterModal.Visible() {
- t.Fatalf("modal should be visible after f")
- }
- if !m.HandleKey("esc") {
- t.Fatalf("esc should route to modal")
- }
- if m.filterModal.Visible() {
- t.Fatalf("modal should close on esc")
+ if m.HandleKey("f") {
+ t.Fatalf("stream-local filter shortcut should no longer be handled here")
}
}
-func TestFilterModalTemporarilyPausesAndRestoresState(t *testing.T) {
+func TestSetFilterReappliesCurrentBufferedRows(t *testing.T) {
rb := NewRingBuffer()
m := NewModel(rb)
m.height = 20
- pushEvents(rb, 4)
+ pushEvents(rb, 6)
m.Refresh()
- if m.paused {
- t.Fatalf("expected model to start unpaused")
- }
- if !m.HandleKey("f") {
- t.Fatalf("f should be handled")
- }
- if !m.paused {
- t.Fatalf("expected model paused while filter modal is open")
- }
- if !m.filterModal.Visible() {
- t.Fatalf("expected filter modal visible after f")
- }
- if !m.HandleKey("esc") {
- t.Fatalf("esc should be routed to filter modal")
- }
- if m.filterModal.Visible() {
- t.Fatalf("expected filter modal closed after esc")
- }
- if m.paused {
- t.Fatalf("expected pause state restored to unpaused after modal close")
+ m.SetFilter(Filter{Syscall: &StringFilter{Pattern: "read"}})
+ if len(m.filtered) != 3 {
+ t.Fatalf("expected 3 matching rows after filter, got %d", len(m.filtered))
}
- // If the user was already paused before opening the filter modal,
- // that pause state should remain after closing.
- if !m.HandleKey("space") {
- t.Fatalf("space should toggle pause")
- }
- if !m.paused {
- t.Fatalf("expected paused=true after space")
- }
- if !m.HandleKey("f") {
- t.Fatalf("f should be handled while paused")
- }
- if !m.HandleKey("esc") {
- t.Fatalf("esc should close modal")
- }
- if !m.paused {
- t.Fatalf("expected paused state preserved after modal close")
+ m.SetFilter(Filter{})
+ if len(m.filtered) != 6 {
+ t.Fatalf("expected clearing filter to restore all rows, got %d", len(m.filtered))
}
}
@@ -416,113 +377,31 @@ func TestPausedSelectionMovesAcrossColumnsWithLeftRightAndHL(t *testing.T) {
}
}
-func TestPausedEnterAddsStackableFiltersAndEscUndoes(t *testing.T) {
- rb := NewRingBuffer()
- rb.Push(StreamEvent{Seq: 1, PID: 10, TID: 101, Comm: "firefox", Syscall: "read", FD: 5, RetVal: 10, Bytes: 10, GapNs: 50, DurationNs: 100, FileName: "/a"})
- rb.Push(StreamEvent{Seq: 2, PID: 10, TID: 102, Comm: "firefox", Syscall: "write", FD: 6, RetVal: 11, Bytes: 11, GapNs: 51, DurationNs: 101, FileName: "/b"})
- rb.Push(StreamEvent{Seq: 3, PID: 11, TID: 201, Comm: "bash", Syscall: "read", FD: 7, RetVal: 12, Bytes: 12, GapNs: 52, DurationNs: 102, FileName: "/c"})
-
- m := NewModel(rb)
- m.height = 20
- m.Refresh()
- if !m.HandleKey("space") {
- t.Fatalf("space should toggle pause")
- }
-
- m.selectedIdx = 0
- m.selectedCol = streamColComm
- if !m.HandleKey("enter") {
- t.Fatalf("enter should add filter from selected comm cell")
- }
- if m.filter.Comm == nil || m.filter.Comm.Pattern != "firefox" {
- t.Fatalf("expected comm filter firefox, got %+v", m.filter.Comm)
- }
- if len(m.filtered) != 2 {
- t.Fatalf("expected 2 rows after comm filter, got %d", len(m.filtered))
- }
- if len(m.filterStack) != 1 {
- t.Fatalf("expected filter stack len 1, got %d", len(m.filterStack))
- }
- if got := m.View(120, 24); !strings.Contains(got, "Stack: comm~firefox") {
- t.Fatalf("expected stack status line with comm filter, got:\n%s", got)
- }
-
- m.selectedIdx = 1
- m.selectedCol = streamColTID
- if !m.HandleKey("enter") {
- t.Fatalf("enter should add filter from selected tid cell")
- }
- if m.filter.TID == nil || m.filter.TID.Value != 102 {
- t.Fatalf("expected tid filter 102, got %+v", m.filter.TID)
- }
- if m.filter.Comm == nil || m.filter.Comm.Pattern != "firefox" {
- t.Fatalf("expected comm filter preserved, got %+v", m.filter.Comm)
- }
- if len(m.filtered) != 1 {
- t.Fatalf("expected 1 row after comm+tid filters, got %d", len(m.filtered))
- }
-
- if !m.HandleKey("esc") {
- t.Fatalf("esc should undo last filter")
- }
- if m.filter.TID != nil {
- t.Fatalf("expected tid filter cleared after undo")
- }
- if m.filter.Comm == nil || m.filter.Comm.Pattern != "firefox" {
- t.Fatalf("expected comm filter kept after first undo")
- }
- if len(m.filtered) != 2 {
- t.Fatalf("expected 2 rows after undo tid filter, got %d", len(m.filtered))
- }
-
- if !m.HandleKey("esc") {
- t.Fatalf("esc should undo previous filter")
- }
- if m.filter.IsActive() {
- t.Fatalf("expected no active filters after second undo, got summary=%q", m.filter.Summary())
- }
- if len(m.filtered) != 3 {
- t.Fatalf("expected all rows after second undo, got %d", len(m.filtered))
- }
- if got := m.View(120, 24); strings.Contains(got, "Stack:") {
- t.Fatalf("did not expect stack status line after all undos, got:\n%s", got)
- }
-}
-
-func TestPausedEnterCanFilterLatencyAndGapColumns(t *testing.T) {
+func TestPausedEnterDoesNotMutateGlobalFilter(t *testing.T) {
rb := NewRingBuffer()
rb.Push(StreamEvent{Seq: 1, PID: 1, TID: 1, Comm: "a", DurationNs: 100, GapNs: 5})
rb.Push(StreamEvent{Seq: 2, PID: 1, TID: 2, Comm: "b", DurationNs: 200, GapNs: 6})
m := NewModel(rb)
m.height = 20
m.Refresh()
- _ = m.HandleKey("space")
+ if !m.HandleKey("space") {
+ t.Fatalf("space should pause")
+ }
m.selectedIdx = 0
m.selectedCol = streamColLatency
- if !m.HandleKey("enter") {
- t.Fatalf("expected enter to filter by latency")
- }
- if m.filter.LatencyNs == nil || m.filter.LatencyNs.Op != OpGte || m.filter.LatencyNs.Value != 100 {
- t.Fatalf("expected latency filter >=100ns, got %+v", m.filter.LatencyNs)
+ if m.HandleKey("enter") {
+ t.Fatalf("expected enter not to mutate filters in paused stream mode")
}
- if len(m.filtered) != 2 {
- t.Fatalf("expected both rows after latency >=100 filter, got %d", len(m.filtered))
- }
-
- m.selectedCol = streamColGap
- if !m.HandleKey("enter") {
- t.Fatalf("expected enter to filter by gap")
+ if m.filter.IsActive() {
+ t.Fatalf("expected enter to leave global filter unchanged")
}
- if m.filter.GapNs == nil || m.filter.GapNs.Op != OpGte || m.filter.GapNs.Value != 5 {
- t.Fatalf("expected gap filter >=5ns, got %+v", m.filter.GapNs)
- }
- if len(m.filtered) != 2 {
- t.Fatalf("expected both rows after gap >=5 filter, got %d", len(m.filtered))
+ if m.HandleKey("esc") {
+ t.Fatalf("expected esc not to act as local filter undo anymore")
}
}
-func TestPausedEnterKeepsSelectedRowCenteredAfterFilter(t *testing.T) {
+func TestSetFilterKeepsPausedSelectionCentered(t *testing.T) {
rb := NewRingBuffer()
for i := 0; i < 300; i++ {
comm := "other"
@@ -549,40 +428,10 @@ func TestPausedEnterKeepsSelectedRowCenteredAfterFilter(t *testing.T) {
t.Fatalf("expected initial selected row near middle, got relative idx %d", before)
}
- m.selectedCol = streamColComm
- if !m.HandleKey("enter") {
- t.Fatalf("expected enter to apply filter")
- }
+ m.SetFilter(Filter{Comm: &StringFilter{Pattern: "match"}})
after := m.selectedIdx - m.scrollOffset
if after < 4 || after > 8 {
- t.Fatalf("expected selected row to stay near middle after filter, got relative idx %d", after)
- }
-}
-
-func TestPausedEnterCanFilterByFDColumn(t *testing.T) {
- rb := NewRingBuffer()
- rb.Push(StreamEvent{Seq: 1, PID: 10, TID: 101, FD: 3, Syscall: "read", FileName: "/a"})
- rb.Push(StreamEvent{Seq: 2, PID: 10, TID: 102, FD: 3, Syscall: "write", FileName: "/a"})
- rb.Push(StreamEvent{Seq: 3, PID: 10, TID: 103, FD: 4, Syscall: "read", FileName: "/b"})
- rb.Push(StreamEvent{Seq: 4, PID: 11, TID: 104, FD: 3, Syscall: "read", FileName: "/c"})
-
- m := NewModel(rb)
- m.height = 20
- m.Refresh()
- if !m.HandleKey("space") {
- t.Fatalf("space should pause")
- }
-
- m.selectedIdx = 0
- m.selectedCol = streamColFD
- if !m.HandleKey("enter") {
- t.Fatalf("enter should add fd filter")
- }
- if m.filter.FD == nil || m.filter.FD.Value != 3 {
- t.Fatalf("expected fd filter 3, got %+v", m.filter.FD)
- }
- if len(m.filtered) != 3 {
- t.Fatalf("expected 3 rows with fd=3, got %d", len(m.filtered))
+ t.Fatalf("expected selected row to stay near middle after global refilter, got relative idx %d", after)
}
}
@@ -600,11 +449,7 @@ func TestPausedQuickExportWritesFilteredRows(t *testing.T) {
t.Fatalf("space should pause")
}
- m.selectedIdx = 0
- m.selectedCol = streamColComm
- if !m.HandleKey("enter") {
- t.Fatalf("enter should apply comm filter")
- }
+ m.SetFilter(Filter{Comm: &StringFilter{Pattern: "firefox"}})
if len(m.filtered) != 2 {
t.Fatalf("expected 2 filtered rows before export, got %d", len(m.filtered))
}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index a1dd8fb..6972e98 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -1091,8 +1091,8 @@ func (m Model) helpSections() []helpSection {
"files: d dirs toggle v bubbles (dirs only) b metric",
"flame: arrows/hjkl nav enter/click zoom click ancestor undo u/bs/esc undo o order",
"flame: / filter n/N match next/prev space/p pause b metric",
- "stream: space pause f filter enter apply esc undo /? n/N",
- "stream: j/k/pg scroll g/G top/tail h/l cols c x/X E open",
+ "stream: space pause /? n/N search",
+ "stream: j/k/pg scroll g/G top/tail h/l cols x/X export E open",
},
},
{