mirror of
https://github.com/RWejlgaard/org.git
synced 2026-05-06 04:34:45 +00:00
feat: SetScheduled
This commit is contained in:
parent
c858e70d07
commit
c347edec80
6 changed files with 169 additions and 101 deletions
|
|
@ -128,6 +128,7 @@ Feel free to fork and create a pull request if there's any features missing for
|
|||
| `i` | Clock in |
|
||||
| `o` | Clock out |
|
||||
| `d` | Set deadline |
|
||||
| `S` | Set scheduled date |
|
||||
| `p` | Set priority |
|
||||
| `e` | Set effort |
|
||||
| `r` | Toggle reorder mode |
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ type KeybindingsConfig struct {
|
|||
ClockIn []string `toml:"clock_in"`
|
||||
ClockOut []string `toml:"clock_out"`
|
||||
SetDeadline []string `toml:"set_deadline"`
|
||||
SetScheduled []string `toml:"set_scheduled"`
|
||||
SetPriority []string `toml:"set_priority"`
|
||||
SetEffort []string `toml:"set_effort"`
|
||||
Help []string `toml:"help"`
|
||||
|
|
@ -124,6 +125,7 @@ func DefaultConfig() *Config {
|
|||
ClockIn: []string{"i"},
|
||||
ClockOut: []string{"o"},
|
||||
SetDeadline: []string{"d"},
|
||||
SetScheduled: []string{"S"},
|
||||
SetPriority: []string{"p"},
|
||||
SetEffort: []string{"e"},
|
||||
Help: []string{"?"},
|
||||
|
|
@ -312,6 +314,9 @@ func (c *Config) fillDefaults() {
|
|||
if len(c.Keybindings.SetDeadline) == 0 {
|
||||
c.Keybindings.SetDeadline = defaults.Keybindings.SetDeadline
|
||||
}
|
||||
if len(c.Keybindings.SetScheduled) == 0 {
|
||||
c.Keybindings.SetScheduled = defaults.Keybindings.SetScheduled
|
||||
}
|
||||
if len(c.Keybindings.SetPriority) == 0 {
|
||||
c.Keybindings.SetPriority = defaults.Keybindings.SetPriority
|
||||
}
|
||||
|
|
@ -567,6 +572,7 @@ func (c *Config) GetAllKeybindings() map[string][]string {
|
|||
"clock_in": c.Keybindings.ClockIn,
|
||||
"clock_out": c.Keybindings.ClockOut,
|
||||
"set_deadline": c.Keybindings.SetDeadline,
|
||||
"set_scheduled": c.Keybindings.SetScheduled,
|
||||
"set_priority": c.Keybindings.SetPriority,
|
||||
"set_effort": c.Keybindings.SetEffort,
|
||||
"help": c.Keybindings.Help,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const (
|
|||
modeCapture
|
||||
modeAddSubTask
|
||||
modeSetDeadline
|
||||
modeSetScheduled
|
||||
modeSetPriority
|
||||
modeSetEffort
|
||||
modeHelp
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type keyMap struct {
|
|||
ClockIn key.Binding
|
||||
ClockOut key.Binding
|
||||
SetDeadline key.Binding
|
||||
SetScheduled key.Binding
|
||||
SetPriority key.Binding
|
||||
SetEffort key.Binding
|
||||
Settings key.Binding
|
||||
|
|
@ -126,6 +127,10 @@ func newKeyMapFromConfig(cfg *config.Config) keyMap {
|
|||
key.WithKeys(kb.SetDeadline...),
|
||||
key.WithHelp(formatKeyHelp(kb.SetDeadline), "set deadline"),
|
||||
),
|
||||
SetScheduled: key.NewBinding(
|
||||
key.WithKeys(kb.SetScheduled...),
|
||||
key.WithHelp(formatKeyHelp(kb.SetScheduled), "set scheduled"),
|
||||
),
|
||||
SetPriority: key.NewBinding(
|
||||
key.WithKeys(kb.SetPriority...),
|
||||
key.WithHelp(formatKeyHelp(kb.SetPriority), "set priority"),
|
||||
|
|
@ -195,7 +200,7 @@ func (k keyMap) getAllBindings() []key.Binding {
|
|||
k.Up, k.Down, k.Left, k.Right,
|
||||
k.ToggleFold, k.EditNotes, k.ToggleReorder,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m.updateAddSubTask(msg)
|
||||
case modeSetDeadline:
|
||||
return m.updateSetDeadline(msg)
|
||||
case modeSetScheduled:
|
||||
return m.updateSetScheduled(msg)
|
||||
case modeSetPriority:
|
||||
return m.updateSetPriority(msg)
|
||||
case modeSetEffort:
|
||||
|
|
@ -281,6 +283,17 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
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):
|
||||
items := m.getVisibleItems()
|
||||
if len(items) > 0 && m.cursor < len(items) {
|
||||
|
|
@ -521,83 +534,11 @@ func (m uiModel) updateAddSubTask(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
|
||||
func (m uiModel) updateSetDeadline(msg tea.Msg) (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 {
|
||||
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
|
||||
return m.updateSetDate(msg, "DEADLINE")
|
||||
}
|
||||
|
||||
// parseDeadlineInput parses deadline input like "2024-01-15" or "+3" (3 days from now)
|
||||
func parseDeadlineInput(input string) (time.Time, error) {
|
||||
// parseDateInput parses date input like "2024-01-15" or "+3" (3 days from now)
|
||||
func parseDateInput(input string) (time.Time, error) {
|
||||
// Check if it's a relative date (+N days)
|
||||
if strings.HasPrefix(input, "+") {
|
||||
daysStr := strings.TrimPrefix(input, "+")
|
||||
|
|
@ -625,6 +566,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)
|
||||
}
|
||||
|
||||
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) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ func (m uiModel) View() string {
|
|||
return m.viewAddSubTask()
|
||||
case modeSetDeadline:
|
||||
return m.viewSetDeadline()
|
||||
case modeSetScheduled:
|
||||
return m.viewSetScheduled()
|
||||
case modeSetPriority:
|
||||
return m.viewSetPriority()
|
||||
case modeSetEffort:
|
||||
|
|
@ -379,6 +381,14 @@ func (m uiModel) viewAddSubTask() 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().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("141")).
|
||||
|
|
@ -386,7 +396,7 @@ func (m uiModel) viewSetDeadline() string {
|
|||
Width(60)
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(m.styles.titleStyle.Render("Set Deadline"))
|
||||
content.WriteString(m.styles.titleStyle.Render(title))
|
||||
content.WriteString("\n")
|
||||
if m.editingItem != nil {
|
||||
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(m.styles.statusStyle.Render("Examples: 2025-12-31, +7 (7 days from now)"))
|
||||
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(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}
|
||||
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}
|
||||
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}
|
||||
viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Settings, m.keys.Save, m.keys.Help, m.keys.Quit}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue