Initial commit

This commit is contained in:
Martin
2024-05-20 21:17:47 +02:00
commit d960f1f113
25 changed files with 1897 additions and 0 deletions

43
taskwarrior/config.go Normal file
View File

@ -0,0 +1,43 @@
package taskwarrior
import (
"fmt"
"log/slog"
"strings"
)
type TWConfig struct {
config map[string]string
}
func NewConfig(config []string) *TWConfig {
return &TWConfig{
config: parseConfig(config),
}
}
func (tc *TWConfig) Get(key string) string {
if _, ok := tc.config[key]; !ok {
slog.Debug(fmt.Sprintf("Key not found in config: %s", key))
return ""
}
return tc.config[key]
}
func parseConfig(config []string) map[string]string {
configMap := make(map[string]string)
for _, line := range config {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
configMap[key] = value
}
return configMap
}

41
taskwarrior/models.go Normal file
View File

@ -0,0 +1,41 @@
package taskwarrior
type Task struct {
Id int64 `json:"id"`
Uuid string `json:"uuid"`
Description string `json:"description"`
Project string `json:"project"`
Priority string `json:"priority"`
Status string `json:"status"`
Tags []string `json:"tags"`
Urgency float32 `json:"urgency"`
Due string `json:"due"`
Wait string `json:"wait"`
Scheduled string `json:"scheduled"`
End string `json:"end"`
Entry string `json:"entry"`
Modified string `json:"modified"`
}
type Tasks []*Task
type Context struct {
Name string
Active bool
ReadFilter string
WriteFilter string
}
type Contexts map[string]*Context
type Report struct {
Name string
Description string
Labels []string
Filter string
Sort string
Columns []string
Context bool
}
type Reports map[string]*Report

398
taskwarrior/taskwarrior.go Normal file
View File

@ -0,0 +1,398 @@
package taskwarrior
import (
"encoding/json"
"fmt"
"log/slog"
"os/exec"
"regexp"
"slices"
"strings"
"sync"
)
const (
twBinary = "task"
)
var (
tagBlacklist = map[string]struct{}{
"ACTIVE": {},
"ANNOTATED": {},
"BLOCKED": {},
"BLOCKING": {},
"CHILD": {},
"COMPLETED": {},
"DELETED": {},
"DUE": {},
"DUETODAY": {},
"INSTANCE": {},
"LATEST": {},
"MONTH": {},
"ORPHAN": {},
"OVERDUE": {},
"PARENT": {},
"PENDING": {},
"PRIORITY": {},
"PROJECT": {},
"QUARTER": {},
"READY": {},
"SCHEDULED": {},
"TAGGED": {},
"TEMPLATE": {},
"TODAY": {},
"TOMORROW": {},
"UDA": {},
"UNBLOCKED": {},
"UNTIL": {},
"WAITING": {},
"WEEK": {},
"YEAR": {},
"YESTERDAY": {},
}
)
type TaskWarrior interface {
GetConfig() *TWConfig
GetActiveContext() *Context
GetContext(context string) *Context
GetContexts() Contexts
SetContext(context *Context) error
GetProjects() []string
GetPriorities() []string
GetTags() []string
GetReport(report string) *Report
GetReports() Reports
GetTasks(report *Report, filter ...string) Tasks
AddTask(task *Task) error
}
type TaskSquire struct {
configLocation string
defaultArgs []string
config *TWConfig
reports Reports
contexts Contexts
mutex sync.Mutex
}
func NewTaskSquire(configLocation string) *TaskSquire {
if _, err := exec.LookPath(twBinary); err != nil {
slog.Error("Taskwarrior not found")
return nil
}
defaultArgs := []string{fmt.Sprintf("rc=%s", configLocation), "rc.verbose=nothing", "rc.dependency.confirmation=no", "rc.recurrence.confirmation=no", "rc.confirmation=no", "rc.json.array=1"}
ts := &TaskSquire{
configLocation: configLocation,
defaultArgs: defaultArgs,
mutex: sync.Mutex{},
}
ts.config = ts.extractConfig()
ts.reports = ts.extractReports()
ts.contexts = ts.extractContexts()
return ts
}
func (ts *TaskSquire) GetConfig() *TWConfig {
ts.mutex.Lock()
defer ts.mutex.Unlock()
return ts.config
}
func (ts *TaskSquire) GetTasks(report *Report, filter ...string) Tasks {
ts.mutex.Lock()
defer ts.mutex.Unlock()
args := ts.defaultArgs
if report.Context {
for _, context := range ts.contexts {
if context.Active && context.Name != "none" {
args = append(args, context.ReadFilter)
}
}
}
if filter != nil {
args = append(args, filter...)
}
cmd := exec.Command(twBinary, append(args, []string{"export", report.Name}...)...)
output, err := cmd.CombinedOutput()
if err != nil {
slog.Error("Failed getting report:", err)
return nil
}
tasks := make(Tasks, 0)
err = json.Unmarshal(output, &tasks)
if err != nil {
slog.Error("Failed unmarshalling tasks:", err)
return nil
}
return tasks
}
func (ts *TaskSquire) GetContext(context string) *Context {
ts.mutex.Lock()
defer ts.mutex.Unlock()
if context, ok := ts.contexts[context]; ok {
return context
} else {
slog.Error(fmt.Sprintf("Context not found: %s", context.Name))
return nil
}
}
func (ts *TaskSquire) GetActiveContext() *Context {
ts.mutex.Lock()
defer ts.mutex.Unlock()
for _, context := range ts.contexts {
if context.Active {
return context
}
}
return ts.contexts["none"]
}
func (ts *TaskSquire) GetContexts() Contexts {
ts.mutex.Lock()
defer ts.mutex.Unlock()
return ts.contexts
}
func (ts *TaskSquire) GetProjects() []string {
ts.mutex.Lock()
defer ts.mutex.Unlock()
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_projects"}...)...)
output, err := cmd.CombinedOutput()
if err != nil {
slog.Error("Failed getting projects:", err)
return nil
}
projects := make([]string, 0)
for _, project := range strings.Split(string(output), "\n") {
if project != "" {
projects = append(projects, project)
}
}
slices.Sort(projects)
return projects
}
func (ts *TaskSquire) GetPriorities() []string {
ts.mutex.Lock()
defer ts.mutex.Unlock()
priorities := make([]string, 0)
for _, priority := range strings.Split(ts.config.Get("uda.priority.values"), ",") {
if priority != "" {
priorities = append(priorities, priority)
}
}
return priorities
}
func (ts *TaskSquire) GetTags() []string {
ts.mutex.Lock()
defer ts.mutex.Unlock()
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_tags"}...)...)
output, err := cmd.CombinedOutput()
if err != nil {
slog.Error("Failed getting tags:", err)
return nil
}
tags := make([]string, 0)
for _, tag := range strings.Split(string(output), "\n") {
if _, ok := tagBlacklist[tag]; !ok && tag != "" {
tags = append(tags, tag)
}
}
slices.Sort(tags)
return tags
}
func (ts *TaskSquire) GetReport(report string) *Report {
ts.mutex.Lock()
defer ts.mutex.Unlock()
return ts.reports[report]
}
func (ts *TaskSquire) GetReports() Reports {
ts.mutex.Lock()
defer ts.mutex.Unlock()
return ts.reports
}
func (ts *TaskSquire) SetContext(context *Context) error {
ts.mutex.Lock()
defer ts.mutex.Unlock()
cmd := exec.Command(twBinary, []string{"context", context.Name}...)
if err := cmd.Run(); err != nil {
slog.Error("Failed setting context:", err)
return err
}
// TODO: optimize this; there should be no need to re-extract everything
ts.config = ts.extractConfig()
ts.contexts = ts.extractContexts()
return nil
}
func (ts *TaskSquire) AddTask(task *Task) error {
ts.mutex.Lock()
defer ts.mutex.Unlock()
addArgs := []string{"add"}
if task.Description == "" {
slog.Error("Task description is required")
return nil
} else {
addArgs = append(addArgs, task.Description)
}
if task.Priority != "" && task.Priority != "(none)" {
addArgs = append(addArgs, fmt.Sprintf("priority:%s", task.Priority))
}
if task.Project != "" && task.Project != "(none)" {
addArgs = append(addArgs, fmt.Sprintf("project:%s", task.Project))
}
if task.Tags != nil {
for _, tag := range task.Tags {
addArgs = append(addArgs, fmt.Sprintf("+%s", tag))
}
}
if task.Due != "" {
addArgs = append(addArgs, fmt.Sprintf("due:%s", task.Due))
}
cmd := exec.Command(twBinary, append(ts.defaultArgs, addArgs...)...)
err := cmd.Run()
if err != nil {
slog.Error("Failed adding task:", err)
}
// TODO remove error?
return nil
}
func (ts *TaskSquire) extractConfig() *TWConfig {
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...)
output, err := cmd.CombinedOutput()
if err != nil {
slog.Error("Failed getting config:", err)
return nil
}
return NewConfig(strings.Split(string(output), "\n"))
}
func (ts *TaskSquire) extractReports() Reports {
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_config"}...)...)
output, err := cmd.CombinedOutput()
if err != nil {
return nil
}
availableReports := extractReports(string(output))
reports := make(Reports)
for _, report := range availableReports {
reports[report] = &Report{
Name: report,
Description: ts.config.Get(fmt.Sprintf("report.%s.description", report)),
Labels: strings.Split(ts.config.Get(fmt.Sprintf("report.%s.labels", report)), ","),
Filter: ts.config.Get(fmt.Sprintf("report.%s.filter", report)),
Sort: ts.config.Get(fmt.Sprintf("report.%s.sort", report)),
Columns: strings.Split(ts.config.Get(fmt.Sprintf("report.%s.columns", report)), ","),
Context: ts.config.Get(fmt.Sprintf("report.%s.context", report)) == "1",
}
}
return reports
}
func extractReports(config string) []string {
re := regexp.MustCompile(`report\.([^.]+)\.[^.]+`)
matches := re.FindAllStringSubmatch(config, -1)
uniques := make(map[string]struct{})
for _, match := range matches {
uniques[match[1]] = struct{}{}
}
var reports []string
for part := range uniques {
reports = append(reports, part)
}
slices.Sort(reports)
return reports
}
func (ts *TaskSquire) extractContexts() Contexts {
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_context"}...)...)
output, err := cmd.CombinedOutput()
if err != nil {
slog.Error("Failed getting contexts:", err)
return nil
}
activeContext := ts.config.Get("context")
if activeContext == "" {
activeContext = "none"
}
contexts := make(Contexts)
contexts["none"] = &Context{
Name: "none",
Active: activeContext == "none",
ReadFilter: "",
WriteFilter: "",
}
for _, context := range strings.Split(string(output), "\n") {
if context == "" {
continue
}
contexts[context] = &Context{
Name: context,
Active: activeContext == context,
ReadFilter: ts.config.Get(fmt.Sprintf("context.%s.read", context)),
WriteFilter: ts.config.Get(fmt.Sprintf("context.%s.write", context)),
}
}
return contexts
}

View File

@ -0,0 +1,65 @@
package taskwarrior
import (
"fmt"
"os"
"testing"
)
func TaskWarriorTestSetup(dir string) {
// Create a taskrc file
taskrc := fmt.Sprintf("%s/taskrc", dir)
taskrcContents := fmt.Sprintf("data.location=%s\n", dir)
os.WriteFile(taskrc, []byte(taskrcContents), 0644)
}
func TestTaskSquire_GetContext(t *testing.T) {
dir := t.TempDir()
fmt.Printf("dir: %s", dir)
TaskWarriorTestSetup(dir)
type fields struct {
configLocation string
}
tests := []struct {
name string
fields fields
prep func()
want string
}{
{
name: "Test without context",
fields: fields{
configLocation: fmt.Sprintf("%s/taskrc", dir),
},
prep: func() {},
want: "none",
},
{
name: "Test with context",
fields: fields{
configLocation: fmt.Sprintf("%s/taskrc", dir),
},
prep: func() {
f, err := os.OpenFile(fmt.Sprintf("%s/taskrc", dir), os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
t.Errorf("Failed to open file: %s", err)
}
defer f.Close()
if _, err := f.Write([]byte("context=test\ncontext.test.read=+test\ncontext.test.write=+test")); err != nil {
t.Error("Failed to write to file")
}
},
want: "test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.prep()
ts := NewTaskSquire(tt.fields.configLocation)
if got := ts.GetActiveContext(); got.Name != tt.want {
t.Errorf("TaskSquire.GetContext() = %v, want %v", got, tt.want)
}
})
}
}