go-todotxt/todolist.go

330 lines
8.1 KiB
Go

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)
}