readme and keybindings improvements

This commit is contained in:
Rasmus Wejlgaard 2025-11-08 14:47:51 +00:00
parent 097703beda
commit 8f6ec4a79f
5 changed files with 180 additions and 17 deletions

BIN
.imgs/priority_prompt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -70,7 +70,7 @@ org # Opens ./todo.org by default
| `p` | Set priority | | `p` | Set priority |
| `e` | Set effort | | `e` | Set effort |
| `r` | Toggle reorder mode | | `r` | Toggle reorder mode |
| `shift+↑/↓` | Move item up/down (in reorder mode) | | `shift+↑/↓` | Move item up/down |
| `ctrl+s` | Save | | `ctrl+s` | Save |
| `?` | Toggle help | | `?` | Toggle help |
| `q` or `ctrl+c` | Quit | | `q` or `ctrl+c` | Quit |
@ -89,6 +89,7 @@ Changes are automatically saved when you quit the application.
### Prompts ### Prompts
![capture](./.imgs/capture_prompt.png) ![capture](./.imgs/capture_prompt.png)
![delete](./.imgs/delete_prompt.png) ![delete](./.imgs/delete_prompt.png)
![priority](./.imgs/priority_prompt.png)
## File Format ## File Format

View file

@ -22,24 +22,26 @@ const (
modeSetDeadline modeSetDeadline
modeSetPriority modeSetPriority
modeSetEffort modeSetEffort
modeHelp
) )
type uiModel struct { type uiModel struct {
orgFile *model.OrgFile orgFile *model.OrgFile
cursor int cursor int
scrollOffset int // Track the scroll position scrollOffset int // Track the scroll position
mode viewMode helpScroll int // Track scroll position in help mode
help help.Model mode viewMode
keys keyMap help help.Model
width int keys keyMap
height int width int
statusMsg string height int
statusExpiry time.Time statusMsg string
editingItem *model.Item statusExpiry time.Time
textarea textarea.Model editingItem *model.Item
textinput textinput.Model textarea textarea.Model
itemToDelete *model.Item textinput textinput.Model
reorderMode bool itemToDelete *model.Item
reorderMode bool
} }
func initialModel(orgFile *model.OrgFile) uiModel { func initialModel(orgFile *model.OrgFile) uiModel {

View file

@ -30,6 +30,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateSetPriority(msg) return m.updateSetPriority(msg)
case modeSetEffort: case modeSetEffort:
return m.updateSetEffort(msg) return m.updateSetEffort(msg)
case modeHelp:
return m.updateHelp(msg)
} }
switch msg := msg.(type) { switch msg := msg.(type) {
@ -48,7 +50,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit return m, tea.Quit
case key.Matches(msg, m.keys.Help): 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 return m, nil
case key.Matches(msg, m.keys.Up): case key.Matches(msg, m.keys.Up):
@ -733,3 +736,41 @@ func (m *uiModel) swapItems(item1, item2 *model.Item) {
} }
swapInList(m.orgFile.Items) 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
}

View file

@ -85,6 +85,8 @@ func (m uiModel) View() string {
return m.viewSetPriority() return m.viewSetPriority()
case modeSetEffort: case modeSetEffort:
return m.viewSetEffort() return m.viewSetEffort()
case modeHelp:
return m.viewHelp()
} }
// Build footer (status + help) // 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) 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 { func (m uiModel) viewEditMode() string {
var b strings.Builder var b strings.Builder