Integrate timewarrior
This commit is contained in:
71
GEMINI.md
Normal file
71
GEMINI.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Tasksquire
|
||||
|
||||
## Project Overview
|
||||
|
||||
Tasksquire is a Terminal User Interface (TUI) for [Taskwarrior](https://taskwarrior.org/), built using Go and the [Charm](https://charm.sh/) ecosystem (Bubble Tea, Lip Gloss, Huh). It provides a visual and interactive way to manage your tasks, contexts, and reports directly from the terminal.
|
||||
|
||||
The application functions as a wrapper around the `task` command-line tool, parsing its output (JSON, config) and executing commands to read and modify task data.
|
||||
|
||||
## Architecture
|
||||
|
||||
The project follows the standard [Bubble Tea](https://github.com/charmbracelet/bubbletea) Model-View-Update (MVU) architecture.
|
||||
|
||||
### Key Directories & Files
|
||||
|
||||
* **`main.go`**: The entry point of the application. It initializes the `TaskSquire` wrapper, sets up logging, and starts the Bubble Tea program with the `MainPage`.
|
||||
* **`taskwarrior/`**: Contains the logic for interacting with the Taskwarrior CLI.
|
||||
* `taskwarrior.go`: The core wrapper (`TaskSquire` struct) that executes `task` commands (`export`, `add`, `modify`, etc.) and parses results.
|
||||
* `models.go`: Defines the Go structs matching Taskwarrior's data model (Tasks, Reports, Config).
|
||||
* **`pages/`**: Contains the different views of the application.
|
||||
* `main.go`: The top-level component (`MainPage`) that manages routing/switching between different pages.
|
||||
* `report.go`: Displays lists of tasks (Taskwarrior reports).
|
||||
* `taskEditor.go`: UI for creating or editing tasks.
|
||||
* **`common/`**: Shared utilities, global state, and data structures used across the application.
|
||||
* **`components/`**: Reusable UI components (e.g., inputs, tables).
|
||||
* **`timewarrior/`**: Contains logic for integration with Timewarrior (likely in progress or planned).
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* **Go**: Version 1.22 or higher.
|
||||
* **Taskwarrior**: The `task` binary must be installed and available in your system's `PATH`.
|
||||
|
||||
### Commands
|
||||
|
||||
To run the application directly:
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
To build a binary:
|
||||
|
||||
```bash
|
||||
go build -o tasksquire main.go
|
||||
```
|
||||
|
||||
### Nix Support
|
||||
|
||||
This project includes a `flake.nix` for users of the Nix package manager. You can enter a development shell with all dependencies (Go, tools) by running:
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Tasksquire respects your existing Taskwarrior configuration (`.taskrc`). It looks for the configuration file in the following order:
|
||||
|
||||
1. `TASKRC` environment variable.
|
||||
2. `$HOME/.taskrc`
|
||||
3. `$HOME/.config/task/taskrc`
|
||||
|
||||
Logging is written to `app.log` in the current working directory.
|
||||
|
||||
## Development Conventions
|
||||
|
||||
* **UI Framework**: Uses [Bubble Tea](https://github.com/charmbracelet/bubbletea) for the TUI loop.
|
||||
* **Styling**: Uses [Lip Gloss](https://github.com/charmbracelet/lipgloss) for terminal styling.
|
||||
* **Forms**: Uses [Huh](https://github.com/charmbracelet/huh) for form inputs.
|
||||
* **Logging**: Uses `log/slog` for structured logging.
|
||||
14
README.md
14
README.md
@ -1,12 +1,10 @@
|
||||
- [ ] Use JJ
|
||||
- [ ] Add tag manager
|
||||
- [ ] Edit default tags
|
||||
- Default tags should be defined in the config and always displayed in the tag picker
|
||||
# TODO
|
||||
- [>] Add default tags
|
||||
- Default tags should be defined in the config and always displayed in the tag picker
|
||||
- [ ] Add project manager
|
||||
- [ ] Add projects that are always displayed in the project picker
|
||||
- Saved in config
|
||||
- [ ] Remove/archive projects
|
||||
- [ ] Update to bubbletea 2.0
|
||||
- [ ] Integrate timewarrior
|
||||
- [ ] Add default timetracking items when addind projects
|
||||
- [ ] Create interface for timewarrior input
|
||||
@ -14,3 +12,9 @@
|
||||
- Combine by project
|
||||
- Combine by task names
|
||||
- Combine by tags
|
||||
- [ ] Add tag manager
|
||||
- [ ] Edit default tags
|
||||
- [ ] Update to bubbletea 2.0
|
||||
|
||||
# Done
|
||||
- [x] Use JJ
|
||||
78
timewarrior/config.go
Normal file
78
timewarrior/config.go
Normal file
@ -0,0 +1,78 @@
|
||||
package timewarrior
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TWConfig struct {
|
||||
config map[string]string
|
||||
}
|
||||
|
||||
var (
|
||||
defaultConfig = map[string]string{
|
||||
"uda.timesquire.default.tag": "",
|
||||
}
|
||||
)
|
||||
|
||||
func NewConfig(config []string) *TWConfig {
|
||||
cfg := parseConfig(config)
|
||||
|
||||
for key, value := range defaultConfig {
|
||||
if _, ok := cfg[key]; !ok {
|
||||
cfg[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return &TWConfig{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *TWConfig) GetConfig() map[string]string {
|
||||
return tc.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 {
|
||||
// Skip empty lines and comments
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Timewarrior config format: key = value or key: value
|
||||
var key, value string
|
||||
if strings.Contains(line, "=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key = strings.TrimSpace(parts[0])
|
||||
value = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.Contains(line, ":") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key = strings.TrimSpace(parts[0])
|
||||
value = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
configMap[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return configMap
|
||||
}
|
||||
277
timewarrior/models.go
Normal file
277
timewarrior/models.go
Normal file
@ -0,0 +1,277 @@
|
||||
package timewarrior
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
dtformat = "20060102T150405Z"
|
||||
)
|
||||
|
||||
type Intervals []*Interval
|
||||
|
||||
type Interval struct {
|
||||
ID int `json:"-"`
|
||||
Start string `json:"start,omitempty"`
|
||||
End string `json:"end,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func NewInterval() *Interval {
|
||||
return &Interval{
|
||||
Tags: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Interval) GetString(field string) string {
|
||||
switch field {
|
||||
case "id":
|
||||
return strconv.Itoa(i.ID)
|
||||
|
||||
case "start":
|
||||
return formatDate(i.Start, "formatted")
|
||||
|
||||
case "end":
|
||||
if i.End == "" {
|
||||
return "now"
|
||||
}
|
||||
return formatDate(i.End, "formatted")
|
||||
|
||||
case "tags":
|
||||
if len(i.Tags) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(i.Tags, " ")
|
||||
|
||||
case "duration":
|
||||
return i.GetDuration()
|
||||
|
||||
case "active":
|
||||
if i.End == "" {
|
||||
return "●"
|
||||
}
|
||||
return ""
|
||||
|
||||
default:
|
||||
slog.Error(fmt.Sprintf("Field not implemented: %s", field))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Interval) GetDuration() string {
|
||||
start, err := time.Parse(dtformat, i.Start)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse start time:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var end time.Time
|
||||
if i.End == "" {
|
||||
end = time.Now()
|
||||
} else {
|
||||
end, err = time.Parse(dtformat, i.End)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse end time:", err)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
duration := end.Sub(start)
|
||||
return formatDuration(duration)
|
||||
}
|
||||
|
||||
func (i *Interval) GetStartTime() time.Time {
|
||||
dt, err := time.Parse(dtformat, i.Start)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse time:", err)
|
||||
return time.Time{}
|
||||
}
|
||||
return dt
|
||||
}
|
||||
|
||||
func (i *Interval) GetEndTime() time.Time {
|
||||
if i.End == "" {
|
||||
return time.Now()
|
||||
}
|
||||
dt, err := time.Parse(dtformat, i.End)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse time:", err)
|
||||
return time.Time{}
|
||||
}
|
||||
return dt
|
||||
}
|
||||
|
||||
func (i *Interval) HasTag(tag string) bool {
|
||||
for _, t := range i.Tags {
|
||||
if t == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *Interval) AddTag(tag string) {
|
||||
if !i.HasTag(tag) {
|
||||
i.Tags = append(i.Tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Interval) RemoveTag(tag string) {
|
||||
for idx, t := range i.Tags {
|
||||
if t == tag {
|
||||
i.Tags = append(i.Tags[:idx], i.Tags[idx+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Interval) IsActive() bool {
|
||||
return i.End == ""
|
||||
}
|
||||
|
||||
func formatDate(date string, format string) string {
|
||||
if date == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
dt, err := time.Parse(dtformat, date)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse time:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "formatted", "":
|
||||
return dt.Format("2006-01-02 15:04")
|
||||
case "time":
|
||||
return dt.Format("15:04")
|
||||
case "date":
|
||||
return dt.Format("2006-01-02")
|
||||
case "iso":
|
||||
return dt.Format("2006-01-02T150405Z")
|
||||
case "epoch":
|
||||
return strconv.FormatInt(dt.Unix(), 10)
|
||||
case "age":
|
||||
return parseDurationVague(time.Since(dt))
|
||||
case "relative":
|
||||
return parseDurationVague(time.Until(dt))
|
||||
default:
|
||||
slog.Error(fmt.Sprintf("Date format not implemented: %s", format))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
hours := int(d.Hours())
|
||||
minutes := int(d.Minutes()) % 60
|
||||
seconds := int(d.Seconds()) % 60
|
||||
|
||||
if hours > 0 {
|
||||
return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
return fmt.Sprintf("%d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
func parseDurationVague(d time.Duration) string {
|
||||
dur := d.Round(time.Second).Abs()
|
||||
days := dur.Hours() / 24
|
||||
|
||||
var formatted string
|
||||
if dur >= time.Hour*24*365 {
|
||||
formatted = fmt.Sprintf("%.1fy", days/365)
|
||||
} else if dur >= time.Hour*24*90 {
|
||||
formatted = strconv.Itoa(int(math.Round(days/30))) + "mo"
|
||||
} else if dur >= time.Hour*24*7 {
|
||||
formatted = strconv.Itoa(int(math.Round(days/7))) + "w"
|
||||
} else if dur >= time.Hour*24 {
|
||||
formatted = strconv.Itoa(int(days)) + "d"
|
||||
} else if dur >= time.Hour {
|
||||
formatted = strconv.Itoa(int(dur.Round(time.Hour).Hours())) + "h"
|
||||
} else if dur >= time.Minute {
|
||||
formatted = strconv.Itoa(int(dur.Round(time.Minute).Minutes())) + "min"
|
||||
} else if dur >= time.Second {
|
||||
formatted = strconv.Itoa(int(dur.Round(time.Second).Seconds())) + "s"
|
||||
}
|
||||
|
||||
if d < 0 {
|
||||
formatted = "-" + formatted
|
||||
}
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
var (
|
||||
dateFormats = []string{
|
||||
"2006-01-02",
|
||||
"2006-01-02T15:04",
|
||||
"20060102T150405Z",
|
||||
}
|
||||
|
||||
specialDateFormats = []string{
|
||||
"",
|
||||
"now",
|
||||
"today",
|
||||
"yesterday",
|
||||
"tomorrow",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri",
|
||||
"sat",
|
||||
"sun",
|
||||
}
|
||||
)
|
||||
|
||||
func ValidateDate(s string) error {
|
||||
for _, f := range dateFormats {
|
||||
if _, err := time.Parse(f, s); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range specialDateFormats {
|
||||
if s == f {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid date")
|
||||
}
|
||||
|
||||
func ValidateDuration(s string) error {
|
||||
// TODO: implement proper duration validation
|
||||
// Should accept formats like: 1h, 30m, 1h30m, etc.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Summary represents time tracking summary data
|
||||
type Summary struct {
|
||||
Range string
|
||||
TotalTime time.Duration
|
||||
ByTag map[string]time.Duration
|
||||
}
|
||||
|
||||
func (s *Summary) GetTotalString() string {
|
||||
return formatDuration(s.TotalTime)
|
||||
}
|
||||
|
||||
func (s *Summary) GetTagTime(tag string) string {
|
||||
if duration, ok := s.ByTag[tag]; ok {
|
||||
return formatDuration(duration)
|
||||
}
|
||||
return "0:00"
|
||||
}
|
||||
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