Integrate timewarrior
This commit is contained in:
305
timewarrior/timewarrior.go
Normal file
305
timewarrior/timewarrior.go
Normal file
@ -0,0 +1,305 @@
|
||||
// TODO: error handling
|
||||
// TODO: split combinedOutput and handle stderr differently
|
||||
package timewarrior
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
twBinary = "timew"
|
||||
)
|
||||
|
||||
type TimeWarrior interface {
|
||||
GetConfig() *TWConfig
|
||||
|
||||
GetTags() []string
|
||||
|
||||
GetIntervals(filter ...string) Intervals
|
||||
StartTracking(tags []string) error
|
||||
StopTracking() error
|
||||
ContinueTracking() error
|
||||
CancelTracking() error
|
||||
DeleteInterval(id int) error
|
||||
ModifyInterval(interval *Interval) error
|
||||
GetSummary(filter ...string) string
|
||||
GetActive() *Interval
|
||||
|
||||
Undo()
|
||||
}
|
||||
|
||||
type TimeSquire struct {
|
||||
configLocation string
|
||||
defaultArgs []string
|
||||
config *TWConfig
|
||||
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewTimeSquire(configLocation string) *TimeSquire {
|
||||
if _, err := exec.LookPath(twBinary); err != nil {
|
||||
slog.Error("Timewarrior not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
ts := &TimeSquire{
|
||||
configLocation: configLocation,
|
||||
defaultArgs: []string{},
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
ts.config = ts.extractConfig()
|
||||
|
||||
return ts
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) GetConfig() *TWConfig {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
return ts.config
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) 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)
|
||||
lines := strings.Split(string(output), "\n")
|
||||
|
||||
// Skip header lines and parse tag names
|
||||
for i, line := range lines {
|
||||
if i < 3 || line == "" {
|
||||
continue
|
||||
}
|
||||
// Tags are space-separated, first column is the tag name
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 {
|
||||
tags = append(tags, fields[0])
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(tags)
|
||||
return tags
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) GetIntervals(filter ...string) Intervals {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
args := append(ts.defaultArgs, "export")
|
||||
if filter != nil {
|
||||
args = append(args, filter...)
|
||||
}
|
||||
|
||||
cmd := exec.Command(twBinary, args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
slog.Error("Failed getting intervals:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
intervals := make(Intervals, 0)
|
||||
err = json.Unmarshal(output, &intervals)
|
||||
if err != nil {
|
||||
slog.Error("Failed unmarshalling intervals:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Assign IDs based on reverse chronological order
|
||||
for i := range intervals {
|
||||
intervals[i].ID = len(intervals) - i
|
||||
}
|
||||
|
||||
return intervals
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) StartTracking(tags []string) error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
if len(tags) == 0 {
|
||||
return fmt.Errorf("at least one tag is required")
|
||||
}
|
||||
|
||||
args := append(ts.defaultArgs, "start")
|
||||
args = append(args, tags...)
|
||||
|
||||
cmd := exec.Command(twBinary, args...)
|
||||
if err := cmd.Run(); err != nil {
|
||||
slog.Error("Failed starting tracking:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) StopTracking() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, "stop")...)
|
||||
if err := cmd.Run(); err != nil {
|
||||
slog.Error("Failed stopping tracking:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) ContinueTracking() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, "continue")...)
|
||||
if err := cmd.Run(); err != nil {
|
||||
slog.Error("Failed continuing tracking:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) CancelTracking() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, "cancel")...)
|
||||
if err := cmd.Run(); err != nil {
|
||||
slog.Error("Failed canceling tracking:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) DeleteInterval(id int) error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"delete", fmt.Sprintf("@%d", id)}...)...)
|
||||
if err := cmd.Run(); err != nil {
|
||||
slog.Error("Failed deleting interval:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) ModifyInterval(interval *Interval) error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
// Export the modified interval
|
||||
intervals, err := json.Marshal(Intervals{interval})
|
||||
if err != nil {
|
||||
slog.Error("Failed marshalling interval:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Import the modified interval
|
||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"import"}...)...)
|
||||
cmd.Stdin = bytes.NewBuffer(intervals)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
slog.Error("Failed modifying interval:", err, string(out))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) GetSummary(filter ...string) string {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
args := append(ts.defaultArgs, "summary")
|
||||
if filter != nil {
|
||||
args = append(args, filter...)
|
||||
}
|
||||
|
||||
cmd := exec.Command(twBinary, args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
slog.Error("Failed getting summary:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) GetActive() *Interval {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"get", "dom.active"}...)...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil || string(output) == "0\n" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the active interval
|
||||
intervals := ts.GetIntervals()
|
||||
for _, interval := range intervals {
|
||||
if interval.End == "" {
|
||||
return interval
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) Undo() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"undo"}...)...)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
slog.Error("Failed undoing:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TimeSquire) 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 extractTags(config string) []string {
|
||||
re := regexp.MustCompile(`tag\.([^.]+)\.[^.]+`)
|
||||
matches := re.FindAllStringSubmatch(config, -1)
|
||||
uniques := make(map[string]struct{})
|
||||
for _, match := range matches {
|
||||
uniques[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
for tag := range uniques {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
slices.Sort(tags)
|
||||
return tags
|
||||
}
|
||||
Reference in New Issue
Block a user