Initial commit
This commit is contained in:
398
taskwarrior/taskwarrior.go
Normal file
398
taskwarrior/taskwarrior.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user