Advertise the rule screens in the key help

The bottom help still described 4 as "rules" from before the rules list
existed, and never mentioned 5 at all, so the new screen was unreachable
unless you already knew about it.

The transactions line had also outgrown the window: at 137 characters it ran
past the edge of a normal terminal and "q quit" was simply gone. Help now
wraps to the window width instead of being cut off, and that line is shorter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 02:09:42 +02:00
co-authored by Claude Opus 5
parent c2eba53fd4
commit 3eca8f5376
3 changed files with 62 additions and 6 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ const usage = `money - statement-driven personal finance tracker
usage: money [--root DIR] <command> [flags] usage: money [--root DIR] <command> [flags]
commands: commands:
tui browse and tag transactions (default) tui browse and tag transactions, build and prune rules (default)
import extract transactions from every statement into the index import extract transactions from every statement into the index
retag re-apply rules.toml to everything already imported retag re-apply rules.toml to everything already imported
ls list transactions ls list transactions
+12 -5
View File
@@ -1111,7 +1111,14 @@ func (m *Model) View() string {
b.WriteString(statusStyle.Render(m.status)) b.WriteString(statusStyle.Render(m.status))
} }
b.WriteString("\n") b.WriteString("\n")
b.WriteString(helpStyle.Render(m.help())) // Wrap the key list rather than letting the terminal cut it off: the keys
// at the end are as important as the ones at the start, and "q quit" was
// the first casualty on a narrow window.
help := helpStyle
if m.width > 0 {
help = help.Width(m.width)
}
b.WriteString(help.Render(m.help()))
return b.String() return b.String()
} }
@@ -1252,17 +1259,17 @@ func (m *Model) help() string {
} }
switch m.view { switch m.view {
case viewAccounts: case viewAccounts:
return "enter open · 2 transactions · 3 report · 4 rules · i import · r retag · q quit" return "enter open · 2 transactions · 3 report · 4 new rule · 5 rules · i import · r retag · q quit"
case viewRules: case viewRules:
return "tab/↑↓ field · pgup/pgdn scroll list · enter save rule · esc back · ctrl+c quit" return "tab/↑↓ field · pgup/pgdn scroll list · enter save rule · esc back · ctrl+c quit"
case viewRuleList: case viewRuleList:
if m.confirm != confirmNone { if m.confirm != confirmNone {
return "y confirm · any other key cancels" return "y confirm · any other key cancels"
} }
return "d delete rule · p prune all unused · r refresh counts · 4 rule builder · esc back · q quit" return "d delete rule · p prune all unused · r refresh counts · 4 new rule · 1 accounts · esc back · q quit"
case viewReport: case viewReport:
return "1 accounts · 2 transactions · 4 rules · u untagged · a all accounts · q quit" return "1 accounts · 2 transactions · 4 new rule · 5 rules · u untagged · a all accounts · q quit"
default: default:
return "t tag · x transfer · c clear · / search · u untagged · a all · i import · r retag · 4 rules · 1 accounts · 3 report · q quit" return "t tag · x transfer · c clear · / search · u untagged · a all · i import · r retag · 4 new rule · 5 rules · q quit"
} }
} }
+49
View File
@@ -840,6 +840,55 @@ func TestRuleListPruneWithNothingToDo(t *testing.T) {
} }
} }
// Every screen must advertise how to reach the others and how to quit. These
// lines drift as screens are added, and a line that outgrows the terminal
// loses its tail silently.
func TestHelpLinesMentionEveryScreen(t *testing.T) {
m, _ := newTestModel(t)
for _, tc := range []struct {
name string
key string
want []string
}{
{"accounts", "1", []string{"4 new rule", "5 rules", "q quit"}},
{"transactions", "2", []string{"4 new rule", "5 rules", "q quit"}},
{"report", "3", []string{"4 new rule", "5 rules", "q quit"}},
{"rules", "5", []string{"d delete rule", "p prune all unused", "4 new rule", "q quit"}},
} {
key(t, m, tc.key)
help := m.help()
for _, want := range tc.want {
if !strings.Contains(help, want) {
t.Errorf("%s help = %q, missing %q", tc.name, help, want)
}
}
}
// The rule builder is a form, so it advertises its own keys instead.
key(t, m, "4")
if help := m.help(); !strings.Contains(help, "enter save rule") || !strings.Contains(help, "esc back") {
t.Errorf("rule builder help = %q", help)
}
}
// A help line longer than the window must wrap, not be cut off.
func TestHelpWrapsOnNarrowTerminals(t *testing.T) {
m, _ := newTestModel(t)
key(t, m, "2")
m.Update(tea.WindowSizeMsg{Width: 90, Height: 30})
view := m.View()
if !strings.Contains(view, "q quit") {
t.Errorf("the end of the help line was lost at 90 columns:\n%s", view)
}
for _, line := range strings.Split(view, "\n") {
if len([]rune(line)) > 90 {
t.Errorf("line is %d columns wide, wider than the window:\n%q", len([]rune(line)), line)
}
}
}
// newEmptyModel builds a model over an index with nothing in it. // newEmptyModel builds a model over an index with nothing in it.
func newEmptyModel(t *testing.T, accounts []*config.Account) *Model { func newEmptyModel(t *testing.T, accounts []*config.Account) *Model {
t.Helper() t.Helper()