Explain empty TUI views instead of showing a bare table

An account folder is only registered in the index by an import, so a freshly
configured account.toml produced an empty accounts table with nothing to say
why or what to do about it.

Each view now explains its own emptiness and names the key that resolves it,
distinguishing configured-but-not-imported from no-folders-at-all, and
telling apart a filter that matched nothing from having no data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 01:04:14 +02:00
co-authored by Claude Opus 5
parent cf1753fb0a
commit 9ba3fe8fbe
4 changed files with 171 additions and 8 deletions
+64 -8
View File
@@ -68,6 +68,7 @@ var (
statusStyle = lipgloss.NewStyle().Faint(true).Padding(0, 1)
errStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9")).Padding(0, 1)
helpStyle = lipgloss.NewStyle().Faint(true).Padding(0, 1)
emptyStyle = lipgloss.NewStyle().Padding(1, 2)
)
// Run starts the interface.
@@ -477,15 +478,22 @@ func (m *Model) updateNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
func (m *Model) View() string {
var b strings.Builder
b.WriteString(titleStyle.Render(m.title()) + "\n")
b.WriteString(titleStyle.Render(m.title()))
b.WriteString("\n")
switch m.view {
case viewAccounts:
b.WriteString(m.accountTable.View())
case viewTxns:
b.WriteString(m.txnTable.View())
case viewReport:
b.WriteString(m.reportTable.View())
// An empty table is just a bare header, which says nothing about why it is
// empty or what to do next.
if empty := m.emptyMessage(); empty != "" {
b.WriteString(emptyStyle.Render(empty))
} else {
switch m.view {
case viewAccounts:
b.WriteString(m.accountTable.View())
case viewTxns:
b.WriteString(m.txnTable.View())
case viewReport:
b.WriteString(m.reportTable.View())
}
}
b.WriteString("\n")
@@ -504,6 +512,54 @@ func (m *Model) View() string {
return b.String()
}
// emptyMessage explains an empty view and names the key that fixes it, or
// returns "" when there is something to show.
func (m *Model) emptyMessage() string {
switch m.view {
case viewAccounts:
if len(m.accountTable.Rows()) > 0 {
return ""
}
// An account.toml on disk is not enough: folders are only registered
// in the index by an import, so say that rather than showing nothing.
if n := len(m.accounts); n > 0 {
return fmt.Sprintf(
"No accounts imported yet.\n\n"+
"%d account folder(s) configured in %s.\n"+
"Press i to import their statements (same as running `money import`).",
n, m.root)
}
return fmt.Sprintf(
"No account folders found in %s.\n\n"+
"An account is a folder containing an account.toml.\n"+
"Create one, drop statements in, then press i to import.",
m.root)
case viewTxns:
if len(m.txns) > 0 {
return ""
}
switch {
case m.filter.Search != "":
return fmt.Sprintf("No transactions match /%s.\n\nPress / to change the search.", m.filter.Search)
case m.onlyUntagged:
return "Nothing untagged.\n\nEvery transaction here has a tag. Press u to see them all."
case m.filter.AccountSlug != "":
return fmt.Sprintf(
"No transactions in %s.\n\nPress i to import, or a to see every account.",
m.filter.AccountSlug)
}
return "No transactions yet.\n\nPress i to import (same as running `money import`)."
case viewReport:
if len(m.reportTable.Rows()) > 0 {
return ""
}
return "Nothing to report yet.\n\nPress i to import some statements first."
}
return ""
}
func (m *Model) title() string {
scope := "all accounts"
if m.filter.AccountSlug != "" {