summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-13 09:45:09 +0300
committerPaul Buetow <paul@buetow.org>2026-05-13 09:45:09 +0300
commit6c7a5d5fb3e88068799fb414e316b6bec31015e9 (patch)
treec47a73f3fd3d3a1089f77e00934bcdb45168965c /internal
parent50d5220ed5a2187dbf548d70f3d795a39f7bfd00 (diff)
split globalfilter presentation and parsing into sub-packages
Move ParseDurationNs to globalfilter/parser and move CompareOpSymbol, AppendStringSummary, AppendNumericSummary, FilterSummary to globalfilter/presenter. The domain Filter type retains only matching, equality, clone, and active-predicate logic. All callers updated; tests for the new sub-packages added. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/globalfilter/filter.go105
-rw-r--r--internal/globalfilter/filter_presenter_test.go75
-rw-r--r--internal/globalfilter/filter_test.go60
-rw-r--r--internal/globalfilter/parser/parser.go49
-rw-r--r--internal/globalfilter/parser/parser_test.go37
-rw-r--r--internal/globalfilter/presenter/presenter.go85
-rw-r--r--internal/globalfilter/presenter/presenter_test.go83
-rw-r--r--internal/tui/dashboard/model.go3
-rw-r--r--internal/tui/eventstream/filter.go9
-rw-r--r--internal/tui/eventstream/filter_test.go8
-rw-r--r--internal/tui/eventstream/render.go3
-rw-r--r--internal/tui/filterstack.go11
-rw-r--r--internal/tui/tracefilter/model.go3
13 files changed, 353 insertions, 178 deletions
diff --git a/internal/globalfilter/filter.go b/internal/globalfilter/filter.go
index ab3b0fb..01213aa 100644
--- a/internal/globalfilter/filter.go
+++ b/internal/globalfilter/filter.go
@@ -1,10 +1,7 @@
package globalfilter
import (
- "fmt"
- "strconv"
"strings"
- "time"
)
type CompareOp int
@@ -176,77 +173,6 @@ func (f Filter) IsActive() bool {
return false
}
-// Summary returns a compact human-readable description of the active filter
-// predicates, e.g. "syscall~read pid=1234". Returns "all" when no predicates
-// are set.
-func (f Filter) Summary() string {
- parts := make([]string, 0, 10)
- if f.ErrorsOnly {
- parts = append(parts, "errors")
- }
- parts = AppendStringSummary(parts, "syscall", f.Syscall)
- parts = AppendStringSummary(parts, "comm", f.Comm)
- parts = AppendStringSummary(parts, "file", f.File)
- parts = AppendNumericSummary(parts, "pid", f.PID, false)
- parts = AppendNumericSummary(parts, "tid", f.TID, false)
- parts = AppendNumericSummary(parts, "fd", f.FD, false)
- parts = AppendNumericSummary(parts, "latency", f.LatencyNs, true)
- parts = AppendNumericSummary(parts, "gap", f.GapNs, true)
- parts = AppendNumericSummary(parts, "bytes", f.Bytes, false)
- parts = AppendNumericSummary(parts, "ret", f.RetVal, false)
- if len(parts) == 0 {
- return "all"
- }
- return strings.Join(parts, " ")
-}
-
-func ParseDurationNs(input string) (int64, error) {
- s := strings.TrimSpace(strings.ToLower(input))
- if s == "" {
- return 0, fmt.Errorf("empty duration")
- }
- s = strings.ReplaceAll(s, "µs", "us")
- s = strings.ReplaceAll(s, "μs", "us")
- if onlyDigits(s) || strings.HasPrefix(s, "-") && onlyDigits(s[1:]) {
- v, err := strconv.ParseInt(s, 10, 64)
- if err != nil {
- return 0, err
- }
- return v, nil
- }
- d, err := time.ParseDuration(s)
- if err != nil {
- return 0, err
- }
- return d.Nanoseconds(), nil
-}
-
-// AppendStringSummary appends a "name~pattern" token to parts when the filter
-// is non-nil and non-empty, then returns the updated slice.
-func AppendStringSummary(parts []string, name string, sf *StringFilter) []string {
- if sf == nil {
- return parts
- }
- pattern := strings.TrimSpace(sf.Pattern)
- if pattern == "" {
- return parts
- }
- return append(parts, fmt.Sprintf("%s~%s", name, pattern))
-}
-
-// AppendNumericSummary appends a "nameOPvalue" token to parts when the filter
-// is non-nil, formatting the value as a duration string when duration is true.
-func AppendNumericSummary(parts []string, name string, nf *NumericFilter, duration bool) []string {
- if nf == nil {
- return parts
- }
- value := strconv.FormatInt(nf.Value, 10)
- if duration {
- value = time.Duration(nf.Value).String()
- }
- return append(parts, fmt.Sprintf("%s%s%s", name, CompareOpSymbol(nf.Op), value))
-}
-
func matchString(sf *StringFilter, value string) bool {
if sf == nil {
return true
@@ -298,26 +224,6 @@ func matchNumeric(nf *NumericFilter, value int64) bool {
}
}
-// CompareOpSymbol returns the summary/render symbol for a numeric comparison operator.
-func CompareOpSymbol(op CompareOp) string {
- switch op {
- case OpEq:
- return "="
- case OpNeq:
- return "!="
- case OpGt:
- return ">"
- case OpGte:
- return ">="
- case OpLt:
- return "<"
- case OpLte:
- return "<="
- default:
- return "?"
- }
-}
-
func cloneFilter[T any](in *T) *T {
if in == nil {
return nil
@@ -337,14 +243,3 @@ func sameFilter[T comparable](left, right *T) bool {
}
}
-func onlyDigits(s string) bool {
- if s == "" {
- return false
- }
- for _, ch := range s {
- if ch < '0' || ch > '9' {
- return false
- }
- }
- return true
-}
diff --git a/internal/globalfilter/filter_presenter_test.go b/internal/globalfilter/filter_presenter_test.go
new file mode 100644
index 0000000..7fb5c17
--- /dev/null
+++ b/internal/globalfilter/filter_presenter_test.go
@@ -0,0 +1,75 @@
+// External test package so that presenter and parser sub-packages can be
+// imported without creating an import cycle with globalfilter itself.
+package globalfilter_test
+
+import (
+ "strings"
+ "testing"
+
+ "ior/internal/globalfilter"
+ "ior/internal/globalfilter/parser"
+ "ior/internal/globalfilter/presenter"
+)
+
+func TestFilterSummaryViaPresenter(t *testing.T) {
+ filter := globalfilter.Filter{
+ ErrorsOnly: true,
+ Syscall: &globalfilter.StringFilter{Pattern: "read"},
+ PID: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 1234},
+ LatencyNs: &globalfilter.NumericFilter{Op: globalfilter.OpGt, Value: 1_000_000},
+ }
+ got := presenter.FilterSummary(filter)
+ for _, want := range []string{"errors", "syscall~read", "pid=1234", "latency>1ms"} {
+ if !strings.Contains(got, want) {
+ t.Fatalf("FilterSummary() = %q, missing %q", got, want)
+ }
+ }
+
+ // Zero-value filter should produce "all".
+ if got := presenter.FilterSummary(globalfilter.Filter{}); got != "all" {
+ t.Fatalf("FilterSummary(zero) = %q, want \"all\"", got)
+ }
+}
+
+func TestParseDurationNsViaParser(t *testing.T) {
+ for _, tc := range []struct {
+ in string
+ want int64
+ }{
+ {in: "1ms", want: 1_000_000},
+ {in: "10us", want: 10_000},
+ {in: "500ns", want: 500},
+ {in: "42", want: 42},
+ } {
+ gotNs, err := parser.ParseDurationNs(tc.in)
+ if err != nil {
+ t.Fatalf("ParseDurationNs(%q) err = %v", tc.in, err)
+ }
+ if gotNs != tc.want {
+ t.Fatalf("ParseDurationNs(%q) = %d, want %d", tc.in, gotNs, tc.want)
+ }
+ }
+ if _, err := parser.ParseDurationNs("garbage"); err == nil {
+ t.Fatalf("ParseDurationNs(garbage) expected error")
+ }
+}
+
+func TestCompareOpSymbolViaPresenter(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ op globalfilter.CompareOp
+ want string
+ }{
+ {name: "eq", op: globalfilter.OpEq, want: "="},
+ {name: "neq", op: globalfilter.OpNeq, want: "!="},
+ {name: "gt", op: globalfilter.OpGt, want: ">"},
+ {name: "gte", op: globalfilter.OpGte, want: ">="},
+ {name: "lt", op: globalfilter.OpLt, want: "<"},
+ {name: "lte", op: globalfilter.OpLte, want: "<="},
+ {name: "unknown", op: globalfilter.CompareOp(99), want: "?"},
+ } {
+ if got := presenter.CompareOpSymbol(tc.op); got != tc.want {
+ t.Fatalf("%s: CompareOpSymbol(%v) = %q, want %q", tc.name, tc.op, got, tc.want)
+ }
+ }
+}
diff --git a/internal/globalfilter/filter_test.go b/internal/globalfilter/filter_test.go
index 7d36c88..c386673 100644
--- a/internal/globalfilter/filter_test.go
+++ b/internal/globalfilter/filter_test.go
@@ -1,7 +1,6 @@
package globalfilter
import (
- "strings"
"testing"
)
@@ -56,9 +55,6 @@ func TestFilterZeroValueMatchesAll(t *testing.T) {
if filter.IsActive() {
t.Fatalf("zero-value filter should be inactive")
}
- if got := filter.Summary(); got != "all" {
- t.Fatalf("Summary() = %q, want all", got)
- }
}
func TestFilterStringAndNumericMatching(t *testing.T) {
@@ -132,62 +128,6 @@ func TestFilterErrorsOnlyAndClone(t *testing.T) {
}
}
-func TestFilterSummaryAndDurationParsing(t *testing.T) {
- filter := Filter{
- ErrorsOnly: true,
- Syscall: &StringFilter{Pattern: "read"},
- PID: &NumericFilter{Op: OpEq, Value: 1234},
- LatencyNs: &NumericFilter{Op: OpGt, Value: 1_000_000},
- }
- got := filter.Summary()
- for _, want := range []string{"errors", "syscall~read", "pid=1234", "latency>1ms"} {
- if !strings.Contains(got, want) {
- t.Fatalf("Summary() = %q, missing %q", got, want)
- }
- }
-
- for _, tc := range []struct {
- in string
- want int64
- }{
- {in: "1ms", want: 1_000_000},
- {in: "10us", want: 10_000},
- {in: "500ns", want: 500},
- {in: "42", want: 42},
- } {
- gotNs, err := ParseDurationNs(tc.in)
- if err != nil {
- t.Fatalf("ParseDurationNs(%q) err = %v", tc.in, err)
- }
- if gotNs != tc.want {
- t.Fatalf("ParseDurationNs(%q) = %d, want %d", tc.in, gotNs, tc.want)
- }
- }
- if _, err := ParseDurationNs("garbage"); err == nil {
- t.Fatalf("ParseDurationNs(garbage) expected error")
- }
-}
-
-func TestCompareOpSymbol(t *testing.T) {
- for _, tc := range []struct {
- name string
- op CompareOp
- want string
- }{
- {name: "eq", op: OpEq, want: "="},
- {name: "neq", op: OpNeq, want: "!="},
- {name: "gt", op: OpGt, want: ">"},
- {name: "gte", op: OpGte, want: ">="},
- {name: "lt", op: OpLt, want: "<"},
- {name: "lte", op: OpLte, want: "<="},
- {name: "unknown", op: CompareOp(99), want: "?"},
- } {
- if got := CompareOpSymbol(tc.op); got != tc.want {
- t.Fatalf("%s: CompareOpSymbol(%v) = %q, want %q", tc.name, tc.op, got, tc.want)
- }
- }
-}
-
func TestFilterEqual(t *testing.T) {
base := Filter{
Syscall: &StringFilter{Pattern: "read"},
diff --git a/internal/globalfilter/parser/parser.go b/internal/globalfilter/parser/parser.go
new file mode 100644
index 0000000..95234a8
--- /dev/null
+++ b/internal/globalfilter/parser/parser.go
@@ -0,0 +1,49 @@
+// Package parser provides input parsing helpers for globalfilter values.
+// It is intentionally kept free of domain types so it can be imported by
+// both globalfilter and its callers without creating import cycles.
+package parser
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// ParseDurationNs parses a human-readable duration string and returns the
+// equivalent number of nanoseconds. Plain integers are interpreted as
+// nanoseconds directly. Standard Go duration suffixes (ns, us/µs/μs, ms, s…)
+// are also accepted. Returns an error for empty or unparseable input.
+func ParseDurationNs(input string) (int64, error) {
+ s := strings.TrimSpace(strings.ToLower(input))
+ if s == "" {
+ return 0, fmt.Errorf("empty duration")
+ }
+ s = strings.ReplaceAll(s, "µs", "us")
+ s = strings.ReplaceAll(s, "μs", "us")
+ if onlyDigits(s) || strings.HasPrefix(s, "-") && onlyDigits(s[1:]) {
+ v, err := strconv.ParseInt(s, 10, 64)
+ if err != nil {
+ return 0, err
+ }
+ return v, nil
+ }
+ d, err := time.ParseDuration(s)
+ if err != nil {
+ return 0, err
+ }
+ return d.Nanoseconds(), nil
+}
+
+// onlyDigits reports whether s is a non-empty string of ASCII decimal digits.
+func onlyDigits(s string) bool {
+ if s == "" {
+ return false
+ }
+ for _, ch := range s {
+ if ch < '0' || ch > '9' {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/globalfilter/parser/parser_test.go b/internal/globalfilter/parser/parser_test.go
new file mode 100644
index 0000000..62e5978
--- /dev/null
+++ b/internal/globalfilter/parser/parser_test.go
@@ -0,0 +1,37 @@
+package parser_test
+
+import (
+ "testing"
+
+ "ior/internal/globalfilter/parser"
+)
+
+func TestParseDurationNsAcceptsValidInputs(t *testing.T) {
+ for _, tc := range []struct {
+ in string
+ want int64
+ }{
+ {in: "1ms", want: 1_000_000},
+ {in: "10us", want: 10_000},
+ {in: "500ns", want: 500},
+ {in: "42", want: 42},
+ {in: "-10", want: -10},
+ {in: "1s", want: 1_000_000_000},
+ } {
+ got, err := parser.ParseDurationNs(tc.in)
+ if err != nil {
+ t.Fatalf("ParseDurationNs(%q) unexpected error: %v", tc.in, err)
+ }
+ if got != tc.want {
+ t.Fatalf("ParseDurationNs(%q) = %d, want %d", tc.in, got, tc.want)
+ }
+ }
+}
+
+func TestParseDurationNsRejectsInvalidInputs(t *testing.T) {
+ for _, bad := range []string{"garbage", "", "abc", "1xx"} {
+ if _, err := parser.ParseDurationNs(bad); err == nil {
+ t.Fatalf("ParseDurationNs(%q) expected error, got nil", bad)
+ }
+ }
+}
diff --git a/internal/globalfilter/presenter/presenter.go b/internal/globalfilter/presenter/presenter.go
new file mode 100644
index 0000000..1bf5c79
--- /dev/null
+++ b/internal/globalfilter/presenter/presenter.go
@@ -0,0 +1,85 @@
+// Package presenter formats globalfilter domain values for human-readable
+// display. It imports globalfilter types but adds no domain logic, keeping
+// presentation concerns out of the core filter package.
+package presenter
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "ior/internal/globalfilter"
+)
+
+// CompareOpSymbol returns the display symbol for a numeric comparison operator
+// (e.g. OpEq → "=", OpNeq → "!=").
+func CompareOpSymbol(op globalfilter.CompareOp) string {
+ switch op {
+ case globalfilter.OpEq:
+ return "="
+ case globalfilter.OpNeq:
+ return "!="
+ case globalfilter.OpGt:
+ return ">"
+ case globalfilter.OpGte:
+ return ">="
+ case globalfilter.OpLt:
+ return "<"
+ case globalfilter.OpLte:
+ return "<="
+ default:
+ return "?"
+ }
+}
+
+// AppendStringSummary appends a "name~pattern" token to parts when sf is
+// non-nil and its pattern is non-empty, then returns the updated slice.
+func AppendStringSummary(parts []string, name string, sf *globalfilter.StringFilter) []string {
+ if sf == nil {
+ return parts
+ }
+ pattern := strings.TrimSpace(sf.Pattern)
+ if pattern == "" {
+ return parts
+ }
+ return append(parts, fmt.Sprintf("%s~%s", name, pattern))
+}
+
+// AppendNumericSummary appends a "nameOPvalue" token to parts when nf is
+// non-nil. When duration is true the value is formatted as a time.Duration
+// string rather than a raw integer.
+func AppendNumericSummary(parts []string, name string, nf *globalfilter.NumericFilter, duration bool) []string {
+ if nf == nil {
+ return parts
+ }
+ value := fmt.Sprintf("%d", nf.Value)
+ if duration {
+ value = time.Duration(nf.Value).String()
+ }
+ return append(parts, fmt.Sprintf("%s%s%s", name, CompareOpSymbol(nf.Op), value))
+}
+
+// FilterSummary returns a compact human-readable description of all active
+// filter predicates, e.g. "syscall~read pid=1234". Returns "all" when no
+// predicates are set. This is the canonical presentation of a Filter value
+// for status bars and log messages.
+func FilterSummary(f globalfilter.Filter) string {
+ parts := make([]string, 0, 10)
+ if f.ErrorsOnly {
+ parts = append(parts, "errors")
+ }
+ parts = AppendStringSummary(parts, "syscall", f.Syscall)
+ parts = AppendStringSummary(parts, "comm", f.Comm)
+ parts = AppendStringSummary(parts, "file", f.File)
+ parts = AppendNumericSummary(parts, "pid", f.PID, false)
+ parts = AppendNumericSummary(parts, "tid", f.TID, false)
+ parts = AppendNumericSummary(parts, "fd", f.FD, false)
+ parts = AppendNumericSummary(parts, "latency", f.LatencyNs, true)
+ parts = AppendNumericSummary(parts, "gap", f.GapNs, true)
+ parts = AppendNumericSummary(parts, "bytes", f.Bytes, false)
+ parts = AppendNumericSummary(parts, "ret", f.RetVal, false)
+ if len(parts) == 0 {
+ return "all"
+ }
+ return strings.Join(parts, " ")
+}
diff --git a/internal/globalfilter/presenter/presenter_test.go b/internal/globalfilter/presenter/presenter_test.go
new file mode 100644
index 0000000..eb35aaf
--- /dev/null
+++ b/internal/globalfilter/presenter/presenter_test.go
@@ -0,0 +1,83 @@
+package presenter_test
+
+import (
+ "strings"
+ "testing"
+
+ "ior/internal/globalfilter"
+ "ior/internal/globalfilter/presenter"
+)
+
+func TestCompareOpSymbolCoversAllOps(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ op globalfilter.CompareOp
+ want string
+ }{
+ {name: "eq", op: globalfilter.OpEq, want: "="},
+ {name: "neq", op: globalfilter.OpNeq, want: "!="},
+ {name: "gt", op: globalfilter.OpGt, want: ">"},
+ {name: "gte", op: globalfilter.OpGte, want: ">="},
+ {name: "lt", op: globalfilter.OpLt, want: "<"},
+ {name: "lte", op: globalfilter.OpLte, want: "<="},
+ {name: "unknown", op: globalfilter.CompareOp(99), want: "?"},
+ } {
+ if got := presenter.CompareOpSymbol(tc.op); got != tc.want {
+ t.Fatalf("%s: CompareOpSymbol() = %q, want %q", tc.name, got, tc.want)
+ }
+ }
+}
+
+func TestAppendStringSummarySkipsEmptyAndNilFilters(t *testing.T) {
+ parts := []string{}
+ parts = presenter.AppendStringSummary(parts, "syscall", nil)
+ if len(parts) != 0 {
+ t.Fatalf("nil filter should not append")
+ }
+ parts = presenter.AppendStringSummary(parts, "syscall", &globalfilter.StringFilter{Pattern: " "})
+ if len(parts) != 0 {
+ t.Fatalf("blank pattern should not append")
+ }
+ parts = presenter.AppendStringSummary(parts, "syscall", &globalfilter.StringFilter{Pattern: "read"})
+ if len(parts) != 1 || parts[0] != "syscall~read" {
+ t.Fatalf("expected syscall~read, got %v", parts)
+ }
+}
+
+func TestAppendNumericSummaryFormatsDurationsAndIntegers(t *testing.T) {
+ parts := []string{}
+ nf := &globalfilter.NumericFilter{Op: globalfilter.OpGt, Value: 1_000_000}
+ parts = presenter.AppendNumericSummary(parts, "latency", nf, true)
+ if len(parts) != 1 || parts[0] != "latency>1ms" {
+ t.Fatalf("expected latency>1ms, got %v", parts)
+ }
+
+ parts = []string{}
+ nf = &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 42}
+ parts = presenter.AppendNumericSummary(parts, "pid", nf, false)
+ if len(parts) != 1 || parts[0] != "pid=42" {
+ t.Fatalf("expected pid=42, got %v", parts)
+ }
+}
+
+func TestFilterSummaryReturnsAllForZeroFilter(t *testing.T) {
+ if got := presenter.FilterSummary(globalfilter.Filter{}); got != "all" {
+ t.Fatalf("FilterSummary(zero) = %q, want \"all\"", got)
+ }
+}
+
+func TestFilterSummaryIncludesAllActivePredicates(t *testing.T) {
+ f := globalfilter.Filter{
+ ErrorsOnly: true,
+ Syscall: &globalfilter.StringFilter{Pattern: "read"},
+ Comm: &globalfilter.StringFilter{Pattern: "nginx"},
+ PID: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 1234},
+ LatencyNs: &globalfilter.NumericFilter{Op: globalfilter.OpGt, Value: 1_000_000},
+ }
+ got := presenter.FilterSummary(f)
+ for _, want := range []string{"errors", "syscall~read", "comm~nginx", "pid=1234", "latency>1ms"} {
+ if !strings.Contains(got, want) {
+ t.Fatalf("FilterSummary() = %q, missing %q", got, want)
+ }
+ }
+}
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 123600a..8548481 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -7,6 +7,7 @@ import (
"time"
"ior/internal/globalfilter"
+ "ior/internal/globalfilter/presenter"
"ior/internal/statsengine"
common "ior/internal/tui/common"
"ior/internal/tui/eventstream"
@@ -1104,7 +1105,7 @@ func (m Model) View() tea.View {
}
func (m Model) filterSummary() string {
- summary := "filter: " + m.globalFilter.Summary()
+ summary := "filter: " + presenter.FilterSummary(m.globalFilter)
if len(m.filterStack) > 0 {
summary += " | stack: " + strings.Join(m.filterStack, " | ")
}
diff --git a/internal/tui/eventstream/filter.go b/internal/tui/eventstream/filter.go
index 61d8c33..801b8ba 100644
--- a/internal/tui/eventstream/filter.go
+++ b/internal/tui/eventstream/filter.go
@@ -1,6 +1,9 @@
package eventstream
-import "ior/internal/globalfilter"
+import (
+ "ior/internal/globalfilter"
+ "ior/internal/globalfilter/parser"
+)
type CompareOp = globalfilter.CompareOp
type NumericFilter = globalfilter.NumericFilter
@@ -16,6 +19,8 @@ const (
OpLte = globalfilter.OpLte
)
+// ParseDurationNs delegates to parser.ParseDurationNs, re-exporting the
+// duration parsing helper for eventstream callers.
func ParseDurationNs(input string) (int64, error) {
- return globalfilter.ParseDurationNs(input)
+ return parser.ParseDurationNs(input)
}
diff --git a/internal/tui/eventstream/filter_test.go b/internal/tui/eventstream/filter_test.go
index 0413904..efedbe4 100644
--- a/internal/tui/eventstream/filter_test.go
+++ b/internal/tui/eventstream/filter_test.go
@@ -3,6 +3,8 @@ package eventstream
import (
"strings"
"testing"
+
+ "ior/internal/globalfilter/presenter"
)
func sampleEvent() StreamEvent {
@@ -31,8 +33,8 @@ func TestFilterZeroValueMatchesAll(t *testing.T) {
if f.IsActive() {
t.Fatalf("zero-value filter should be inactive")
}
- if got := f.Summary(); got != "all" {
- t.Fatalf("Summary() = %q, want all", got)
+ if got := presenter.FilterSummary(f); got != "all" {
+ t.Fatalf("FilterSummary() = %q, want all", got)
}
}
@@ -120,7 +122,7 @@ func TestFilterSummaryIncludesActivePredicates(t *testing.T) {
PID: &NumericFilter{Op: OpEq, Value: 1234},
LatencyNs: &NumericFilter{Op: OpGt, Value: 1000000},
}
- got := f.Summary()
+ got := presenter.FilterSummary(f)
for _, wantPart := range []string{"errors", "syscall~read", "pid=1234", "latency>1ms"} {
if !strings.Contains(got, wantPart) {
t.Fatalf("Summary() = %q, missing %q", got, wantPart)
diff --git a/internal/tui/eventstream/render.go b/internal/tui/eventstream/render.go
index 4d7970b..cd4d528 100644
--- a/internal/tui/eventstream/render.go
+++ b/internal/tui/eventstream/render.go
@@ -5,6 +5,7 @@ import (
"strconv"
"strings"
+ "ior/internal/globalfilter/presenter"
"ior/internal/tui/common"
"charm.land/lipgloss/v2"
@@ -75,7 +76,7 @@ func renderStatusLine(paused bool, totalCount, filteredCount, bufferLen, bufferC
}
func renderFilterLine(filter Filter) string {
- summary := filter.Summary()
+ summary := presenter.FilterSummary(filter)
if summary == "all" {
summary = common.HighlightStyle.Render(summary)
}
diff --git a/internal/tui/filterstack.go b/internal/tui/filterstack.go
index dbd26a0..016d845 100644
--- a/internal/tui/filterstack.go
+++ b/internal/tui/filterstack.go
@@ -4,6 +4,7 @@ import (
"strings"
"ior/internal/globalfilter"
+ "ior/internal/globalfilter/presenter"
)
// filterStack manages the trace filter chain: the active filter, the undo
@@ -124,14 +125,14 @@ func globalFilterActionLabel(prev, next globalfilter.Filter, action string) stri
parts = appendNumericFilterChange(parts, "bytes", prev.Bytes, next.Bytes, false)
parts = appendNumericFilterChange(parts, "ret", prev.RetVal, next.RetVal, false)
if len(parts) == 0 {
- return next.Summary()
+ return presenter.FilterSummary(next)
}
return strings.Join(parts, " ")
}
// appendStringFilterChange appends a change token to parts for a string
// filter field. It emits "clear name" when the filter is removed, or delegates
-// to globalfilter.AppendStringSummary for the canonical "name~pattern" format.
+// to presenter.AppendStringSummary for the canonical "name~pattern" format.
func appendStringFilterChange(parts []string, name string, prev, next *globalfilter.StringFilter) []string {
if sameStringFilter(prev, next) {
return parts
@@ -139,12 +140,12 @@ func appendStringFilterChange(parts []string, name string, prev, next *globalfil
if next == nil || strings.TrimSpace(next.Pattern) == "" {
return append(parts, "clear "+name)
}
- return globalfilter.AppendStringSummary(parts, name, next)
+ return presenter.AppendStringSummary(parts, name, next)
}
// appendNumericFilterChange appends a change token to parts for a numeric
// filter field. It emits "clear name" when the filter is removed, or delegates
-// to globalfilter.AppendNumericSummary for the canonical "nameOPvalue" format.
+// to presenter.AppendNumericSummary for the canonical "nameOPvalue" format.
func appendNumericFilterChange(parts []string, name string, prev, next *globalfilter.NumericFilter, duration bool) []string {
if sameNumericFilter(prev, next) {
return parts
@@ -152,7 +153,7 @@ func appendNumericFilterChange(parts []string, name string, prev, next *globalfi
if next == nil {
return append(parts, "clear "+name)
}
- return globalfilter.AppendNumericSummary(parts, name, next, duration)
+ return presenter.AppendNumericSummary(parts, name, next, duration)
}
func sameStringFilter(a, b *globalfilter.StringFilter) bool {
diff --git a/internal/tui/tracefilter/model.go b/internal/tui/tracefilter/model.go
index caef948..b46d50a 100644
--- a/internal/tui/tracefilter/model.go
+++ b/internal/tui/tracefilter/model.go
@@ -6,6 +6,7 @@ import (
"strings"
"ior/internal/globalfilter"
+ "ior/internal/globalfilter/parser"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
@@ -323,7 +324,7 @@ func parseNumericFilter(value string, opIndex int, duration bool) (*globalfilter
err error
)
if duration {
- number, err = globalfilter.ParseDurationNs(value)
+ number, err = parser.ParseDurationNs(value)
} else {
number, err = strconv.ParseInt(value, 10, 64)
}