diff options
Diffstat (limited to 'internal/tui/eventstream/model.go')
| -rw-r--r-- | internal/tui/eventstream/model.go | 132 |
1 files changed, 114 insertions, 18 deletions
diff --git a/internal/tui/eventstream/model.go b/internal/tui/eventstream/model.go index 467c137..600d40d 100644 --- a/internal/tui/eventstream/model.go +++ b/internal/tui/eventstream/model.go @@ -36,25 +36,30 @@ type Model struct { allEvents []StreamEvent filtered []StreamEvent - filter Filter + filter Filter + filterStack []string paused 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 + 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 + pendingFilter Filter + pendingFilterAction string + hasPendingFilter bool + pendingUndo bool + statusMessage string + exportDir string + isDark bool width int height int @@ -135,6 +140,11 @@ func (m *Model) SetFilter(filter Filter) { m.restoreSelectionBySeq(targetSeq) } +// SetFilterStack updates the visible shared filter stack summary. +func (m *Model) SetFilterStack(stack []string) { + m.filterStack = append(m.filterStack[:0], stack...) +} + // SetDarkMode updates stream modal text input styles for the active theme. func (m *Model) SetDarkMode(isDark bool) { m.isDark = isDark @@ -231,6 +241,23 @@ func (m *Model) HandleKey(keyStr string) bool { } switch keyStr { + case "enter": + if m.paused { + return m.requestGlobalFilterFromSelectedCell() + } + return false + case "F": + if len(m.filterStack) == 0 { + return false + } + m.pendingUndo = true + return true + case "esc": + if m.paused && len(m.filterStack) > 0 { + m.pendingUndo = true + return true + } + return false case "/": m.openSearch(SearchForward) return true @@ -458,7 +485,7 @@ func (m *Model) View(width, height int) string { if m.paused && selectedVisibleIdx >= 0 { selectedCol = m.selectedCol } - base := RenderStreamTable(width, m.paused, len(m.allEvents), len(m.filtered), bufferLen, ringBufferCapacity, m.filter, visible, selectedVisibleIdx, selectedCol) + base := RenderStreamTable(width, m.paused, len(m.allEvents), len(m.filtered), bufferLen, ringBufferCapacity, m.filter, m.filterStack, visible, selectedVisibleIdx, selectedCol) if !m.showFooter { if m.exportModal.Visible() { return m.exportModal.View(width, height) @@ -471,7 +498,7 @@ 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", rowNumber(start, len(m.filtered)), len(m.filtered), rowNumber(m.selectedIdx, len(m.filtered)), len(m.filtered), m.selectedCol+1, streamColumnCount) + status = fmt.Sprintf("Row %d/%d | Sel %d/%d Col %d/%d | Enter push-filter | Esc/F undo", 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 m.statusMessage != "" { @@ -699,6 +726,53 @@ func (m *Model) moveSelectedColBy(delta int) { m.selectedCol = clamp(m.selectedCol+delta, 0, streamColumnCount-1) } +func (m *Model) requestGlobalFilterFromSelectedCell() bool { + if m.fdTraceView.visible || m.selectedIdx < 0 || m.selectedIdx >= len(m.filtered) { + return false + } + ev := m.filtered[m.selectedIdx] + next := m.filter.Clone() + + switch m.selectedCol { + case streamColGap: + next.GapNs = &NumericFilter{Op: OpGte, Value: int64(ev.GapNs)} + m.pendingFilterAction = fmt.Sprintf("gap>=%s", formatDurationNs(ev.GapNs)) + case streamColLatency: + next.LatencyNs = &NumericFilter{Op: OpGte, Value: int64(ev.DurationNs)} + m.pendingFilterAction = fmt.Sprintf("latency>=%s", formatDurationNs(ev.DurationNs)) + case streamColComm: + next.Comm = &StringFilter{Pattern: ev.Comm} + m.pendingFilterAction = "comm~" + ev.Comm + case streamColPID: + next.PID = &NumericFilter{Op: OpEq, Value: int64(ev.PID)} + m.pendingFilterAction = fmt.Sprintf("pid=%d", ev.PID) + case streamColTID: + next.TID = &NumericFilter{Op: OpEq, Value: int64(ev.TID)} + m.pendingFilterAction = fmt.Sprintf("tid=%d", ev.TID) + case streamColSyscall: + next.Syscall = &StringFilter{Pattern: ev.Syscall} + m.pendingFilterAction = "syscall~" + ev.Syscall + case streamColFD: + next.FD = &NumericFilter{Op: OpEq, Value: int64(ev.FD)} + m.pendingFilterAction = fmt.Sprintf("fd=%d", ev.FD) + case streamColRet: + next.RetVal = &NumericFilter{Op: OpEq, Value: ev.RetVal} + m.pendingFilterAction = fmt.Sprintf("ret=%d", ev.RetVal) + case streamColBytes: + next.Bytes = &NumericFilter{Op: OpEq, Value: int64(ev.Bytes)} + m.pendingFilterAction = fmt.Sprintf("bytes=%d", ev.Bytes) + case streamColFile: + next.File = &StringFilter{Pattern: ev.FileName} + m.pendingFilterAction = "file~" + ev.FileName + default: + return false + } + + m.pendingFilter = next + m.hasPendingFilter = true + return true +} + func (m *Model) currentSelectedSeq() uint64 { if m.selectedIdx < 0 || m.selectedIdx >= len(m.filtered) { return 0 @@ -790,6 +864,28 @@ func (m *Model) ConsumeOpenEditorRequest() (string, bool) { return path, true } +// ConsumeGlobalFilterRequest returns the pending global-filter request once. +func (m *Model) ConsumeGlobalFilterRequest() (Filter, string, bool) { + if !m.hasPendingFilter { + return Filter{}, "", false + } + filter := m.pendingFilter.Clone() + action := m.pendingFilterAction + m.pendingFilter = Filter{} + m.pendingFilterAction = "" + m.hasPendingFilter = false + return filter, action, true +} + +// ConsumeGlobalFilterUndoRequest returns the pending undo request once. +func (m *Model) ConsumeGlobalFilterUndoRequest() bool { + if !m.pendingUndo { + return false + } + m.pendingUndo = false + return true +} + // SetStatusMessage updates the stream footer status line. func (m *Model) SetStatusMessage(message string) { m.statusMessage = message |
