diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-06 22:58:07 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-07 09:24:18 +0300 |
| commit | 42831b2cd321cc4682a00f3494f1579c2e19d0e9 (patch) | |
| tree | 9e72069f190639ec52b59d9378d8eb8fad1e5d10 | |
| parent | 2f7c0a0cb8c247b7ebda3b1873f2219105693818 (diff) | |
ui: improve ultra mode colors, separators, and navigation
- Theme: SelectedBG changed from purple (57) to dark grey (238) so
selected cards are clearly distinct from the status bar; priority
badge colors switched to subtler dark variants (red/blue/green)
- Ultra header: ID bold white, urgency amber, age dim, status grey;
no background on unselected cards (pure black terminal background)
- Priority badge: 3-char wide centred pill with white text
- Annotations: italic and dimmer (244) vs description (253)
- Card separator: full-width ─── line in dim grey (237) instead of
a blank line, making task boundaries easier to scan
- u key in ultra mode toggles back to table view (syncs cursor);
from --ultra startup mode u is a no-op since there is no table to
return to
- q/esc from --ultra startup mode exits directly instead of dropping
to the table view (ultraStartup flag tracks launch origin)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/ui/keyhandlers.go | 5 | ||||
| -rw-r--r-- | internal/ui/table.go | 4 | ||||
| -rw-r--r-- | internal/ui/theme.go | 20 | ||||
| -rw-r--r-- | internal/ui/ultra.go | 57 |
4 files changed, 61 insertions, 25 deletions
diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go index e9235cd..d68a7f5 100644 --- a/internal/ui/keyhandlers.go +++ b/internal/ui/keyhandlers.go @@ -157,6 +157,11 @@ func (m *Model) handleQuitOrEscape() (tea.Model, tea.Cmd) { return m, nil } if m.showUltra { + // When started via --ultra flag there is no table view to return to, + // so q/esc exits the application directly. + if m.ultraStartup { + return m, tea.Quit + } m.ultraClearFocusedID() m.showUltra = false m.ultraSearchRegex = nil diff --git a/internal/ui/table.go b/internal/ui/table.go index ef81b9a..4cb918f 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -91,6 +91,7 @@ type detailViewState struct { // ultraState holds the state for the ultra mode task list and its search UI. type ultraState struct { showUltra bool + ultraStartup bool // true when ultra was set via --ultra flag; q quits directly ultraCursor int ultraOffset int ultraSearching bool @@ -1328,6 +1329,9 @@ func (m *Model) SetDisco(d bool) { // SetUltra enables or disables ultra mode, causing the UI to start directly // in the ultra task list view instead of the default table view. +// When u is true, q/esc quits the application immediately rather than +// returning to the table view, because there is no table view to return to. func (m *Model) SetUltra(u bool) { m.showUltra = u + m.ultraStartup = u } diff --git a/internal/ui/theme.go b/internal/ui/theme.go index 9d81aab..ad02a83 100644 --- a/internal/ui/theme.go +++ b/internal/ui/theme.go @@ -26,20 +26,20 @@ type Theme struct { // DefaultTheme returns the color theme used by Task Samurai. func DefaultTheme() Theme { return Theme{ - HeaderFG: "205", - SelectedFG: "229", - SelectedBG: "57", + HeaderFG: "75", // steel blue — labels in ultra cards + SelectedFG: "255", // bright white — text on selected card + SelectedBG: "238", // dark grey — clean selection highlight on black background RowFG: "0", RowBG: "57", - StatusFG: "229", - StatusBG: "57", + StatusFG: "229", // light yellow + StatusBG: "57", // dark purple — status bar background StartBG: "6", OverdueBG: "1", - PrioLowBG: "10", - PrioMedBG: "12", - PrioHighBG: "9", - SearchFG: "21", - SearchBG: "226", + PrioLowBG: "28", // dark green — subtler than bright 10 + PrioMedBG: "33", // medium blue — subtler than bright 12 + PrioHighBG: "160", // dark red — subtler than bright 9 + SearchFG: "16", + SearchBG: "220", // amber — easier on eyes than pure yellow 226 } } diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go index 288400f..c3e6ad5 100644 --- a/internal/ui/ultra.go +++ b/internal/ui/ultra.go @@ -626,7 +626,6 @@ func (m *Model) renderUltraAnnotations(t task.Task, width int) string { } func (m *Model) renderUltraHeaderWithRegex(t task.Task, width int, re *regexp.Regexp) string { - _ = width idText := fmt.Sprintf("#%d", t.ID) priorityText := ultraOrDash(t.Priority) statusText := ultraOrDash(t.Status) @@ -634,14 +633,15 @@ func (m *Model) renderUltraHeaderWithRegex(t task.Task, width int, re *regexp.Re ageText := ultraTaskAge(t.Entry) line := strings.Join([]string{idText, priorityText, statusText, urgencyText, ageText}, " | ") if re != nil && re.MatchString(line) && !ultraRegexMatchesAny(re, idText, priorityText, statusText, urgencyText, ageText) { - return m.renderUltraSearchLine(line, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("252")), re) + return m.renderUltraSearchLine(line, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("253")), re) } - id := m.ultraStyledText(re, lipgloss.NewStyle().Bold(true), idText) + // Render header fields with distinct colors; no background so unselected cards stay on black. + id := m.ultraStyledText(re, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("253")), idText) priority := m.ultraStyledText(re, ultraPriorityStyle(m.theme, t.Priority), priorityText) - status := m.ultraStyledText(re, lipgloss.NewStyle().Foreground(lipgloss.Color("252")), statusText) - urgency := m.ultraStyledText(re, lipgloss.NewStyle().Foreground(lipgloss.Color("252")), urgencyText) - age := m.ultraStyledText(re, lipgloss.NewStyle().Foreground(lipgloss.Color("252")), ageText) + status := m.ultraStyledText(re, lipgloss.NewStyle().Foreground(lipgloss.Color("246")), statusText) + urgency := m.ultraStyledText(re, lipgloss.NewStyle().Foreground(lipgloss.Color("214")), urgencyText) // amber for urgency score + age := m.ultraStyledText(re, lipgloss.NewStyle().Foreground(lipgloss.Color("240")), ageText) return strings.Join([]string{id, priority, status, urgency, age}, " | ") } @@ -663,7 +663,7 @@ func (m *Model) renderUltraMetaWithRegex(t task.Task, width int, re *regexp.Rege " | ", ) if re != nil && re.MatchString(line) && !ultraRegexMatchesAny(re, "project:", project, "tags:", tags, "due:", due, "recur:", recur, "start:", start) { - return m.renderUltraSearchLine(line, lipgloss.NewStyle().Foreground(lipgloss.Color("252")), re) + return m.renderUltraSearchLine(line, lipgloss.NewStyle().Foreground(lipgloss.Color("253")), re) } parts := []string{ @@ -677,7 +677,8 @@ func (m *Model) renderUltraMetaWithRegex(t task.Task, width int, re *regexp.Rege } func (m *Model) renderUltraDescriptionWithRegex(t task.Task, width int, re *regexp.Regexp) string { - style := lipgloss.NewStyle().Foreground(lipgloss.Color("250")) + // Brighter foreground ("253") than annotations to give description visual priority. + style := lipgloss.NewStyle().Foreground(lipgloss.Color("253")) var lines []string for _, line := range m.ultraDescriptionLines(t, width) { if re != nil && re.MatchString(line) { @@ -694,7 +695,8 @@ func (m *Model) renderUltraAnnotationsWithRegex(t task.Task, width int, re *rege return "" } - style := lipgloss.NewStyle().Foreground(lipgloss.Color("248")) + // Dimmer than description ("244") to visually subordinate annotations. + style := lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Italic(true) for i, line := range lines { if re != nil && re.MatchString(line) { line = m.highlightMatches(line, re) @@ -704,6 +706,11 @@ func (m *Model) renderUltraAnnotationsWithRegex(t task.Task, width int, re *rege return strings.Join(lines, "\n") } +// ultraSeparator returns a full-width dim line used between cards. +func ultraSeparator(width int) string { + return lipgloss.NewStyle().Foreground(lipgloss.Color("237")).Render(strings.Repeat("─", width)) +} + func (m *Model) ultraRenderCards(tasks []task.Task, width, selected, start, cardBudget int) []string { if start < 0 { start = 0 @@ -712,6 +719,7 @@ func (m *Model) ultraRenderCards(tasks []task.Task, width, selected, start, card start = len(tasks) } + sep := ultraSeparator(width) var lines []string used := 0 for i := start; i < len(tasks); i++ { @@ -721,11 +729,12 @@ func (m *Model) ultraRenderCards(tasks []task.Task, width, selected, start, card } cardHeight := lipgloss.Height(card) + // Account for separator line (1 line) between cards. if len(lines) > 0 { if used+1+cardHeight > cardBudget { break } - lines = append(lines, "") + lines = append(lines, sep) used++ } else if cardHeight > cardBudget { break @@ -772,16 +781,18 @@ func ultraCardStyle(theme Theme, width int, selected, blink bool) lipgloss.Style } func ultraPriorityStyle(theme Theme, priority string) lipgloss.Style { - style := lipgloss.NewStyle().Width(1) + // Width(3) so the badge reads as a pill rather than a single character. + style := lipgloss.NewStyle().Width(3).Align(lipgloss.Center).Bold(true).Foreground(lipgloss.Color("255")) switch priority { case "H": - style = style.Background(lipgloss.Color(theme.PrioHighBG)) + return style.Background(lipgloss.Color(theme.PrioHighBG)) case "M": - style = style.Background(lipgloss.Color(theme.PrioMedBG)) + return style.Background(lipgloss.Color(theme.PrioMedBG)) case "L": - style = style.Background(lipgloss.Color(theme.PrioLowBG)) + return style.Background(lipgloss.Color(theme.PrioLowBG)) } - return style + // No priority — render dimly without a badge background. + return lipgloss.NewStyle().Foreground(lipgloss.Color("240")) } func ultraJoinSections(sections ...string) string { @@ -906,6 +917,22 @@ func (m *Model) handleUltraMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { return m.handleToggleHelp() case "q", "esc": return m.handleQuitOrEscape() + case "u": + // Toggle back to the traditional table view (only available when not + // started via --ultra, where there is no table view to return to). + if !m.ultraStartup { + m.ultraClearFocusedID() + m.showUltra = false + m.ultraSearchRegex = nil + m.ultraFiltered = nil + m.ultraSearchInput.SetValue("") + // Sync the table cursor to the task we were on in ultra mode. + tasks := m.ultraTaskList() + if m.ultraCursor >= 0 && m.ultraCursor < len(tasks) { + m.tbl.SetCursor(m.ultraCursor) + } + return m, nil + } case "/": m.ultraSearching = true m.ultraSearchInput.SetValue("") |
