2023-08-23 14:17:16 +00:00
|
|
|
package todotxt
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2023-08-23 14:55:38 +00:00
|
|
|
"sort"
|
2023-08-23 14:17:16 +00:00
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type TodoList struct {
|
2023-08-23 16:10:03 +00:00
|
|
|
Todos []*Todo `json:"todos"`
|
|
|
|
SortFlag int `json:"sortFlag"`
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Newtodolist creates a new empty todolist.
|
2023-08-23 16:29:45 +00:00
|
|
|
func NewTodoList() *TodoList { return &TodoList{Todos: []*Todo{}} }
|
2023-08-23 16:10:03 +00:00
|
|
|
func (todolist *TodoList) Size() int { return len(todolist.Todos) }
|
|
|
|
func (todolist *TodoList) GetTaskSlice() []*Todo { return todolist.Todos }
|
2023-08-23 14:17:16 +00:00
|
|
|
|
|
|
|
func (todolist *TodoList) Contains(t *Todo) bool {
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, tsk := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
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)
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, tsk := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
for _, c := range tsk.Contexts {
|
|
|
|
if !added[c] {
|
|
|
|
ret = append(ret, c)
|
|
|
|
added[c] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-23 14:55:38 +00:00
|
|
|
sort.Strings(ret)
|
2023-08-23 14:17:16 +00:00
|
|
|
return ret
|
|
|
|
}
|
|
|
|
func (todolist *TodoList) GetProjects() []string {
|
|
|
|
var ret []string
|
|
|
|
added := make(map[string]bool)
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, tsk := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
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)
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, tsk := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
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)
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, tsk := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
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)
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, tsk := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
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()
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, v := range todolist.Todos {
|
2023-08-23 14:55:38 +00:00
|
|
|
if !v.Completed {
|
2023-08-23 16:10:03 +00:00
|
|
|
t.Todos = append(t.Todos, v)
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return &t
|
|
|
|
}
|
|
|
|
|
2023-09-07 18:06:22 +00:00
|
|
|
func (todolist *TodoList) GetNextId() int {
|
|
|
|
nextId := 0
|
|
|
|
for _, v := range todolist.Todos {
|
|
|
|
if v.Id > nextId {
|
|
|
|
nextId = v.Id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nextId + 1
|
|
|
|
}
|
|
|
|
|
2023-08-23 14:17:16 +00:00
|
|
|
// String returns a complete list of tasks in todo.txt format.
|
|
|
|
func (todolist *TodoList) String() string {
|
|
|
|
var ret string
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, todo := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
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) {
|
2023-09-07 18:06:22 +00:00
|
|
|
todo.Id = todolist.GetNextId()
|
2023-08-23 16:10:03 +00:00
|
|
|
todolist.Todos = append(todolist.Todos, todo)
|
2023-08-23 14:17:16 +00:00
|
|
|
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) {
|
2023-09-07 18:06:22 +00:00
|
|
|
for _, v := range todos {
|
|
|
|
todolist.AddTodo(v)
|
|
|
|
}
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
2023-08-23 16:10:03 +00:00
|
|
|
func (todolist *TodoList) Combine(other *TodoList) { todolist.AddTodos(other.Todos) }
|
2023-08-23 14:17:16 +00:00
|
|
|
|
|
|
|
// 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) {
|
2023-08-23 16:10:03 +00:00
|
|
|
for i := range todolist.Todos {
|
|
|
|
if todolist.Todos[i].Id == id {
|
|
|
|
return todolist.Todos[i], nil
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
2023-08-23 16:10:03 +00:00
|
|
|
for remIdx, t = range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
if t.Id == id {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
return errors.New("todo not found")
|
|
|
|
}
|
2023-08-23 16:10:03 +00:00
|
|
|
todolist.Todos = append(todolist.Todos[:remIdx], todolist.Todos[remIdx+1:]...)
|
2023-08-23 14:17:16 +00:00
|
|
|
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
|
2023-08-23 16:10:03 +00:00
|
|
|
for remIdx, t = range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
if t.String() == todo.String() {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
return errors.New("todo not found")
|
|
|
|
}
|
2023-08-23 16:10:03 +00:00
|
|
|
todolist.Todos = append(todolist.Todos[:remIdx], todolist.Todos[remIdx+1:]...)
|
2023-08-23 14:17:16 +00:00
|
|
|
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
|
2023-08-23 16:10:03 +00:00
|
|
|
for _, t := range todolist.Todos {
|
2023-08-23 14:17:16 +00:00
|
|
|
if predicate(t) {
|
2023-08-23 16:10:03 +00:00
|
|
|
newList.Todos = append(newList.Todos, t)
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return &newList
|
|
|
|
}
|
|
|
|
|
2023-09-07 18:06:22 +00:00
|
|
|
func (todolist *TodoList) ResetIds() {
|
|
|
|
todoId := 1
|
|
|
|
for _, v := range todolist.Todos {
|
|
|
|
v.Id = todoId
|
|
|
|
todoId++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-23 14:17:16 +00:00
|
|
|
// 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 {
|
2023-08-23 16:10:03 +00:00
|
|
|
todolist.Todos = []*Todo{} // Empty todolist
|
2023-08-23 14:17:16 +00:00
|
|
|
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++
|
2023-08-23 16:10:03 +00:00
|
|
|
todolist.Todos = append(todolist.Todos, todo)
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
|
|
|
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()
|
2023-09-07 18:06:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
todolist.ResetIds()
|
|
|
|
return nil
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2023-09-07 18:06:22 +00:00
|
|
|
if err := ioutil.WriteFile(filename, []byte(todolist.String()), 0640); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
todolist.ResetIds()
|
|
|
|
return nil
|
2023-08-23 14:17:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|