diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-28 11:54:03 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-28 11:54:03 +0300 |
| commit | 99280920a5bc7d655c4ca8f1f06f2c66b208986f (patch) | |
| tree | 9ba72a66ee805053a18d0e71a912c99849ab8d15 | |
| parent | d06b179332e82635f6a7c8366e51fb5b421a7c2c (diff) | |
feat: make help screen scrollable and theme-aware
- Added viewport for help screen to enable scrolling on small terminals
- Removed line spacing between help section headers and content for compactness
- Updated help screen to follow current color theme
- Added support for theme changes in help view (including disco mode)
- Added keyboard navigation for help viewport (up/down, page up/down, home/end)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
| -rw-r--r-- | internal/ui/keyhandlers.go | 32 | ||||
| -rw-r--r-- | internal/ui/table.go | 224 | ||||
| -rwxr-xr-x | tasksamurai | bin | 5998672 -> 6024296 bytes |
3 files changed, 209 insertions, 47 deletions
diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go index 2ff0ed7..428f28e 100644 --- a/internal/ui/keyhandlers.go +++ b/internal/ui/keyhandlers.go @@ -7,6 +7,7 @@ import ( "time" "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "codeberg.org/snonux/tasksamurai/internal/task" @@ -25,6 +26,24 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.handleNextHelpSearchMatch() case "N": return m.handlePrevHelpSearchMatch() + case "up", "k": + m.helpViewport.LineUp(1) + return m, nil + case "down", "j": + m.helpViewport.LineDown(1) + return m, nil + case "pgup", "b": + m.helpViewport.ViewUp() + return m, nil + case "pgdown", " ": + m.helpViewport.ViewDown() + return m, nil + case "g", "home": + m.helpViewport.GotoTop() + return m, nil + case "G", "end": + m.helpViewport.GotoBottom() + return m, nil default: // Ignore other keys in help mode return m, nil @@ -92,6 +111,17 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *Model) handleToggleHelp() (tea.Model, tea.Cmd) { m.showHelp = true + // Initialize help viewport with proper dimensions + width := m.tbl.Width() - 4 // Account for padding + height := m.windowHeight - 6 // Leave room for status bars and search input + if width <= 0 { + width = 80 // Default width + } + if height <= 0 { + height = 20 // Default height + } + m.helpViewport = viewport.New(width, height) + m.helpViewport.SetContent("") // Content will be set in renderHelpScreen return m, nil } @@ -116,6 +146,8 @@ func (m *Model) handleQuitOrEscape() (tea.Model, tea.Cmd) { m.helpSearchMatches = nil m.helpSearchIndex = 0 m.helpSearchInput.SetValue("") + // Reset help viewport + m.helpViewport = viewport.Model{} return m, nil } if m.searchRegex != nil { diff --git a/internal/ui/table.go b/internal/ui/table.go index 74d5440..a0a82c5 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -13,6 +13,7 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -135,6 +136,9 @@ type Model struct { detailBlinkCount int // Number of blinks remaining detailDescEditing bool // Whether we're editing description in detail view detailDescTempFile string // Temp file path for description editing + + // Help view fields + helpViewport viewport.Model } // editDoneMsg is emitted when the external editor process finishes. @@ -432,6 +436,17 @@ func (m *Model) handleWindowResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { m.windowHeight = msg.Height m.computeColumnWidths() m.updateTableHeight() + + // Update help viewport if active + if m.showHelp && m.helpViewport.Width > 0 { + width := msg.Width - 4 + height := msg.Height - 6 + if width > 0 && height > 0 { + m.helpViewport.Width = width + m.helpViewport.Height = height + } + } + return m, nil } @@ -543,6 +558,8 @@ func (m *Model) handleBlinkMsg() (tea.Model, tea.Cmd) { // View renders the table UI. func (m Model) View() string { if m.showHelp { + // Update help content before rendering + m.updateHelpContent() return m.renderHelpScreen() } if m.showTaskDetail { @@ -617,79 +634,192 @@ func (m Model) View() string { return view } -// renderHelpScreen renders the help screen with optional search highlighting -func (m Model) renderHelpScreen() string { - helpLines := m.getHelpLines() +// updateHelpContent updates the help viewport content +func (m *Model) updateHelpContent() { + content := m.buildHelpContent() + m.helpViewport.SetContent(content) +} + +// buildHelpContent builds the help content +func (m Model) buildHelpContent() string { + // Create styles using theme colors + headerStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color(m.theme.HeaderFG)). + Background(lipgloss.Color(m.theme.SelectedBG)). + Padding(0, 1) + + keyStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color(m.theme.SelectedFG)) + + descStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("250")) // Light gray for readability + // Build help content with styled headers + var sections []string + + // Navigation section + sections = append(sections, headerStyle.Render("Navigation"), + m.formatHelpLine("↑/k, ↓/j", "move up/down", keyStyle, descStyle), + m.formatHelpLine("←/h, →/l", "move left/right", keyStyle, descStyle), + m.formatHelpLine("g/Home, G/End", "go to start/end", keyStyle, descStyle), + m.formatHelpLine("pgup/pgdn, b", "page up/down", keyStyle, descStyle), + "") + + // Task Management section + sections = append(sections, headerStyle.Render("Task Management"), + m.formatHelpLine("Enter", "view task details", keyStyle, descStyle), + m.formatHelpLine("+", "add new task", keyStyle, descStyle), + m.formatHelpLine("E", "edit entire task", keyStyle, descStyle), + m.formatHelpLine("d", "mark task done", keyStyle, descStyle), + m.formatHelpLine("U", "undo last done", keyStyle, descStyle), + m.formatHelpLine("s", "start/stop task", keyStyle, descStyle), + "") + + // Task Fields section + sections = append(sections, headerStyle.Render("Task Fields"), + m.formatHelpLine("i", "edit current field", keyStyle, descStyle), + m.formatHelpLine("p", "set priority", keyStyle, descStyle), + m.formatHelpLine("w, W", "set/remove due date", keyStyle, descStyle), + m.formatHelpLine("r", "set random due date", keyStyle, descStyle), + m.formatHelpLine("R", "edit recurrence", keyStyle, descStyle), + m.formatHelpLine("t", "edit tags", keyStyle, descStyle), + m.formatHelpLine("a, A", "add/replace annotations", keyStyle, descStyle), + m.formatHelpLine("o", "open URL from description", keyStyle, descStyle), + "") + + // View & Search section + sections = append(sections, headerStyle.Render("View & Search"), + m.formatHelpLine("f", "change filter", keyStyle, descStyle), + m.formatHelpLine("/", "search", keyStyle, descStyle), + m.formatHelpLine("n, N", "next/previous match", keyStyle, descStyle), + m.formatHelpLine("space", "refresh tasks", keyStyle, descStyle), + "") + + // Appearance section + sections = append(sections, headerStyle.Render("Appearance"), + m.formatHelpLine("c, C", "random/reset theme", keyStyle, descStyle), + m.formatHelpLine("x", "toggle disco mode", keyStyle, descStyle), + "") + + // General section + sections = append(sections, headerStyle.Render("General"), + m.formatHelpLine("H", "toggle help", keyStyle, descStyle), + m.formatHelpLine("ESC", "close dialogs/cancel", keyStyle, descStyle), + m.formatHelpLine("q", "quit", keyStyle, descStyle)) + // Apply search highlighting if active if m.helpSearchRegex != nil { - for i, line := range helpLines { + for i, line := range sections { if m.helpSearchRegex.MatchString(line) { - // Highlight matching lines - matches := m.helpSearchRegex.FindAllStringIndex(line, -1) - highlighted := line - offset := 0 - for _, match := range matches { - start := match[0] + offset - end := match[1] + offset - style := lipgloss.NewStyle(). - Background(lipgloss.Color(m.theme.SearchBG)). - Foreground(lipgloss.Color(m.theme.SearchFG)) - highlighted = highlighted[:start] + style.Render(highlighted[start:end]) + highlighted[end:] - offset += len(style.Render(highlighted[start:end])) - (end - start) - } - helpLines[i] = highlighted + sections[i] = m.highlightHelpLine(line) } } } + + // Join all sections + return strings.Join(sections, "\n") +} - // Center all lines - for i, l := range helpLines { - helpLines[i] = centerLines(l, m.tbl.Width()) - } - - result := lipgloss.JoinVertical(lipgloss.Top, helpLines...) +// renderHelpScreen renders the help screen with optional search highlighting +func (m Model) renderHelpScreen() string { + containerStyle := lipgloss.NewStyle(). + Padding(1, 2) + + // Render viewport + viewportView := m.helpViewport.View() + + result := containerStyle.Render(viewportView) // Add search input at the bottom if in help search mode if m.helpSearching { + searchStyle := lipgloss.NewStyle(). + Padding(0, 2) result = lipgloss.JoinVertical(lipgloss.Left, result, - m.helpSearchInput.View(), + searchStyle.Render(m.helpSearchInput.View()), ) } return result } -// getHelpLines returns the help lines as a slice +// formatHelpLine formats a help line with key and description styling +func (m Model) formatHelpLine(key, desc string, keyStyle, descStyle lipgloss.Style) string { + // Pad key to consistent width for alignment + paddedKey := fmt.Sprintf("%-12s", key) + return keyStyle.Render(paddedKey) + " " + descStyle.Render(desc) +} + +// highlightHelpLine applies search highlighting to a help line +func (m Model) highlightHelpLine(line string) string { + if m.helpSearchRegex == nil { + return line + } + + matches := m.helpSearchRegex.FindAllStringIndex(line, -1) + if len(matches) == 0 { + return line + } + + highlighted := line + offset := 0 + highlightStyle := lipgloss.NewStyle(). + Background(lipgloss.Color(m.theme.SearchBG)). + Foreground(lipgloss.Color(m.theme.SearchFG)) + + for _, match := range matches { + start := match[0] + offset + end := match[1] + offset + highlighted = highlighted[:start] + highlightStyle.Render(highlighted[start:end]) + highlighted[end:] + offset += len(highlightStyle.Render(highlighted[start:end])) - (end - start) + } + + return highlighted +} + +// getHelpLines returns searchable help content as plain text lines func (m Model) getHelpLines() []string { return []string{ - m.tbl.HelpView(), - "enter/i: edit or expand cell", - "E: edit task", - "+: add task", - "s: toggle start/stop", + "Navigation", + "↑/k, ↓/j: move up/down", + "←/h, →/l: move left/right", + "g/Home, G/End: go to start/end", + "pgup/pgdn, b: page up/down", + "", + "Task Management", + "Enter: view task details", + "+: add new task", + "E: edit entire task", "d: mark task done", - "o: open URL", - "U: undo done", - "w: set due date (when)", - "W: remove due date", - "r: random due date", - "R: edit recurrence", - "a: annotate task", - "A: replace annotations", + "U: undo last done", + "s: start/stop task", + "", + "Task Fields", + "i: edit current field", "p: set priority", - "f: change filter", + "w, W: set/remove due date", + "r: set random due date", + "R: edit recurrence", "t: edit tags", - "c: random theme", - "C: reset theme", - "x: toggle disco mode", + "a, A: add/replace annotations", + "o: open URL from description", + "", + "View & Search", + "f: change filter", + "/: search", + "n, N: next/previous match", "space: refresh tasks", - "/, ?: search", - "n/N: next/prev search match", - "esc: close help/search", + "", + "Appearance", + "c, C: random/reset theme", + "x: toggle disco mode", + "", + "General", + "H: toggle help", + "ESC: close dialogs/cancel", "q: quit", - "H: help", } } diff --git a/tasksamurai b/tasksamurai Binary files differindex d845745..dff17f4 100755 --- a/tasksamurai +++ b/tasksamurai |
