diff --git a/cmd/list.go b/cmd/list.go index 42d798cc..46dfa111 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -34,6 +34,12 @@ Filtering by date: Lists all todos whose due date is today or earlier: ultralist list due:agenda +Filtering by status: +------------------ + + List all todos with a status of "started" + ultralist list status:started + Filtering by priority, completed, etc: -------------------------------------- @@ -61,6 +67,9 @@ Grouping: Lists all todos grouped by project: ultralist list group:p + Lists all todos grouped by status: + ultralist list group:s + Combining filters: ------------------ diff --git a/cmd/status.go b/cmd/status.go new file mode 100644 index 00000000..495cbd37 --- /dev/null +++ b/cmd/status.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "strings" + + "github.com/spf13/cobra" + "github.com/ultralist/ultralist/ultralist" +) + +func init() { + var ( + setStatusCmdDesc = "Sets the status of a todo item" + setStatusCmdExample = ` +ultralist status 33 blocked +ultralist s 33 blocked` + setStatusCmdLongDesc = `Sets the status of a todo item. Status can be any string.` + ) + + var setStatusCmd = &cobra.Command{ + Use: "status [id] ", + Aliases: []string{"s"}, + Example: setStatusCmdExample, + Long: setStatusCmdLongDesc, + Short: setStatusCmdDesc, + Run: func(cmd *cobra.Command, args []string) { + ultralist.NewApp().SetTodoStatus(strings.Join(args, " ")) + }, + } + + rootCmd.AddCommand(setStatusCmd) +} diff --git a/ultralist/app.go b/ultralist/app.go index cda79cab..365ce3e3 100644 --- a/ultralist/app.go +++ b/ultralist/app.go @@ -335,6 +335,21 @@ func (a *App) UnprioritizeTodo(input string) { fmt.Println("Todo un-prioritized.") } +// StartTodo will start a todo. +func (a *App) SetTodoStatus(input string) { + a.Load() + ids := a.getIDs(input) + if len(ids) == 0 { + return + } + + splitted := strings.Split(input, " ") + + a.TodoList.SetStatus(splitted[len(splitted)-1], ids...) + a.save() + fmt.Println("Todo status updated.") +} + // GarbageCollect will delete all archived todos. func (a *App) GarbageCollect() { a.Load() @@ -474,6 +489,7 @@ func (a *App) getGroups(input string, todos []*Todo) *GroupedTodos { grouper := &Grouper{} contextRegex, _ := regexp.Compile(`group:c.*$`) projectRegex, _ := regexp.Compile(`group:p.*$`) + statusRegex, _ := regexp.Compile(`group:s.*$`) var grouped *GroupedTodos @@ -481,6 +497,9 @@ func (a *App) getGroups(input string, todos []*Todo) *GroupedTodos { grouped = grouper.GroupByContext(todos) } else if projectRegex.MatchString(input) { grouped = grouper.GroupByProject(todos) + } else if statusRegex.MatchString(input) { + fmt.Println("grouping by status") + grouped = grouper.GroupByStatus(todos) } else { grouped = grouper.GroupByNothing(todos) } diff --git a/ultralist/event_logger.go b/ultralist/event_logger.go index 5b9ddbdf..efc424d1 100644 --- a/ultralist/event_logger.go +++ b/ultralist/event_logger.go @@ -46,6 +46,7 @@ type EventLog struct { Completed bool `json:"completed"` CompletedDate string `json:"completedDate"` Archived bool `json:"archived"` + Status string `json:"status"` IsPriority bool `json:"isPriority"` Notes []string `json:"notes"` } @@ -180,6 +181,7 @@ func (e *EventLogger) writeTodoEvent(eventType string, todo *Todo) *EventLog { Completed: todo.Completed, CompletedDate: todo.CompletedDate, Archived: todo.Archived, + Status: todo.Status, IsPriority: todo.IsPriority, Notes: todo.Notes, } diff --git a/ultralist/filter.go b/ultralist/filter.go index e1590ef5..69662c57 100644 --- a/ultralist/filter.go +++ b/ultralist/filter.go @@ -1,6 +1,9 @@ package ultralist -import "regexp" +import ( + "regexp" + "strings" +) // TodoFilter filters todos based on patterns. type TodoFilter struct { @@ -19,6 +22,7 @@ func (f *TodoFilter) Filter(input string) []*Todo { f.Todos = f.filterPrioritized(input) f.Todos = f.filterProjects(input) f.Todos = f.filterContexts(input) + f.Todos = f.filterStatus(input) f.Todos = NewDateFilter(f.Todos).FilterDate(input) return f.Todos @@ -73,6 +77,26 @@ func (f *TodoFilter) filterPrioritized(input string) []*Todo { return f.Todos } +func (f *TodoFilter) filterStatus(input string) []*Todo { + + r, _ := regexp.Compile(`status:\w+`) + if !r.MatchString(input) { + return f.Todos + } + + statusString := strings.Split(r.FindString(input), ":")[1] + + var ret []*Todo + + for _, todo := range f.Todos { + if todo.Status == statusString { + ret = append(ret, todo) + } + } + + return ret +} + func (f *TodoFilter) filterProjects(input string) []*Todo { if !f.isFilteringByProjects(input) { return f.Todos diff --git a/ultralist/grouper.go b/ultralist/grouper.go index 91a5476b..e94b6e9f 100644 --- a/ultralist/grouper.go +++ b/ultralist/grouper.go @@ -66,6 +66,26 @@ func (g *Grouper) GroupByProject(todos []*Todo) *GroupedTodos { return &GroupedTodos{Groups: groups} } +// GroupByStatus is grouping todos by status +func (g *Grouper) GroupByStatus(todos []*Todo) *GroupedTodos { + groups := map[string][]*Todo{} + + for _, todo := range todos { + if todo.Status != "" { + groups[todo.Status] = append(groups[todo.Status], todo) + } else { + groups["No status"] = append(groups["No status"], todo) + } + } + + // finally, sort the todos + for groupName, todos := range groups { + groups[groupName] = g.sort(todos) + } + + return &GroupedTodos{Groups: groups} +} + // GroupByNothing is the default result if todos are not grouped by context project. func (g *Grouper) GroupByNothing(todos []*Todo) *GroupedTodos { groups := map[string][]*Todo{} diff --git a/ultralist/screen_printer.go b/ultralist/screen_printer.go index 8ebad9aa..2750595b 100644 --- a/ultralist/screen_printer.go +++ b/ultralist/screen_printer.go @@ -15,6 +15,8 @@ import ( var ( blue = color.New(0, color.FgBlue) blueBold = color.New(color.Bold, color.FgBlue) + green = color.New(0, color.FgGreen) + greenBold = color.New(color.Bold, color.FgGreen) cyan = color.New(0, color.FgCyan) cyanBold = color.New(color.Bold, color.FgCyan) magenta = color.New(0, color.FgMagenta) @@ -65,14 +67,15 @@ func (f *ScreenPrinter) printTodo(tabby *tabby.Tabby, todo *Todo, printNotes boo tabby.AddLine( f.formatID(todo.ID, todo.IsPriority), f.formatCompleted(todo.Completed), - f.formatInformation(todo), f.formatDue(todo.Due, todo.IsPriority, todo.Completed), + f.formatStatus(todo.Status, todo.IsPriority), f.formatSubject(todo.Subject, todo.IsPriority)) } else { tabby.AddLine( f.formatID(todo.ID, todo.IsPriority), f.formatCompleted(todo.Completed), f.formatDue(todo.Due, todo.IsPriority, todo.Completed), + f.formatStatus(todo.Status, todo.IsPriority), f.formatSubject(todo.Subject, todo.IsPriority)) } @@ -83,6 +86,7 @@ func (f *ScreenPrinter) printTodo(tabby *tabby.Tabby, todo *Todo, printNotes boo white.Sprint(""), white.Sprint(""), white.Sprint(""), + white.Sprint(""), white.Sprint(note)) } } @@ -99,9 +103,8 @@ func (f *ScreenPrinter) formatCompleted(completed bool) string { if completed { if f.UnicodeSupport { return white.Sprint("[✔]") - } else { - return white.Sprint("[x]") } + return white.Sprint("[x]") } return white.Sprint("[ ]") } @@ -118,6 +121,25 @@ func (f *ScreenPrinter) formatDue(due string, isPriority bool, completed bool) s return f.printDue(dueTime, completed) } +func (f *ScreenPrinter) formatStatus(status string, isPriority bool) string { + if status == "" { + return green.Sprint(" ") + } + + if len(status) < 10 { + for x := len(status); x <= 10; x++ { + status += " " + } + } + + statusRune := []rune(status) + + if isPriority { + return greenBold.Sprintf("%-10v", string(statusRune[0:10])) + } + return green.Sprintf("%-10s", string(statusRune[0:10])) +} + func (f *ScreenPrinter) formatInformation(todo *Todo) string { var information []string if todo.IsPriority { @@ -130,12 +152,8 @@ func (f *ScreenPrinter) formatInformation(todo *Todo) string { } else { information = append(information, " ") } - if todo.Archived { - information = append(information, "A") - } else { - information = append(information, " ") - } - return white.Sprint(strings.Join(information, " ")) + + return white.Sprint(strings.Join(information, "")) } func (f *ScreenPrinter) printDue(due time.Time, completed bool) string { diff --git a/ultralist/simple_screen_printer.go b/ultralist/simple_screen_printer.go index 0ed18cac..86cae001 100644 --- a/ultralist/simple_screen_printer.go +++ b/ultralist/simple_screen_printer.go @@ -120,7 +120,7 @@ func (f *SimpleScreenPrinter) formatInformation(todo *Todo) string { } else { information = append(information, " ") } - return fmt.Sprint(strings.Join(information, " ")) + return fmt.Sprint(strings.Join(information, "")) } func (f *SimpleScreenPrinter) printDue(due time.Time, completed bool) string { diff --git a/ultralist/todo_item.go b/ultralist/todo_item.go index 6c86420f..fc488bef 100644 --- a/ultralist/todo_item.go +++ b/ultralist/todo_item.go @@ -18,6 +18,7 @@ type Todo struct { Due string `json:"due"` Completed bool `json:"completed"` CompletedDate string `json:"completedDate"` + Status string `json:"status"` Archived bool `json:"archived"` IsPriority bool `json:"isPriority"` Notes []string `json:"notes"` @@ -98,6 +99,7 @@ func (t Todo) Equals(other *Todo) bool { !reflect.DeepEqual(t.Contexts, other.Contexts) || t.Due != other.Due || t.Completed != other.Completed || + t.Status != other.Status || t.CompletedDate != other.CompletedDate || t.Archived != other.Archived || t.IsPriority != other.IsPriority || diff --git a/ultralist/todo_list.go b/ultralist/todo_list.go index 30e27673..c728c2cf 100644 --- a/ultralist/todo_list.go +++ b/ultralist/todo_list.go @@ -1,6 +1,8 @@ package ultralist -import "sort" +import ( + "sort" +) // TodoList is the struct of a list with several todos. type TodoList struct { @@ -117,6 +119,19 @@ func (t *TodoList) Unprioritize(ids ...int) { } } +// SetStatus sets the status of a todo +func (t *TodoList) SetStatus(input string, ids ...int) { + for _, id := range ids { + todo := t.FindByID(id) + if todo == nil { + continue + } + todo.Status = input + t.Delete(id) + t.Data = append(t.Data, todo) + } +} + // IndexOf finds the index of a todo. func (t *TodoList) IndexOf(todoToFind *Todo) int { for i, todo := range t.Data {