Let rules carry a note
A glob like *4412* says nothing about why it exists or who it catches, and six months later neither does memory. Rules get an optional note: free text that never takes part in matching, written as a TOML key rather than a comment so it survives a round trip and can be shown back. The rule builder grows a fourth field for it and the rules screen a last column. Four fields spaced out are taller than a short window has room for, so the form now drops its blank lines and then the hints on unfocused fields before anything would run off the bottom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -638,7 +638,7 @@ func TestRuleBuilderTabCyclesFieldsAndEscLeaves(t *testing.T) {
|
||||
key(t, m, "2") // come from the transactions view
|
||||
key(t, m, "4")
|
||||
|
||||
for i, want := range []int{1, 2, 0} {
|
||||
for i, want := range []int{1, 2, 3, 0} {
|
||||
m.Update(tea.KeyMsg{Type: tea.KeyTab})
|
||||
if m.ruleFocus != want {
|
||||
t.Errorf("tab %d moved focus to %d, want %d", i+1, m.ruleFocus, want)
|
||||
@@ -783,8 +783,89 @@ func TestRuleBuilderKeepsUnmatchedInput(t *testing.T) {
|
||||
if m.ruleTag.Value() != "zzz" {
|
||||
t.Errorf("tag = %q, want the typed text untouched", m.ruleTag.Value())
|
||||
}
|
||||
if m.ruleFocus != 0 {
|
||||
t.Errorf("focus = %d, want tab to wrap round to the glob field", m.ruleFocus)
|
||||
if m.ruleFocus != 3 {
|
||||
t.Errorf("focus = %d, want tab to move on to the note field", m.ruleFocus)
|
||||
}
|
||||
}
|
||||
|
||||
// A note is free text that rides along with the rule: written to rules.toml,
|
||||
// read back, and shown on the rules screen so the reason survives longer than
|
||||
// the memory of writing it.
|
||||
func TestRuleBuilderSavesNote(t *testing.T) {
|
||||
m, _, root := newRuleModel(t)
|
||||
key(t, m, "4")
|
||||
|
||||
m.ruleGlob.SetValue("*LIDL*")
|
||||
m.setRuleFocus(2)
|
||||
m.ruleTag.SetValue("groceries")
|
||||
m.setRuleFocus(3)
|
||||
typeText(t, m, "the weekly shop")
|
||||
key(t, m, "enter")
|
||||
|
||||
loaded, err := config.LoadRules(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(loaded.Rule) != 1 || loaded.Rule[0].Note != "the weekly shop" {
|
||||
t.Fatalf("rules.toml holds %+v, want the note round-tripped", loaded.Rule)
|
||||
}
|
||||
// The note is documentation, not a pattern: it must not narrow what the
|
||||
// rule catches.
|
||||
if loaded.Rule[0].Match != "*LIDL*" || loaded.Rule[0].Tag != "groceries" {
|
||||
t.Errorf("rule = %+v, want the glob and tag untouched", loaded.Rule[0])
|
||||
}
|
||||
if m.ruleNote.Value() != "" {
|
||||
t.Errorf("note = %q, want the field cleared for the next rule", m.ruleNote.Value())
|
||||
}
|
||||
|
||||
m.openRuleList()
|
||||
row := m.ruleListTable.Rows()[0]
|
||||
if row[len(row)-1] != "the weekly shop" {
|
||||
t.Errorf("rules screen row = %v, want the note in the last column", row)
|
||||
}
|
||||
}
|
||||
|
||||
// A rule without a note is still a rule; nothing about saving changes.
|
||||
func TestRuleBuilderNoteIsOptional(t *testing.T) {
|
||||
m, _, root := newRuleModel(t)
|
||||
key(t, m, "4")
|
||||
|
||||
m.ruleGlob.SetValue("*LIDL*")
|
||||
m.ruleTag.SetValue("groceries")
|
||||
key(t, m, "enter")
|
||||
|
||||
raw, err := os.ReadFile(filepath.Join(root, config.RulesFile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "note") {
|
||||
t.Errorf("rules.toml = %q, want no empty note key", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// The form has to survive a short window: four fields spaced out are taller
|
||||
// than the room a 24-line terminal leaves.
|
||||
func TestRuleFormFitsShortTerminals(t *testing.T) {
|
||||
m, _, _ := newRuleModel(t)
|
||||
key(t, m, "4")
|
||||
|
||||
for _, height := range []int{40, 32, 30, 26, 24} {
|
||||
m.Update(tea.WindowSizeMsg{Width: 140, Height: height})
|
||||
form := m.ruleFormView()
|
||||
// The same budget the tables get: the title, status and a help line
|
||||
// that may wrap onto a second row.
|
||||
if lines := strings.Count(form, "\n") + 1; lines > height-6 {
|
||||
t.Errorf("at height %d the form is %d lines, want at most %d", height, lines, height-6)
|
||||
}
|
||||
// However tight it gets, every field keeps its label and its box.
|
||||
for _, label := range []string{"glob", "account", "tag", "note"} {
|
||||
if !strings.Contains(form, label) {
|
||||
t.Errorf("at height %d the %s field disappeared", height, label)
|
||||
}
|
||||
}
|
||||
if boxes := strings.Count(form, "╭"); boxes != 4 {
|
||||
t.Errorf("at height %d there are %d input boxes, want 4", height, boxes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user