diff --git a/common/keymap.go b/common/keymap.go index 849e0b6..9988d55 100644 --- a/common/keymap.go +++ b/common/keymap.go @@ -21,6 +21,7 @@ type Keymap struct { SetProject key.Binding Select key.Binding Insert key.Binding + Undo key.Binding } // NewKeymap creates a new Keymap. @@ -100,5 +101,10 @@ func NewKeymap() *Keymap { key.WithKeys("i"), key.WithHelp("insert", "Insert mode"), ), + + Undo: key.NewBinding( + key.WithKeys("u"), + key.WithHelp("undo", "Undo"), + ), } } diff --git a/pages/contextPicker.go b/pages/contextPicker.go index 1e59769..a98cd27 100644 --- a/pages/contextPicker.go +++ b/pages/contextPicker.go @@ -102,7 +102,7 @@ func (p *ContextPickerPage) View() string { func (p *ContextPickerPage) updateContextCmd() tea.Msg { context := p.form.GetString("context") if context == "(none)" { - context = "none" + context = "" } return UpdateContextMsg(p.common.TW.GetContext(context)) } diff --git a/pages/report.go b/pages/report.go index b519273..bdeee2b 100644 --- a/pages/report.go +++ b/pages/report.go @@ -150,6 +150,9 @@ func (p *ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.subpageActive = true p.common.PushPage(p) return p.subpage, nil + case key.Matches(msg, p.common.Keymap.Undo): + p.common.TW.Undo() + return p, p.getTasks() } } @@ -175,6 +178,8 @@ func (p *ReportPage) View() string { } func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) { + var selected int + nCols := len(p.activeReport.Columns) columns := make([]table.Column, 0) columnSizes := make([]int, nCols) @@ -182,6 +187,10 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) { rows := make([]table.Row, len(tasks)) for i, task := range tasks { + if p.selectedTask != nil && task.Uuid == p.selectedTask.Uuid { + selected = i + } + row := table.Row{} for i, col := range p.activeReport.Columns { field := task.GetString(col) @@ -210,6 +219,10 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) { columns = append(columns, table.Column{Title: label, Width: max(columnSizes[i], len(label))}) } + if selected == 0 { + selected = p.taskTable.Cursor() + } + p.taskTable = table.New( table.WithColumns(columns), table.WithRows(rows), @@ -218,6 +231,12 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) { // table.WithWidth(100), ) p.taskTable.SetStyles(p.tableStyle) + + if selected < len(p.tasks) { + p.taskTable.SetCursor(selected) + } else { + p.taskTable.SetCursor(len(p.tasks) - 1) + } } func (p *ReportPage) getTasks() tea.Cmd { diff --git a/pages/taskEditor.go b/pages/taskEditor.go index 8262f77..c355777 100644 --- a/pages/taskEditor.go +++ b/pages/taskEditor.go @@ -3,6 +3,7 @@ package pages import ( "fmt" "log/slog" + "strings" "tasksquire/common" "tasksquire/taskwarrior" "time" @@ -24,11 +25,17 @@ const ( ) type TaskEditorPage struct { - common *common.Common - task taskwarrior.Task - form *huh.Form - mode Mode - statusline tea.Model + common *common.Common + task taskwarrior.Task + form *huh.Form + mode Mode + statusline tea.Model + nFields int + currentField int + + // TODO: rework support for adding tags and projects + additionalTags string + additionalProject string } type TaskEditorKeys struct { @@ -85,11 +92,27 @@ func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditor Title("Project"). Value(&p.task.Project), + huh.NewInput(). + Title("Project"). + Value(&p.additionalProject). + Validate(func(project string) error { + if strings.Contains(project, " ") { + return fmt.Errorf("project name cannot contain spaces") + } + return nil + }). + Inline(true), + huh.NewMultiSelect[string](). Options(huh.NewOptions(tagOptions...)...). Title("Tags"). Value(&p.task.Tags), + huh.NewInput(). + Title("Tags"). + Value(&p.additionalTags). + Inline(true), + huh.NewInput(). Title("Due"). Value(&p.task.Due). @@ -112,10 +135,13 @@ func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditor WithShowHelp(false). WithShowErrors(true). // use styles from common - WithHeight(30). + WithHeight(40). WithWidth(50). WithTheme(p.common.Styles.Form) + p.nFields = 6 + p.currentField = 0 + p.statusline = NewStatusLine(common, p.mode) return p @@ -141,7 +167,6 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.mode = ModeInsert } } - switch p.mode { case ModeNormal: switch msg := msg.(type) { @@ -208,7 +233,19 @@ func (p *TaskEditorPage) View() string { } func (p *TaskEditorPage) updateTasksCmd() tea.Msg { - p.common.TW.AddTask(&p.task) + if p.task.Project == "(none)" { + p.task.Project = "" + } + if p.task.Priority == "(none)" { + p.task.Priority = "" + } + if p.additionalTags != "" { + p.task.Tags = append(p.task.Tags, strings.Split(p.additionalTags, " ")...) + } + if p.additionalProject != "" { + p.task.Project = p.additionalProject + } + p.common.TW.ImportTask(&p.task) return UpdatedTasksMsg{} } diff --git a/taskwarrior/models.go b/taskwarrior/models.go index 31627ba..ce1c6d7 100644 --- a/taskwarrior/models.go +++ b/taskwarrior/models.go @@ -19,25 +19,26 @@ func (a Annotation) String() string { } type Task struct { - Id int64 `json:"id,omitempty"` - Uuid string `json:"uuid,omitempty"` - Description string `json:"description,omitempty"` - Project string `json:"project,omitempty"` - Priority string `json:"priority,omitempty"` - Status string `json:"status,omitempty"` - Tags []string `json:"tags,omitempty"` - Depends []string `json:"depends,omitempty"` + Id int64 `json:"id,omitempty"` + Uuid string `json:"uuid,omitempty"` + Description string `json:"description,omitempty"` + Project string `json:"project"` + Priority string `json:"priority"` + Status string `json:"status,omitempty"` + Tags []string `json:"tags"` + Depends []string `json:"depends,omitempty"` + DependsIds string Urgency float32 `json:"urgency,omitempty"` Parent string `json:"parent,omitempty"` - Due string `json:"due,omitempty"` - Wait string `json:"wait,omitempty"` - Scheduled string `json:"scheduled,omitempty"` - Until string `json:"until,omitempty"` + Due string `json:"due"` + Wait string `json:"wait"` + Scheduled string `json:"scheduled"` + Until string `json:"until"` Start string `json:"start,omitempty"` End string `json:"end,omitempty"` Entry string `json:"entry,omitempty"` Modified string `json:"modified,omitempty"` - Recur string `json:"recur,omitempty"` + Recur string `json:"recur"` Annotations []Annotation `json:"annotations,omitempty"` } @@ -171,8 +172,7 @@ func (t *Task) GetString(fieldWFormat string) string { return "" } } - // TODO: get Ids from UUIDs - return strings.Join(t.Depends, ", ") + return t.DependsIds case "recur": return t.Recur diff --git a/taskwarrior/taskwarrior.go b/taskwarrior/taskwarrior.go index a4f1678..43ad999 100644 --- a/taskwarrior/taskwarrior.go +++ b/taskwarrior/taskwarrior.go @@ -1,3 +1,6 @@ +// TODO: error handling +// TODO: split combinedOutput and handle stderr differently +// TODO: reorder functions package taskwarrior import ( @@ -90,6 +93,8 @@ type TaskWarrior interface { AddTask(task *Task) error ImportTask(task *Task) SetTaskDone(task *Task) + + Undo() } type TaskSquire struct { @@ -138,6 +143,7 @@ func (ts *TaskSquire) GetTasks(report *Report, filter ...string) Tasks { for _, context := range ts.contexts { if context.Active && context.Name != "none" { args = append(args, context.ReadFilter) + break } } } @@ -160,13 +166,39 @@ func (ts *TaskSquire) GetTasks(report *Report, filter ...string) Tasks { return nil } + for _, task := range tasks { + if task.Depends != nil && len(task.Depends) > 0 { + ids := make([]string, len(task.Depends)) + for i, dependUuid := range task.Depends { + ids[i] = ts.getIds([]string{fmt.Sprintf("uuid:%s", dependUuid)}) + } + + task.DependsIds = strings.Join(ids, " ") + } + } + return tasks } +func (ts *TaskSquire) getIds(filter []string) string { + cmd := exec.Command(twBinary, append(append(ts.defaultArgs, filter...), "_ids")...) + out, err := cmd.CombinedOutput() + if err != nil { + slog.Error("Failed getting field:", err) + return "" + } + + return strings.TrimSpace(string(out)) +} + func (ts *TaskSquire) GetContext(context string) *Context { ts.mutex.Lock() defer ts.mutex.Unlock() + if context == "" { + context = "none" + } + if context, ok := ts.contexts[context]; ok { return context } else { @@ -276,6 +308,10 @@ func (ts *TaskSquire) SetContext(context *Context) error { ts.mutex.Lock() defer ts.mutex.Unlock() + if context.Name == "none" && ts.contexts["none"].Active { + return nil + } + cmd := exec.Command(twBinary, []string{"context", context.Name}...) if err := cmd.Run(); err != nil { slog.Error("Failed setting context:", err) @@ -355,6 +391,17 @@ func (ts *TaskSquire) SetTaskDone(task *Task) { } } +func (ts *TaskSquire) Undo() { + ts.mutex.Lock() + defer ts.mutex.Unlock() + + cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"undo"}...)...) + err := cmd.Run() + if err != nil { + slog.Error("Failed undoing task:", err) + } +} + func (ts *TaskSquire) extractConfig() *TWConfig { cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...) output, err := cmd.CombinedOutput()