Compare commits

..

3 commits

Author SHA1 Message Date
Vitaliy Sh
7bc00d6891
feat: ToggleFoldAll (#18) 2026-02-17 09:50:37 +00:00
Vitaliy Sh
fce607e29d
feat: SetScheduled (#17) 2026-02-10 13:29:23 +00:00
c858e70d07
chore: update readme for -c --capture (#16) 2025-11-17 21:44:37 +00:00
6 changed files with 270 additions and 130 deletions

View file

@ -23,6 +23,9 @@ org tasks.org # Open specific org file
org /path/to/work.org # Open specific org file with path org /path/to/work.org # Open specific org file with path
org -m # Multi-file: Load all .org files in current directory org -m # Multi-file: Load all .org files in current directory
org -m /path/to/dir # Multi-file: Load all .org files in specified directory org -m /path/to/dir # Multi-file: Load all .org files in specified directory
org -c # Quick capture mode
org -c "Task description" # Quick capture with pre-filled text
echo "Task" | org # Pipe text to capture
``` ```
### Single-File Mode (Default) ### Single-File Mode (Default)
@ -35,6 +38,20 @@ org tasks.org # Opens tasks.org
org ~/work/project.org # Opens specific file org ~/work/project.org # Opens specific file
``` ```
### Quick Capture Mode
Use the `-c` or `--capture` flag to quickly add tasks without navigating through the UI:
```bash
org -c # Open directly in capture mode
org -c "Buy groceries" # Capture with pre-filled text
org -c "Write report" tasks.org # Capture to specific file
echo "Meeting notes" | org # Pipe text to capture
echo "Task" | org ~/work.org # Pipe to specific file
```
This is perfect for quickly capturing tasks from scripts, terminal workflows, or shell aliases. The capture mode skips the need to press 'c' once inside the application, making it faster to add quick TODO items.
### Multi-File Mode ### Multi-File Mode
Use the `-m` or `--multi` flag to load all `.org` files in a directory as top-level items. Each file appears as a top-level item in the interface, with its contents nested underneath. Changes made to items are automatically saved back to their respective files. Use the `-m` or `--multi` flag to load all `.org` files in a directory as top-level items. Each file appears as a top-level item in the interface, with its contents nested underneath. Changes made to items are automatically saved back to their respective files.
@ -92,6 +109,7 @@ Feel free to fork and create a pull request if there's any features missing for
- **Syntax Highlighting**: Code blocks are automatically highlighted (supports both ```lang and #+BEGIN_SRC formats) - **Syntax Highlighting**: Code blocks are automatically highlighted (supports both ```lang and #+BEGIN_SRC formats)
- **Markdown Support**: Use markdown-style code blocks in your notes - **Markdown Support**: Use markdown-style code blocks in your notes
- **Drawer Management**: LOGBOOK and PROPERTIES drawers are automatically filtered in list view - **Drawer Management**: LOGBOOK and PROPERTIES drawers are automatically filtered in list view
- **Fold/Unfold All**: Fold/Unfold all items with shift+tab
### Keybindings ### Keybindings
@ -101,6 +119,7 @@ Feel free to fork and create a pull request if there's any features missing for
| `←/h`, `→/l` | Cycle state backward/forward | | `←/h`, `→/l` | Cycle state backward/forward |
| `t` or `space` | Cycle TODO state | | `t` or `space` | Cycle TODO state |
| `tab` | Fold/unfold item | | `tab` | Fold/unfold item |
| `shift+tab` | Fold/Unfold all items |
| `enter` | Edit notes | | `enter` | Edit notes |
| `c` | Capture new TODO | | `c` | Capture new TODO |
| `s` | Add sub-task | | `s` | Add sub-task |
@ -111,6 +130,7 @@ Feel free to fork and create a pull request if there's any features missing for
| `i` | Clock in | | `i` | Clock in |
| `o` | Clock out | | `o` | Clock out |
| `d` | Set deadline | | `d` | Set deadline |
| `S` | Set scheduled date |
| `p` | Set priority | | `p` | Set priority |
| `e` | Set effort | | `e` | Set effort |
| `r` | Toggle reorder mode | | `r` | Toggle reorder mode |

View file

@ -31,6 +31,7 @@ type KeybindingsConfig struct {
Rename []string `toml:"rename"` Rename []string `toml:"rename"`
CycleState []string `toml:"cycle_state"` CycleState []string `toml:"cycle_state"`
ToggleFold []string `toml:"toggle_fold"` ToggleFold []string `toml:"toggle_fold"`
ToggleFoldAll []string `toml:"toggle_fold_all"`
EditNotes []string `toml:"edit_notes"` EditNotes []string `toml:"edit_notes"`
ToggleView []string `toml:"toggle_view"` ToggleView []string `toml:"toggle_view"`
Capture []string `toml:"capture"` Capture []string `toml:"capture"`
@ -41,6 +42,7 @@ type KeybindingsConfig struct {
ClockIn []string `toml:"clock_in"` ClockIn []string `toml:"clock_in"`
ClockOut []string `toml:"clock_out"` ClockOut []string `toml:"clock_out"`
SetDeadline []string `toml:"set_deadline"` SetDeadline []string `toml:"set_deadline"`
SetScheduled []string `toml:"set_scheduled"`
SetPriority []string `toml:"set_priority"` SetPriority []string `toml:"set_priority"`
SetEffort []string `toml:"set_effort"` SetEffort []string `toml:"set_effort"`
Help []string `toml:"help"` Help []string `toml:"help"`
@ -114,6 +116,7 @@ func DefaultConfig() *Config {
Rename: []string{"R"}, Rename: []string{"R"},
CycleState: []string{"t", " "}, CycleState: []string{"t", " "},
ToggleFold: []string{"tab"}, ToggleFold: []string{"tab"},
ToggleFoldAll: []string{"shift+tab", "backtab"},
EditNotes: []string{"enter"}, EditNotes: []string{"enter"},
ToggleView: []string{"a"}, ToggleView: []string{"a"},
Capture: []string{"c"}, Capture: []string{"c"},
@ -124,6 +127,7 @@ func DefaultConfig() *Config {
ClockIn: []string{"i"}, ClockIn: []string{"i"},
ClockOut: []string{"o"}, ClockOut: []string{"o"},
SetDeadline: []string{"d"}, SetDeadline: []string{"d"},
SetScheduled: []string{"S"},
SetPriority: []string{"p"}, SetPriority: []string{"p"},
SetEffort: []string{"e"}, SetEffort: []string{"e"},
Help: []string{"?"}, Help: []string{"?"},
@ -282,6 +286,9 @@ func (c *Config) fillDefaults() {
if len(c.Keybindings.ToggleFold) == 0 { if len(c.Keybindings.ToggleFold) == 0 {
c.Keybindings.ToggleFold = defaults.Keybindings.ToggleFold c.Keybindings.ToggleFold = defaults.Keybindings.ToggleFold
} }
if len(c.Keybindings.ToggleFoldAll) == 0 {
c.Keybindings.ToggleFoldAll = defaults.Keybindings.ToggleFoldAll
}
if len(c.Keybindings.EditNotes) == 0 { if len(c.Keybindings.EditNotes) == 0 {
c.Keybindings.EditNotes = defaults.Keybindings.EditNotes c.Keybindings.EditNotes = defaults.Keybindings.EditNotes
} }
@ -312,6 +319,9 @@ func (c *Config) fillDefaults() {
if len(c.Keybindings.SetDeadline) == 0 { if len(c.Keybindings.SetDeadline) == 0 {
c.Keybindings.SetDeadline = defaults.Keybindings.SetDeadline c.Keybindings.SetDeadline = defaults.Keybindings.SetDeadline
} }
if len(c.Keybindings.SetScheduled) == 0 {
c.Keybindings.SetScheduled = defaults.Keybindings.SetScheduled
}
if len(c.Keybindings.SetPriority) == 0 { if len(c.Keybindings.SetPriority) == 0 {
c.Keybindings.SetPriority = defaults.Keybindings.SetPriority c.Keybindings.SetPriority = defaults.Keybindings.SetPriority
} }
@ -517,6 +527,8 @@ func (c *Config) UpdateKeybinding(action string, keys []string) error {
c.Keybindings.CycleState = keys c.Keybindings.CycleState = keys
case "toggle_fold": case "toggle_fold":
c.Keybindings.ToggleFold = keys c.Keybindings.ToggleFold = keys
case "toggle_fold_all":
c.Keybindings.ToggleFoldAll = keys
case "edit_notes": case "edit_notes":
c.Keybindings.EditNotes = keys c.Keybindings.EditNotes = keys
case "capture": case "capture":
@ -546,33 +558,35 @@ func (c *Config) UpdateKeybinding(action string, keys []string) error {
// GetAllKeybindings returns a map of all keybindings // GetAllKeybindings returns a map of all keybindings
func (c *Config) GetAllKeybindings() map[string][]string { func (c *Config) GetAllKeybindings() map[string][]string {
return map[string][]string{ return map[string][]string{
"up": c.Keybindings.Up, "up": c.Keybindings.Up,
"down": c.Keybindings.Down, "down": c.Keybindings.Down,
"left": c.Keybindings.Left, "left": c.Keybindings.Left,
"right": c.Keybindings.Right, "right": c.Keybindings.Right,
"shift_up": c.Keybindings.ShiftUp, "shift_up": c.Keybindings.ShiftUp,
"shift_down": c.Keybindings.ShiftDown, "shift_down": c.Keybindings.ShiftDown,
"shift_left": c.Keybindings.ShiftLeft, "shift_left": c.Keybindings.ShiftLeft,
"shift_right": c.Keybindings.ShiftRight, "shift_right": c.Keybindings.ShiftRight,
"rename": c.Keybindings.Rename, "rename": c.Keybindings.Rename,
"cycle_state": c.Keybindings.CycleState, "cycle_state": c.Keybindings.CycleState,
"toggle_fold": c.Keybindings.ToggleFold, "toggle_fold": c.Keybindings.ToggleFold,
"edit_notes": c.Keybindings.EditNotes, "toggle_fold_all": c.Keybindings.ToggleFoldAll,
"toggle_view": c.Keybindings.ToggleView, "edit_notes": c.Keybindings.EditNotes,
"capture": c.Keybindings.Capture, "toggle_view": c.Keybindings.ToggleView,
"add_subtask": c.Keybindings.AddSubTask, "capture": c.Keybindings.Capture,
"delete": c.Keybindings.Delete, "add_subtask": c.Keybindings.AddSubTask,
"save": c.Keybindings.Save, "delete": c.Keybindings.Delete,
"toggle_reorder": c.Keybindings.ToggleReorder, "save": c.Keybindings.Save,
"clock_in": c.Keybindings.ClockIn, "toggle_reorder": c.Keybindings.ToggleReorder,
"clock_out": c.Keybindings.ClockOut, "clock_in": c.Keybindings.ClockIn,
"set_deadline": c.Keybindings.SetDeadline, "clock_out": c.Keybindings.ClockOut,
"set_priority": c.Keybindings.SetPriority, "set_deadline": c.Keybindings.SetDeadline,
"set_effort": c.Keybindings.SetEffort, "set_scheduled": c.Keybindings.SetScheduled,
"help": c.Keybindings.Help, "set_priority": c.Keybindings.SetPriority,
"quit": c.Keybindings.Quit, "set_effort": c.Keybindings.SetEffort,
"settings": c.Keybindings.Settings, "help": c.Keybindings.Help,
"tag_item": c.Keybindings.TagItem, "quit": c.Keybindings.Quit,
"settings": c.Keybindings.Settings,
"tag_item": c.Keybindings.TagItem,
} }
} }

View file

@ -22,6 +22,7 @@ const (
modeCapture modeCapture
modeAddSubTask modeAddSubTask
modeSetDeadline modeSetDeadline
modeSetScheduled
modeSetPriority modeSetPriority
modeSetEffort modeSetEffort
modeHelp modeHelp
@ -31,28 +32,28 @@ const (
) )
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
helpScroll int // Track scroll position in help mode helpScroll int // Track scroll position in help mode
mode viewMode mode viewMode
help help.Model help help.Model
keys keyMap keys keyMap
styles styleMap styles styleMap
config *config.Config config *config.Config
width int width int
height int height int
statusMsg string statusMsg string
statusExpiry time.Time statusExpiry time.Time
editingItem *model.Item editingItem *model.Item
textarea textarea.Model textarea textarea.Model
textinput textinput.Model textinput textinput.Model
itemToDelete *model.Item itemToDelete *model.Item
reorderMode bool reorderMode bool
settingsCursor int // Cursor position in settings view settingsCursor int // Cursor position in settings view
settingsScroll int // Scroll position in settings view settingsScroll int // Scroll position in settings view
settingsSection settingsSection // Current settings section/tab settingsSection settingsSection // Current settings section/tab
captureCursor int // Store cursor position when entering capture mode captureCursor int // Store cursor position when entering capture mode
} }
func InitialModel(orgFile *model.OrgFile, cfg *config.Config, captureMode bool, captureText string) uiModel { func InitialModel(orgFile *model.OrgFile, cfg *config.Config, captureMode bool, captureText string) uiModel {

View file

@ -26,11 +26,13 @@ type keyMap struct {
Delete key.Binding Delete key.Binding
Save key.Binding Save key.Binding
ToggleFold key.Binding ToggleFold key.Binding
ToggleFoldAll key.Binding
EditNotes key.Binding EditNotes key.Binding
ToggleReorder key.Binding ToggleReorder key.Binding
ClockIn key.Binding ClockIn key.Binding
ClockOut key.Binding ClockOut key.Binding
SetDeadline key.Binding SetDeadline key.Binding
SetScheduled key.Binding
SetPriority key.Binding SetPriority key.Binding
SetEffort key.Binding SetEffort key.Binding
Settings key.Binding Settings key.Binding
@ -86,6 +88,10 @@ func newKeyMapFromConfig(cfg *config.Config) keyMap {
key.WithKeys(kb.ToggleFold...), key.WithKeys(kb.ToggleFold...),
key.WithHelp(formatKeyHelp(kb.ToggleFold), "fold/unfold"), key.WithHelp(formatKeyHelp(kb.ToggleFold), "fold/unfold"),
), ),
ToggleFoldAll: key.NewBinding(
key.WithKeys(kb.ToggleFoldAll...),
key.WithHelp(formatKeyHelp(kb.ToggleFoldAll), "fold/unfold all"),
),
EditNotes: key.NewBinding( EditNotes: key.NewBinding(
key.WithKeys(kb.EditNotes...), key.WithKeys(kb.EditNotes...),
key.WithHelp(formatKeyHelp(kb.EditNotes), "edit notes"), key.WithHelp(formatKeyHelp(kb.EditNotes), "edit notes"),
@ -126,6 +132,10 @@ func newKeyMapFromConfig(cfg *config.Config) keyMap {
key.WithKeys(kb.SetDeadline...), key.WithKeys(kb.SetDeadline...),
key.WithHelp(formatKeyHelp(kb.SetDeadline), "set deadline"), key.WithHelp(formatKeyHelp(kb.SetDeadline), "set deadline"),
), ),
SetScheduled: key.NewBinding(
key.WithKeys(kb.SetScheduled...),
key.WithHelp(formatKeyHelp(kb.SetScheduled), "set scheduled"),
),
SetPriority: key.NewBinding( SetPriority: key.NewBinding(
key.WithKeys(kb.SetPriority...), key.WithKeys(kb.SetPriority...),
key.WithHelp(formatKeyHelp(kb.SetPriority), "set priority"), key.WithHelp(formatKeyHelp(kb.SetPriority), "set priority"),
@ -183,7 +193,7 @@ func (k keyMap) FullHelp() [][]key.Binding {
// This will be overridden by custom rendering in viewFullHelp // This will be overridden by custom rendering in viewFullHelp
return [][]key.Binding{ return [][]key.Binding{
{k.Up, k.Down, k.Left, k.Right}, {k.Up, k.Down, k.Left, k.Right},
{k.ToggleFold, k.EditNotes, k.ToggleReorder}, {k.ToggleFold, k.ToggleFoldAll, k.EditNotes, k.ToggleReorder},
{k.Capture, k.AddSubTask, k.Delete, k.Save}, {k.Capture, k.AddSubTask, k.Delete, k.Save},
{k.ToggleView, k.Help, k.Quit}, {k.ToggleView, k.Help, k.Quit},
} }
@ -193,9 +203,9 @@ func (k keyMap) FullHelp() [][]key.Binding {
func (k keyMap) getAllBindings() []key.Binding { func (k keyMap) getAllBindings() []key.Binding {
return []key.Binding{ return []key.Binding{
k.Up, k.Down, k.Left, k.Right, k.Up, k.Down, k.Left, k.Right,
k.ToggleFold, k.EditNotes, k.ToggleReorder, k.ToggleFold, k.ToggleFoldAll, k.EditNotes, k.ToggleReorder,
k.Capture, k.AddSubTask, k.Delete, k.Save, k.Capture, k.AddSubTask, k.Delete, k.Save,
k.ClockIn, k.ClockOut, k.SetDeadline, k.SetPriority, k.SetEffort, k.ClockIn, k.ClockOut, k.SetDeadline, k.SetScheduled, k.SetPriority, k.SetEffort,
k.TagItem, k.Settings, k.ToggleView, k.Help, k.Quit, k.TagItem, k.Settings, k.ToggleView, k.Help, k.Quit,
} }
} }

View file

@ -26,6 +26,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateAddSubTask(msg) return m.updateAddSubTask(msg)
case modeSetDeadline: case modeSetDeadline:
return m.updateSetDeadline(msg) return m.updateSetDeadline(msg)
case modeSetScheduled:
return m.updateSetScheduled(msg)
case modeSetPriority: case modeSetPriority:
return m.updateSetPriority(msg) return m.updateSetPriority(msg)
case modeSetEffort: case modeSetEffort:
@ -153,6 +155,46 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
case key.Matches(msg, m.keys.ToggleFoldAll):
if len(m.orgFile.Items) > 0 {
// Check if any top-level item is not folded
anyUnfolded := false
for _, item := range m.orgFile.Items {
if !item.Folded {
anyUnfolded = true
break
}
}
if anyUnfolded {
// Fold all items recursively (collapse all)
var foldAll func([]*model.Item)
foldAll = func(items []*model.Item) {
for _, item := range items {
item.Folded = true
if len(item.Children) > 0 {
foldAll(item.Children)
}
}
}
foldAll(m.orgFile.Items)
m.setStatus("All items folded")
} else {
// Unfold everything recursively
var unfoldAll func([]*model.Item)
unfoldAll = func(items []*model.Item) {
for _, item := range items {
item.Folded = false
if len(item.Children) > 0 {
unfoldAll(item.Children)
}
}
}
unfoldAll(m.orgFile.Items)
m.setStatus("All items unfolded")
}
}
case key.Matches(msg, m.keys.EditNotes): case key.Matches(msg, m.keys.EditNotes):
items := m.getVisibleItems() items := m.getVisibleItems()
if len(items) > 0 && m.cursor < len(items) { if len(items) > 0 && m.cursor < len(items) {
@ -281,6 +323,17 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, textinput.Blink return m, textinput.Blink
} }
case key.Matches(msg, m.keys.SetScheduled):
items := m.getVisibleItems()
if len(items) > 0 && m.cursor < len(items) {
m.editingItem = items[m.cursor]
m.mode = modeSetScheduled
m.textinput.SetValue("")
m.textinput.Placeholder = "YYYY-MM-DD or +N (days from today)"
m.textinput.Focus()
return m, textinput.Blink
}
case key.Matches(msg, m.keys.SetPriority): case key.Matches(msg, m.keys.SetPriority):
items := m.getVisibleItems() items := m.getVisibleItems()
if len(items) > 0 && m.cursor < len(items) { if len(items) > 0 && m.cursor < len(items) {
@ -521,83 +574,11 @@ func (m uiModel) updateAddSubTask(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m uiModel) updateSetDeadline(msg tea.Msg) (tea.Model, tea.Cmd) { func (m uiModel) updateSetDeadline(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd return m.updateSetDate(msg, "DEADLINE")
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.textinput.Width = 50
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
input := strings.TrimSpace(m.textinput.Value())
if m.editingItem != nil {
if input == "" {
// Empty input clears the deadline
m.editingItem.Deadline = nil
// Remove DEADLINE line from notes (only lines starting with DEADLINE:)
var filteredNotes []string
for _, note := range m.editingItem.Notes {
trimmedNote := strings.TrimSpace(note)
if !strings.HasPrefix(trimmedNote, "DEADLINE:") {
filteredNotes = append(filteredNotes, note)
}
}
m.editingItem.Notes = filteredNotes
m.setStatus("Deadline cleared!")
} else {
deadline, err := parseDeadlineInput(input)
if err != nil {
m.setStatus(fmt.Sprintf("Invalid date: %v", err))
} else {
m.editingItem.Deadline = &deadline
// Also update or add DEADLINE line in notes
updatedNotes := false
for i, note := range m.editingItem.Notes {
trimmedNote := strings.TrimSpace(note)
if strings.HasPrefix(trimmedNote, "DEADLINE:") {
m.editingItem.Notes[i] = fmt.Sprintf("DEADLINE: <%s>", parser.FormatOrgDate(deadline))
updatedNotes = true
break
}
}
// If DEADLINE wasn't in notes, it will be added by writeItem
if !updatedNotes {
// Remove old deadline lines just to be safe
var filteredNotes []string
for _, note := range m.editingItem.Notes {
trimmedNote := strings.TrimSpace(note)
if !strings.HasPrefix(trimmedNote, "DEADLINE:") {
filteredNotes = append(filteredNotes, note)
}
}
m.editingItem.Notes = filteredNotes
}
m.setStatus("Deadline set!")
}
}
}
m.mode = modeList
m.textinput.Blur()
m.editingItem = nil
return m, nil
case tea.KeyEsc:
m.mode = modeList
m.textinput.Blur()
m.editingItem = nil
m.setStatus("Cancelled")
return m, nil
}
}
m.textinput, cmd = m.textinput.Update(msg)
return m, cmd
} }
// parseDeadlineInput parses deadline input like "2024-01-15" or "+3" (3 days from now) // parseDateInput parses date input like "2024-01-15" or "+3" (3 days from now)
func parseDeadlineInput(input string) (time.Time, error) { func parseDateInput(input string) (time.Time, error) {
// Check if it's a relative date (+N days) // Check if it's a relative date (+N days)
if strings.HasPrefix(input, "+") { if strings.HasPrefix(input, "+") {
daysStr := strings.TrimPrefix(input, "+") daysStr := strings.TrimPrefix(input, "+")
@ -625,6 +606,110 @@ func parseDeadlineInput(input string) (time.Time, error) {
return time.Time{}, fmt.Errorf("unable to parse date: %s (use YYYY-MM-DD or +N)", input) return time.Time{}, fmt.Errorf("unable to parse date: %s (use YYYY-MM-DD or +N)", input)
} }
func (m uiModel) updateSetScheduled(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateSetDate(msg, "SCHEDULED")
}
func (m uiModel) updateSetDate(msg tea.Msg, dateType string) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.textinput.Width = 50
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
input := strings.TrimSpace(m.textinput.Value())
if m.editingItem != nil {
var prefixDate string
var clearedDateMsg string
var setDateMsg string
if dateType == "DEADLINE" {
prefixDate = "DEADLINE:"
clearedDateMsg = "Deadline cleared!"
setDateMsg = "Deadline set!"
} else {
prefixDate = "SCHEDULED:"
clearedDateMsg = "Scheduled date cleared!"
setDateMsg = "Scheduled date set!"
}
if input == "" {
// Empty input clears the date
if dateType == "DEADLINE" {
m.editingItem.Deadline = nil
} else {
m.editingItem.Scheduled = nil
}
// Remove property line from notes
var filteredNotes []string
for _, note := range m.editingItem.Notes {
trimmedNote := strings.TrimSpace(note)
if !strings.HasPrefix(trimmedNote, prefixDate) {
filteredNotes = append(filteredNotes, note)
}
}
m.editingItem.Notes = filteredNotes
m.setStatus(clearedDateMsg)
} else {
dateVal, err := parseDateInput(input)
if err != nil {
m.setStatus(fmt.Sprintf("Invalid date: %v", err))
} else {
if dateType == "DEADLINE" {
m.editingItem.Deadline = &dateVal
} else {
m.editingItem.Scheduled = &dateVal
}
// Also update or add property line in notes
updatedNotes := false
for i, note := range m.editingItem.Notes {
trimmedNote := strings.TrimSpace(note)
if strings.HasPrefix(trimmedNote, prefixDate) {
m.editingItem.Notes[i] = fmt.Sprintf("%s <%s>", prefixDate, parser.FormatOrgDate(dateVal))
updatedNotes = true
break
}
}
// If property wasn't in notes, it will be added by writeItem
if !updatedNotes {
// Remove old property lines just to be safe
var filteredNotes []string
for _, note := range m.editingItem.Notes {
trimmedNote := strings.TrimSpace(note)
if !strings.HasPrefix(trimmedNote, prefixDate) {
filteredNotes = append(filteredNotes, note)
}
}
m.editingItem.Notes = filteredNotes
}
m.setStatus(setDateMsg)
}
}
}
m.mode = modeList
m.textinput.Blur()
m.editingItem = nil
return m, nil
case tea.KeyEsc:
m.mode = modeList
m.textinput.Blur()
m.editingItem = nil
m.setStatus("Cancelled")
return m, nil
}
}
m.textinput, cmd = m.textinput.Update(msg)
return m, cmd
}
func (m uiModel) updateSetPriority(msg tea.Msg) (tea.Model, tea.Cmd) { func (m uiModel) updateSetPriority(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:

View file

@ -78,6 +78,8 @@ func (m uiModel) View() string {
return m.viewAddSubTask() return m.viewAddSubTask()
case modeSetDeadline: case modeSetDeadline:
return m.viewSetDeadline() return m.viewSetDeadline()
case modeSetScheduled:
return m.viewSetScheduled()
case modeSetPriority: case modeSetPriority:
return m.viewSetPriority() return m.viewSetPriority()
case modeSetEffort: case modeSetEffort:
@ -379,6 +381,14 @@ func (m uiModel) viewAddSubTask() string {
} }
func (m uiModel) viewSetDeadline() string { func (m uiModel) viewSetDeadline() string {
return m.viewSetDate("Set Deadline", "Leave empty to clear deadline")
}
func (m uiModel) viewSetScheduled() string {
return m.viewSetDate("Set Scheduled Date", "Leave empty to clear scheduled date")
}
func (m uiModel) viewSetDate(title, helpMsg string) string {
dialogStyle := lipgloss.NewStyle(). dialogStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("141")). BorderForeground(lipgloss.Color("141")).
@ -386,7 +396,7 @@ func (m uiModel) viewSetDeadline() string {
Width(60) Width(60)
var content strings.Builder var content strings.Builder
content.WriteString(m.styles.titleStyle.Render("Set Deadline")) content.WriteString(m.styles.titleStyle.Render(title))
content.WriteString("\n") content.WriteString("\n")
if m.editingItem != nil { if m.editingItem != nil {
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title))) content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
@ -396,7 +406,7 @@ func (m uiModel) viewSetDeadline() string {
content.WriteString("\n\n") content.WriteString("\n\n")
content.WriteString(m.styles.statusStyle.Render("Examples: 2025-12-31, +7 (7 days from now)")) content.WriteString(m.styles.statusStyle.Render("Examples: 2025-12-31, +7 (7 days from now)"))
content.WriteString("\n") content.WriteString("\n")
content.WriteString(m.styles.statusStyle.Render("Leave empty to clear deadline")) content.WriteString(m.styles.statusStyle.Render(helpMsg))
content.WriteString("\n") content.WriteString("\n")
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel")) content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel"))
@ -488,7 +498,7 @@ func (m uiModel) viewHelp() string {
navigationBindings := []key.Binding{m.keys.Up, m.keys.Down, m.keys.Left, m.keys.Right} 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} itemBindings := []key.Binding{m.keys.ToggleFold, m.keys.EditNotes, m.keys.CycleState}
taskBindings := []key.Binding{m.keys.Capture, m.keys.AddSubTask, m.keys.Delete} 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} timeBindings := []key.Binding{m.keys.ClockIn, m.keys.ClockOut, m.keys.SetDeadline, m.keys.SetScheduled, m.keys.SetEffort}
organizationBindings := []key.Binding{m.keys.SetPriority, m.keys.TagItem, m.keys.ShiftUp, m.keys.ShiftDown, m.keys.ToggleReorder} organizationBindings := []key.Binding{m.keys.SetPriority, m.keys.TagItem, m.keys.ShiftUp, m.keys.ShiftDown, m.keys.ToggleReorder}
viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Settings, m.keys.Save, m.keys.Help, m.keys.Quit} viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Settings, m.keys.Save, m.keys.Help, m.keys.Quit}