package todotxt import ( "bufio" "errors" "fmt" "io/ioutil" "os" "sort" "strings" ) type TodoList struct { Todos []*Todo `json:"todos"` SortFlag int `json:"sortFlag"` } // Newtodolist creates a new empty todolist. func NewTodoList() *TodoList { return &TodoList{Todos: []*Todo{}} } func (todolist *TodoList) Size() int { return len(todolist.Todos) } func (todolist *TodoList) GetTaskSlice() []*Todo { return todolist.Todos } func (todolist *TodoList) Contains(t *Todo) bool { for _, tsk := range todolist.Todos { if tsk == t { return true } } return false } func (todolist *TodoList) GetTasksWithContext(context string) *TodoList { return todolist.Filter(func(t *Todo) bool { return t.HasContext(context) }) } func (todolist *TodoList) GetTasksWithProject(project string) *TodoList { return todolist.Filter(func(t *Todo) bool { return t.HasProject(project) }) } func (todolist *TodoList) GetContexts() []string { var ret []string added := make(map[string]bool) for _, tsk := range todolist.Todos { for _, c := range tsk.Contexts { if !added[c] { ret = append(ret, c) added[c] = true } } } sort.Strings(ret) return ret } func (todolist *TodoList) GetProjects() []string { var ret []string added := make(map[string]bool) for _, tsk := range todolist.Todos { for _, p := range tsk.Projects { if !added[p] { ret = append(ret, p) added[p] = true } } } sort.Strings(ret) return ret } func (todolist *TodoList) GetTagKVList() []string { var ret []string added := make(map[string]bool) for _, tsk := range todolist.Todos { for k, v := range tsk.AdditionalTags { tag := fmt.Sprintf("%s:%s", k, v) if !added[tag] { ret = append(ret, tag) added[tag] = true } } } sort.Strings(ret) return ret } func (todolist *TodoList) GetTagKeys() []string { var ret []string added := make(map[string]bool) for _, tsk := range todolist.Todos { for k := range tsk.AdditionalTags { if !added[k] { ret = append(ret, k) added[k] = true } } } sort.Strings(ret) return ret } func (todolist *TodoList) GetTagValuesForKey(key string) []string { var ret []string added := make(map[string]bool) for _, tsk := range todolist.Todos { if v, ok := tsk.AdditionalTags[key]; ok && !added[v] { ret = append(ret, v) added[v] = true } } sort.Strings(ret) return ret } func (todolist *TodoList) GetIncompleteTasks() *TodoList { t := *NewTodoList() for _, v := range todolist.Todos { if !v.Completed { t.Todos = append(t.Todos, v) } } return &t } func (todolist *TodoList) GetNextId() int { nextId := 0 for _, v := range todolist.Todos { if v.Id > nextId { nextId = v.Id } } return nextId + 1 } // String returns a complete list of tasks in todo.txt format. func (todolist *TodoList) String() string { var ret string for _, todo := range todolist.Todos { ret += fmt.Sprintf("%s\n", todo.String()) } return ret } // AddTodo prepends a Todo to the current TodoList and takes care to set the Todo.Id correctly func (todolist *TodoList) AddTodo(todo *Todo) { todo.Id = todolist.GetNextId() todolist.Todos = append(todolist.Todos, todo) todolist.refresh() } // AddTimers adds all passed in timers to the list, sorts the list, then updates the Timer.Id values. func (todolist *TodoList) AddTodos(todos []*Todo) { for _, v := range todos { todolist.AddTodo(v) } } func (todolist *TodoList) Combine(other *TodoList) { todolist.AddTodos(other.Todos) } // GetTodo returns the Todo with the given todo 'id' from the TodoList. // Returns an error if Todo could not be found. func (todolist *TodoList) GetTodo(id int) (*Todo, error) { for i := range todolist.Todos { if todolist.Todos[i].Id == id { return todolist.Todos[i], nil } } return nil, errors.New("todo not found") } // RemoveTodoById removes any Todo with the given Todo 'id' from the TodoList. // Returns an error if no Todo was removed func (todolist *TodoList) RemoveTodoById(id int) error { found := false var remIdx int var t *Todo for remIdx, t = range todolist.Todos { if t.Id == id { found = true break } } if !found { return errors.New("todo not found") } todolist.Todos = append(todolist.Todos[:remIdx], todolist.Todos[remIdx+1:]...) todolist.refresh() return nil } // RemoveTodo removes any Todo from the TodoList with the same String representation as the given Todo. // Returns an error if no Todo was removed. func (todolist *TodoList) RemoveTodo(todo Todo) error { found := false var remIdx int var t *Todo for remIdx, t = range todolist.Todos { if t.String() == todo.String() { found = true break } } if !found { return errors.New("todo not found") } todolist.Todos = append(todolist.Todos[:remIdx], todolist.Todos[remIdx+1:]...) todolist.refresh() return nil } // ArchiveTodoToFile removes the todo from the active list and concatenates it to // the passed in filename // Return an err if any part of that fails func (todolist *TodoList) ArchiveTodoToFile(todo Todo, filename string) error { if err := todolist.RemoveTodo(todo); err != nil { return err } f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) if err != nil { return err } defer f.Close() _, err = f.WriteString(todo.String() + "\n") return err } // Filter filters the current TodoList for the given predicate (a function that takes a todo as input and returns a // bool), and returns a new TodoList. The original TodoList is not modified. func (todolist *TodoList) Filter(predicate func(*Todo) bool) *TodoList { var newList TodoList for _, t := range todolist.Todos { if predicate(t) { newList.Todos = append(newList.Todos, t) } } return &newList } func (todolist *TodoList) ResetIds() { todoId := 1 for _, v := range todolist.Todos { v.Id = todoId todoId++ } } // LoadFromFile loads a TodoList from *os.File. // Note: This will clear the current TodoList and overwrite it's contents with whatever is in *os.File. func (todolist *TodoList) LoadFromFile(file *os.File) error { todolist.Todos = []*Todo{} // Empty todolist todoId := 1 scanner := bufio.NewScanner(file) for scanner.Scan() { text := strings.Trim(scanner.Text(), "\t\n\r") // Read Line // Ignore blank lines if text == "" { continue } todo, err := ParseTodo(text) if err != nil { return err } todo.Id = todoId todoId++ todolist.Todos = append(todolist.Todos, todo) } if err := scanner.Err(); err != nil { return err } todolist.refresh() return nil } // WriteToFile writes a TodoList to *os.File func (todolist *TodoList) WriteToFile(file *os.File) error { writer := bufio.NewWriter(file) _, err := writer.WriteString(todolist.String()) writer.Flush() if err != nil { return err } todolist.ResetIds() return nil } // LoadFromFilename loads a TodoList from the filename. func (todolist *TodoList) LoadFromFilename(filename string) error { file, err := os.Open(filename) if err != nil { return err } defer file.Close() return todolist.LoadFromFile(file) } // WriteToFilename writes a TodoList to the specified file (most likely called "todo.txt"). func (todolist *TodoList) WriteToFilename(filename string) error { if err := ioutil.WriteFile(filename, []byte(todolist.String()), 0640); err != nil { return err } todolist.ResetIds() return nil } // LoadFromFile loads and returns a TodoList from *os.File. func LoadFromFile(file *os.File) (*TodoList, error) { todolist := TodoList{} if err := todolist.LoadFromFile(file); err != nil { return nil, err } return &todolist, nil } // WriteToFile writes a TodoList to *os.File. func WriteToFile(todolist *TodoList, file *os.File) error { return todolist.WriteToFile(file) } // LoadFromFilename loads and returns a TodoList from a file (most likely called "todo.txt") func LoadFromFilename(filename string) (*TodoList, error) { todolist := TodoList{} if err := todolist.LoadFromFilename(filename); err != nil { return nil, err } return &todolist, nil } // WriteToFilename write a TodoList to the specified file (most likely called "todo.txt") func WriteToFilename(todolist *TodoList, filename string) error { return todolist.WriteToFilename(filename) }