summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/ui/handlers.go23
-rw-r--r--internal/ui/helpers.go23
-rw-r--r--internal/ui/helpers_test.go80
3 files changed, 123 insertions, 3 deletions
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go
index 2d3208a..a210ffc 100644
--- a/internal/ui/handlers.go
+++ b/internal/ui/handlers.go
@@ -292,11 +292,28 @@ func (m *Model) handlePriorityMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
return m, nil
}
-// handleFilterMode handles filter editing
+// handleFilterMode handles filter editing for both traditional and ultra mode.
+// The filter value is split using shell-quoting rules (via parseFilterInput)
+// so that expressions with quoted values (e.g. description:"my task") are
+// passed to taskwarrior as a single argument. Any taskwarrior filter expression
+// that is valid on the command line (proj:xxx, +tag, description:"...", etc.)
+// is therefore accepted here too. Taskwarrior errors are propagated back to
+// the user via the status bar rather than being silently discarded.
func (m *Model) handleFilterMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
onEnter := func(value string) error {
- m.filters = strings.Fields(value)
- _ = m.reload()
+ fields, err := parseFilterInput(value)
+ if err != nil {
+ return err
+ }
+ m.filters = fields
+ // Propagate taskwarrior errors so the user sees feedback when a
+ // filter expression is rejected by taskwarrior.
+ if err := m.reload(); err != nil {
+ // Roll back the filters to avoid leaving the UI in a broken state
+ // where an empty task list is shown without any explanation.
+ m.filters = nil
+ return fmt.Errorf("filter error: %w", err)
+ }
return nil
}
diff --git a/internal/ui/helpers.go b/internal/ui/helpers.go
index 4cdf3cc..6cefa96 100644
--- a/internal/ui/helpers.go
+++ b/internal/ui/helpers.go
@@ -6,6 +6,8 @@ import (
"strings"
"time"
+ "github.com/google/shlex"
+
"codeberg.org/snonux/tasksamurai/internal/task"
)
@@ -72,6 +74,27 @@ func compileAndCacheRegex(pattern string) (*regexp.Regexp, error) {
return re, nil
}
+// parseFilterInput splits a raw filter string typed by the user into the
+// individual filter tokens that are passed to taskwarrior. Shell-quoting
+// rules are applied via shlex so that expressions like
+//
+// description:"my task"
+// proj:dtail +urgent
+//
+// are handled correctly: quoted values are kept as a single argument (with the
+// quotes stripped) rather than being split on whitespace. An empty input
+// returns a nil slice, which clears the current filter.
+func parseFilterInput(input string) ([]string, error) {
+ fields, err := shlex.Split(input)
+ if err != nil {
+ return nil, fmt.Errorf("invalid filter expression: %w", err)
+ }
+ if len(fields) == 0 {
+ return nil, nil
+ }
+ return fields, nil
+}
+
// Validation functions
// validateTagName validates a tag name
diff --git a/internal/ui/helpers_test.go b/internal/ui/helpers_test.go
index 9ba620c..f0c0e27 100644
--- a/internal/ui/helpers_test.go
+++ b/internal/ui/helpers_test.go
@@ -1,6 +1,7 @@
package ui
import (
+ "reflect"
"testing"
"time"
)
@@ -360,4 +361,83 @@ func TestValidateRecurrence(t *testing.T) {
}
})
}
+}
+
+// TestParseFilterInput verifies that parseFilterInput correctly handles
+// taskwarrior filter expressions, including attribute filters (proj:xxx),
+// tag filters (+tag), quoted values (description:"some text"), and empty input.
+func TestParseFilterInput(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "empty input clears filter",
+ input: "",
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "project attribute filter",
+ input: "proj:dtail",
+ want: []string{"proj:dtail"},
+ wantErr: false,
+ },
+ {
+ name: "tag filter",
+ input: "+urgent",
+ want: []string{"+urgent"},
+ wantErr: false,
+ },
+ {
+ name: "multiple filters",
+ input: "proj:dtail +urgent",
+ want: []string{"proj:dtail", "+urgent"},
+ wantErr: false,
+ },
+ {
+ name: "quoted description filter keeps value as single token",
+ input: `description:"my task"`,
+ // shlex strips the quotes and keeps the value as one argument
+ want: []string{"description:my task"},
+ wantErr: false,
+ },
+ {
+ name: "project filter with multiple words via quoting",
+ input: `project:"my project"`,
+ want: []string{"project:my project"},
+ },
+ {
+ name: "status filter",
+ input: "status:pending",
+ want: []string{"status:pending"},
+ wantErr: false,
+ },
+ {
+ name: "whitespace-only input clears filter",
+ input: " ",
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "unclosed quote returns error",
+ input: `proj:"unclosed`,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := parseFilterInput(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("parseFilterInput(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("parseFilterInput(%q) = %v, want %v", tt.input, got, tt.want)
+ }
+ })
+ }
} \ No newline at end of file