Initial commit
This commit is contained in:
43
taskwarrior/config.go
Normal file
43
taskwarrior/config.go
Normal 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
41
taskwarrior/models.go
Normal 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
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
|
||||
}
|
||||
65
taskwarrior/taskwarrior_test.go
Normal file
65
taskwarrior/taskwarrior_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user