From 8f6ec4a79f1b1e153c96948067a382072febfec5 Mon Sep 17 00:00:00 2001 From: Rasmus Wejlgaard Date: Sat, 8 Nov 2025 14:47:51 +0000 Subject: [PATCH] readme and keybindings improvements --- .imgs/priority_prompt.png | Bin 0 -> 19310 bytes README.md | 3 +- internal/ui/app.go | 32 +++++----- internal/ui/modes.go | 43 +++++++++++++- internal/ui/views.go | 119 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 .imgs/priority_prompt.png diff --git a/.imgs/priority_prompt.png b/.imgs/priority_prompt.png new file mode 100644 index 0000000000000000000000000000000000000000..f62c69053053e2461ae4c1987c304ba363c345a1 GIT binary patch literal 19310 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 WZ+b91SQq>WL%4Vzp&Y(&>wf{TG)!aw literal 0 HcmV?d00001 diff --git a/README.md b/README.md index f5d4f47..f863ecf 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ org # Opens ./todo.org by default | `p` | Set priority | | `e` | Set effort | | `r` | Toggle reorder mode | -| `shift+↑/↓` | Move item up/down (in reorder mode) | +| `shift+↑/↓` | Move item up/down | | `ctrl+s` | Save | | `?` | Toggle help | | `q` or `ctrl+c` | Quit | @@ -89,6 +89,7 @@ Changes are automatically saved when you quit the application. ### Prompts ![capture](./.imgs/capture_prompt.png) ![delete](./.imgs/delete_prompt.png) +![priority](./.imgs/priority_prompt.png) ## File Format diff --git a/internal/ui/app.go b/internal/ui/app.go index b5968d7..a9a3b8d 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -22,24 +22,26 @@ const ( modeSetDeadline modeSetPriority modeSetEffort + modeHelp ) type uiModel struct { - orgFile *model.OrgFile - cursor int - scrollOffset int // Track the scroll position - mode viewMode - help help.Model - keys keyMap - width int - height int - statusMsg string - statusExpiry time.Time - editingItem *model.Item - textarea textarea.Model - textinput textinput.Model - itemToDelete *model.Item - reorderMode bool + orgFile *model.OrgFile + cursor int + scrollOffset int // Track the scroll position + helpScroll int // Track scroll position in help mode + mode viewMode + help help.Model + keys keyMap + width int + height int + statusMsg string + statusExpiry time.Time + editingItem *model.Item + textarea textarea.Model + textinput textinput.Model + itemToDelete *model.Item + reorderMode bool } func initialModel(orgFile *model.OrgFile) uiModel { diff --git a/internal/ui/modes.go b/internal/ui/modes.go index 97df9b4..cd5b518 100644 --- a/internal/ui/modes.go +++ b/internal/ui/modes.go @@ -30,6 +30,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.updateSetPriority(msg) case modeSetEffort: return m.updateSetEffort(msg) + case modeHelp: + return m.updateHelp(msg) } switch msg := msg.(type) { @@ -48,7 +50,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit case key.Matches(msg, m.keys.Help): - m.help.ShowAll = !m.help.ShowAll + m.mode = modeHelp + m.helpScroll = 0 // Reset scroll when entering help return m, nil case key.Matches(msg, m.keys.Up): @@ -733,3 +736,41 @@ func (m *uiModel) swapItems(item1, item2 *model.Item) { } swapInList(m.orgFile.Items) } + +func (m uiModel) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + + case tea.KeyMsg: + switch msg.String() { + case "?", "esc", "q": + m.mode = modeList + m.helpScroll = 0 // Reset scroll when exiting + return m, nil + case "up", "k": + if m.helpScroll > 0 { + m.helpScroll-- + } + return m, nil + case "down", "j": + m.helpScroll++ + // The view will handle clamping to max scroll + return m, nil + case "pageup": + m.helpScroll -= 10 + if m.helpScroll < 0 { + m.helpScroll = 0 + } + return m, nil + case "pagedown": + m.helpScroll += 10 + return m, nil + case "home", "g": + m.helpScroll = 0 + return m, nil + } + } + return m, nil +} diff --git a/internal/ui/views.go b/internal/ui/views.go index 34ec0ec..042b8d2 100644 --- a/internal/ui/views.go +++ b/internal/ui/views.go @@ -85,6 +85,8 @@ func (m uiModel) View() string { return m.viewSetPriority() case modeSetEffort: return m.viewSetEffort() + case modeHelp: + return m.viewHelp() } // Build footer (status + help) @@ -421,6 +423,123 @@ func (m uiModel) viewSetEffort() string { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog) } +func (m uiModel) viewHelp() string { + // Build the full help content first + var lines []string + + // Title + lines = append(lines, titleStyle.Render("Keybindings Help")) + lines = append(lines, "") + + // Group bindings by category + navigationBindings := []key.Binding{m.keys.Up, m.keys.Down, m.keys.Left, m.keys.Right} + itemBindings := []key.Binding{m.keys.ToggleFold, m.keys.EditNotes, m.keys.CycleState} + taskBindings := []key.Binding{m.keys.Capture, m.keys.AddSubTask, m.keys.Delete} + timeBindings := []key.Binding{m.keys.ClockIn, m.keys.ClockOut, m.keys.SetDeadline, m.keys.SetEffort} + organizationBindings := []key.Binding{m.keys.SetPriority, m.keys.ShiftUp, m.keys.ShiftDown, m.keys.ToggleReorder} + viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Save, m.keys.Help, m.keys.Quit} + + // Helper function to render a binding + renderBinding := func(b key.Binding) string { + keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).Bold(true) + descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245")) + help := b.Help() + return fmt.Sprintf(" %s %s", keyStyle.Render(help.Key), descStyle.Render(help.Desc)) + } + + // Render categories + categoryStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) + + lines = append(lines, categoryStyle.Render("Navigation")) + for _, binding := range navigationBindings { + lines = append(lines, renderBinding(binding)) + } + lines = append(lines, "") + + lines = append(lines, categoryStyle.Render("Item Actions")) + for _, binding := range itemBindings { + lines = append(lines, renderBinding(binding)) + } + lines = append(lines, "") + + lines = append(lines, categoryStyle.Render("Task Management")) + for _, binding := range taskBindings { + lines = append(lines, renderBinding(binding)) + } + lines = append(lines, "") + + lines = append(lines, categoryStyle.Render("Time Tracking")) + for _, binding := range timeBindings { + lines = append(lines, renderBinding(binding)) + } + lines = append(lines, "") + + lines = append(lines, categoryStyle.Render("Organization")) + for _, binding := range organizationBindings { + lines = append(lines, renderBinding(binding)) + } + lines = append(lines, "") + + lines = append(lines, categoryStyle.Render("View & System")) + for _, binding := range viewBindings { + lines = append(lines, renderBinding(binding)) + } + lines = append(lines, "") + + // Calculate visible area + footerLines := 2 // Footer text + availableHeight := m.height - footerLines + if availableHeight < 5 { + availableHeight = 5 + } + + totalLines := len(lines) + + // Determine which lines to show based on scroll offset + startLine := m.helpScroll + endLine := startLine + availableHeight + if endLine > totalLines { + endLine = totalLines + } + if startLine >= totalLines { + startLine = totalLines - 1 + if startLine < 0 { + startLine = 0 + } + } + + // Build visible content + var content strings.Builder + for i := startLine; i < endLine && i < len(lines); i++ { + content.WriteString(lines[i]) + content.WriteString("\n") + } + + // Add scroll indicators and footer + var footer strings.Builder + if startLine > 0 || endLine < totalLines { + scrollInfo := fmt.Sprintf("(Scroll: %d-%d of %d lines)", startLine+1, endLine, totalLines) + footer.WriteString(statusStyle.Render(scrollInfo)) + footer.WriteString(" ") + } + footer.WriteString(statusStyle.Render("↑/↓ scroll • ? or ESC to close")) + + // Combine content and footer + var result strings.Builder + result.WriteString(content.String()) + + // Add padding if needed + currentHeight := lipgloss.Height(content.String()) + paddingNeeded := availableHeight - currentHeight + if paddingNeeded > 0 { + result.WriteString(strings.Repeat("\n", paddingNeeded)) + } + + result.WriteString(footer.String()) + + return result.String() +} + func (m uiModel) viewEditMode() string { var b strings.Builder