Home

todo @main - refs - log -
-
https://git.jolheiser.com/todo.git
TODO management CLI using Vikunja
todo / main.go
- raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package main

import (
	"cmp"
	"errors"
	"flag"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/peterbourgon/ff/v3"
	"go.jolheiser.com/ffjsonnet"
	"go.jolheiser.com/todo/vikunja"
)

var defaultConfig = func() string {
	cfgDir, err := os.UserConfigDir()
	if err != nil {
		return "todo.jsonnet"
	}
	return filepath.Join(cfgDir, "todo", "todo.jsonnet")
}()

type args struct {
	url     string
	token   string
	project string
}

func maine() error {
	var args args
	fs := flag.NewFlagSet("todo", flag.ExitOnError)
	fs.String("config", defaultConfig, "Config file")
	fs.StringVar(&args.url, "url", args.url, "Base URL for Vikunja")
	fs.StringVar(&args.url, "u", args.url, "--url")
	fs.StringVar(&args.token, "token", args.token, "API Token")
	fs.StringVar(&args.token, "t", args.token, "--token")
	fs.StringVar(&args.project, "project", args.project, "Project to work with")
	fs.StringVar(&args.project, "p", args.project, "--project")
	if err := ff.Parse(fs, os.Args[1:],
		ff.WithEnvVarPrefix("TODO"),
		ff.WithConfigFileFlag("config"),
		ff.WithAllowMissingConfigFile(true),
		ff.WithConfigFileParser(ffjsonnet.Parser),
	); err != nil {
		return err
	}
	client, err := vikunja.New(args.url, args.token)
	if err != nil {
		return err
	}
	if fs.NArg() == 0 {
		return list(client, args.project)
	}

	switch strings.ToLower(fs.Arg(0)) {
	case "list", "ls":
		return list(client, args.project)
	case "add", "create":
		return add(client, args.project, fs.Args()[1:]...)
	case "complete", "finish", "done":
		return finish(client, strings.Join(fs.Args()[1:], " "))
	}

	return nil
}

func add(client *vikunja.Client, project string, args ...string) error {
	if len(args) == 0 {
		return errors.New("new todo requires text")
	}
	defaultProject := project
	projects, err := client.Projects()
	if err != nil {
		return err
	}
	title := strings.Join(args, " ")
	if strings.HasPrefix(args[0], "+") {
		project = strings.TrimPrefix(args[0], "+")
		title = strings.Join(args[1:], " ")
	}
	var pid int
	for _, p := range projects {
		if strings.EqualFold(p.Title, project) {
			pid = p.ID
			break
		}
		if strings.EqualFold(p.Title, defaultProject) {
			pid = p.ID
		}
	}
	if pid == 0 {
		return fmt.Errorf("no project found %q", project)
	}

	return client.AddTask(pid, title)
}

func finish(client *vikunja.Client, title string) error {
	tasks, err := client.Tasks()
	if err != nil {
		return err
	}
	finishable := make([]vikunja.Task, 0)
	for _, task := range tasks {
		if strings.Contains(strings.ToLower(task.Title), strings.ToLower(title)) {
			finishable = append(finishable, task)
		}
	}
	if len(finishable) == 0 {
		return errors.New("could not find task to finish")
	}
	if len(finishable) > 1 {
		return fmt.Errorf("found multiple tasks that match: %v", finishable)
	}
	return client.FinishTask(finishable[0].ID)
}

func list(client *vikunja.Client, projectFilter string) error {
	projects, err := client.Projects()
	if err != nil {
		return err
	}
	pids := make(map[int]vikunja.Project)
	for _, project := range projects {
		pids[project.ID] = project
	}
	tasks, err := client.Tasks()
	if err != nil {
		return err
	}
	for idx, task := range tasks {
		tasks[idx].Project = pids[task.ProjectID]
	}
	slices.SortFunc(tasks, func(a, b vikunja.Task) int {
		return cmp.Compare(a.Project.Title, b.Project.Title)
	})
	var project string
	for _, task := range tasks {
		if projectFilter != "" && !strings.EqualFold(projectFilter, task.Project.Title) {
			continue
		}
		if task.Project.Title != project {
			project = task.Project.Title
			fmt.Println(project)
		}
		fmt.Printf("- %s\n", task.Title)
	}
	return nil
}

func main() {
	if err := maine(); err != nil {
		slog.Error("error running todo", slog.Any("error", err))
		os.Exit(1)
	}
}