Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ manually launched TUIs.

The app has four sections: Mail, Contacts, Calendar and Journal. The context-sensitive
shortcut bar is visible by default; press `?` to hide or restore it, and the choice is
remembered across restarts.
remembered across restarts. While it is hidden, `? help` at the left of the top bar says
how to bring it back.

Mail navigation includes HEY's boxes plus separate Labels and Collections tabs: Shift+L
opens Labels directly and Shift+K opens Collections. Previously Seen has its own tab after
Expand Down
21 changes: 16 additions & 5 deletions internal/tui/nav.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,11 @@ func centerText(text string, width int) string {
}

// renderTopRule draws the top rule with HEY centered and the account
// aligned to the right, both bold:
// aligned to the right, both bold. A hint, when given, sits at the left end
// if there is room for it beside HEY:
//
// ─────────── HEY ─────────── jz@example.com ──
func renderTopRule(width int, account string) string {
// ── ? help ── HEY ─────────── jz@example.com ──
func renderTopRule(width int, hint, account string) string {
ruleStyle := lipgloss.NewStyle().Foreground(colorChrome)
labelStyle := lipgloss.NewStyle().Foreground(colorChrome).Bold(true)

Expand All @@ -346,6 +347,12 @@ func renderTopRule(width int, account string) string {
}

var b strings.Builder
const lead = 2
if hintWidth := lipgloss.Width(hint) + 2; hint != "" && left >= lead+hintWidth+1 {
b.WriteString(ruleStyle.Render(strings.Repeat("─", lead)))
b.WriteString(" " + labelStyle.Render(hint) + " ")
left -= lead + hintWidth
}
b.WriteString(ruleStyle.Render(strings.Repeat("─", left)))
b.WriteString(" " + labelStyle.Render("HEY") + " ")
b.WriteString(ruleStyle.Render(strings.Repeat("─", mid)))
Expand All @@ -360,8 +367,12 @@ func renderTopRule(width int, account string) string {
func renderHeader(m *model) string {
var b strings.Builder

// Row 1: section rule + items
b.WriteString(renderTopRule(m.width, m.mailAccount.label))
// Row 1: section rule + items. Hidden help leaves the one key that brings it back.
hint := ""
if m.help.hidden && m.canToggleHelp() {
Comment thread
robzolkos marked this conversation as resolved.
hint = "? help"
}
b.WriteString(renderTopRule(m.width, hint, m.mailAccount.label))
b.WriteString("\n")
b.WriteString(renderNavRow(sectionItems, int(m.section), m.focus == rowSection, m.width, true))
b.WriteString("\n")
Expand Down
3 changes: 2 additions & 1 deletion internal/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,8 @@ func (m model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
}

func (m model) canToggleHelp() bool {
if m.mailAccountPicker {
// An error box takes every key until it is dismissed, ? included.
if m.mailAccountPicker || m.err != nil {
return false
}
if m.activeView == m.screenerView {
Expand Down
37 changes: 36 additions & 1 deletion internal/tui/tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,19 @@ func TestQuestionMarkTogglesHelpAndResizesContent(t *testing.T) {
if !slices.Equal(saved, []bool{true}) {
t.Errorf("saved preferences = %v, want [true]", saved)
}
if top := lines[0]; !strings.Contains(top, " ? help ") {
t.Errorf("hidden help left no way back on the top rule: %q", top)
}
m.err = errors.New("HEY is not answering")
if top := strings.SplitN(stripANSI(m.View().Content), "\n", 2)[0]; strings.Contains(top, "? help") {
t.Errorf("the top rule offers ? while an error box would swallow it: %q", top)
}
updated, _ = m.Update(keyPress("?"))
m = updated.(model)
if !m.help.hidden {
t.Error("? acted through an error box")
}
m.err = nil

updated, _ = m.Update(keyPress("?"))
m = updated.(model)
Expand Down Expand Up @@ -1436,8 +1449,30 @@ func TestChromeUsesBlueBoldConvention(t *testing.T) {
}
}

func TestTopRuleShowsAHintBesideHeyWhenThereIsRoom(t *testing.T) {
line := stripANSI(renderTopRule(80, "? help", "frank.castillo@example.com"))
if got := lipgloss.Width(line); got != 80 {
t.Errorf("top rule width = %d, want 80", got)
}
if !strings.HasPrefix(line, "── ? help ─") {
t.Errorf("the hint should open the rule: %q", line)
}
heyColumn := func(rule string) int { return lipgloss.Width(rule[:strings.Index(rule, "HEY")]) }
if want := stripANSI(renderTopRule(80, "", "frank.castillo@example.com")); heyColumn(line) != heyColumn(want) {
t.Errorf("the hint moved HEY: %q, want HEY where it is in %q", line, want)
}
if !strings.HasSuffix(line, "frank.castillo@example.com ──") {
t.Errorf("the account should stay right-aligned: %q", line)
}

narrow := stripANSI(renderTopRule(40, "? help", "frank.castillo@example.com"))
if strings.Contains(narrow, "help") || lipgloss.Width(narrow) != 40 {
t.Errorf("a rule with no room for the hint = %q, want it left out", narrow)
}
}

func TestTopRuleCentersHeyAndRightAlignsAccount(t *testing.T) {
line := renderTopRule(80, "frank.castillo@example.com")
line := renderTopRule(80, "", "frank.castillo@example.com")
if got := lipgloss.Width(line); got != 80 {
t.Errorf("top rule width = %d, want 80", got)
}
Expand Down
Loading