Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Connect to server",
|
||||||
|
"type": "go",
|
||||||
|
"request": "attach",
|
||||||
|
"mode": "remote",
|
||||||
|
"remotePath": "${workspaceFolder}",
|
||||||
|
"port": 43000,
|
||||||
|
"host": "127.0.0.1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
common/common.go
Normal file
27
common/common.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Common struct {
|
||||||
|
Ctx context.Context
|
||||||
|
PageStack *Stack[tea.Model]
|
||||||
|
TW taskwarrior.TaskWarrior
|
||||||
|
Keymap *Keymap
|
||||||
|
Styles *Styles
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommon(ctx context.Context, tw taskwarrior.TaskWarrior) *Common {
|
||||||
|
return &Common{
|
||||||
|
Ctx: ctx,
|
||||||
|
PageStack: NewStack[tea.Model](),
|
||||||
|
TW: tw,
|
||||||
|
Keymap: NewKeymap(),
|
||||||
|
Styles: NewStyles(tw.GetConfig()),
|
||||||
|
}
|
||||||
|
}
|
||||||
62
common/keymap.go
Normal file
62
common/keymap.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keymap is a collection of key bindings.
|
||||||
|
type Keymap struct {
|
||||||
|
Quit key.Binding
|
||||||
|
Back key.Binding
|
||||||
|
Add key.Binding
|
||||||
|
Edit key.Binding
|
||||||
|
SetReport key.Binding
|
||||||
|
SetContext key.Binding
|
||||||
|
SetProject key.Binding
|
||||||
|
Select key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeymap creates a new Keymap.
|
||||||
|
func NewKeymap() *Keymap {
|
||||||
|
return &Keymap{
|
||||||
|
Quit: key.NewBinding(
|
||||||
|
key.WithKeys("q", "ctrl+c"),
|
||||||
|
key.WithHelp("q, ctrl+c", "Quit"),
|
||||||
|
),
|
||||||
|
|
||||||
|
Back: key.NewBinding(
|
||||||
|
key.WithKeys("esc"),
|
||||||
|
key.WithHelp("esc", "Back"),
|
||||||
|
),
|
||||||
|
|
||||||
|
Add: key.NewBinding(
|
||||||
|
key.WithKeys("a"),
|
||||||
|
key.WithHelp("a", "Add new task"),
|
||||||
|
),
|
||||||
|
|
||||||
|
Edit: key.NewBinding(
|
||||||
|
key.WithKeys("e"),
|
||||||
|
key.WithHelp("e", "Edit task"),
|
||||||
|
),
|
||||||
|
|
||||||
|
SetReport: key.NewBinding(
|
||||||
|
key.WithKeys("r"),
|
||||||
|
key.WithHelp("r", "Set report"),
|
||||||
|
),
|
||||||
|
|
||||||
|
SetContext: key.NewBinding(
|
||||||
|
key.WithKeys("c"),
|
||||||
|
key.WithHelp("c", "Set context"),
|
||||||
|
),
|
||||||
|
|
||||||
|
SetProject: key.NewBinding(
|
||||||
|
key.WithKeys("p"),
|
||||||
|
key.WithHelp("p", "Set project"),
|
||||||
|
),
|
||||||
|
|
||||||
|
Select: key.NewBinding(
|
||||||
|
key.WithKeys("enter"),
|
||||||
|
key.WithHelp("enter", "Select"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
40
common/stack.go
Normal file
40
common/stack.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stack[T any] struct {
|
||||||
|
items []T
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStack[T any]() *Stack[T] {
|
||||||
|
return &Stack[T]{
|
||||||
|
items: make([]T, 0),
|
||||||
|
mutex: sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack[T]) Push(item T) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.items = append(s.items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack[T]) Pop() (T, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(s.items) == 0 {
|
||||||
|
var empty T
|
||||||
|
return empty, errors.New("stack is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
item := s.items[len(s.items)-1]
|
||||||
|
s.items = s.items[:len(s.items)-1]
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
25
common/styles.go
Normal file
25
common/styles.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Styles struct {
|
||||||
|
Main lipgloss.Style
|
||||||
|
|
||||||
|
ActiveTask lipgloss.Style
|
||||||
|
InactiveTask lipgloss.Style
|
||||||
|
NormalTask lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStyles(config *taskwarrior.TWConfig) *Styles {
|
||||||
|
return &Styles{
|
||||||
|
Main: lipgloss.NewStyle().Foreground(lipgloss.Color("241")),
|
||||||
|
|
||||||
|
ActiveTask: lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Background(lipgloss.Color("236")),
|
||||||
|
InactiveTask: lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Background(lipgloss.Color("236")),
|
||||||
|
NormalTask: lipgloss.NewStyle().Foreground(lipgloss.Color("241")),
|
||||||
|
}
|
||||||
|
}
|
||||||
60
flake.lock
generated
Normal file
60
flake.lock
generated
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1715787315,
|
||||||
|
"narHash": "sha256-cYApT0NXJfqBkKcci7D9Kr4CBYZKOQKDYA23q8XNuWg=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "33d1e753c82ffc557b4a585c77de43d4c922ebb5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
39
flake.nix
Normal file
39
flake.nix
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
description = "Tasksquire";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
|
||||||
|
buildDeps = with pkgs; [
|
||||||
|
go_1_22
|
||||||
|
gcc
|
||||||
|
];
|
||||||
|
|
||||||
|
devDeps = with pkgs; buildDeps ++ [
|
||||||
|
gotools
|
||||||
|
golangci-lint
|
||||||
|
gopls
|
||||||
|
go-outline
|
||||||
|
gopkgs
|
||||||
|
go-tools
|
||||||
|
gotests
|
||||||
|
delve
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = devDeps;
|
||||||
|
CGO_CFLAGS="-O";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
33
go.mod
Normal file
33
go.mod
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module tasksquire
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/catppuccin/go v0.2.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v0.18.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v0.26.1 // indirect
|
||||||
|
github.com/charmbracelet/huh v0.3.0 // indirect
|
||||||
|
github.com/charmbracelet/huh/spinner v0.0.0-20240508140610-13957916abf0 // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb // indirect
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240515162549-69ee4f765313 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495 // indirect
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/ethanefung/bubble-datepicker v0.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/moustachioed/go-taskwarrior v0.0.0-20220111032313-0ea4f466b47c // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/term v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.15.0 // indirect
|
||||||
|
)
|
||||||
78
go.sum
Normal file
78
go.sum
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||||
|
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
|
github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 h1:6nVCV8pqGaeyxetur3gpX3AAaiyKgzjIoCPV3NXKZBE=
|
||||||
|
github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o=
|
||||||
|
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||||
|
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||||
|
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||||
|
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||||
|
github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
|
||||||
|
github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo=
|
||||||
|
github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
|
||||||
|
github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA=
|
||||||
|
github.com/charmbracelet/huh/spinner v0.0.0-20240508140610-13957916abf0 h1:79JTuYRirtyCn9ac6rzPt5AQKtBDFc1gKxpw0wBrI+Y=
|
||||||
|
github.com/charmbracelet/huh/spinner v0.0.0-20240508140610-13957916abf0/go.mod h1:Zxt9FH6togK9kY71pRJGtmyNkJ1eIWdK1gRaXrS/FKA=
|
||||||
|
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||||
|
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb h1:Hs3xzxHuruNT2Iuo87iS40c0PhLqpnUKBI6Xw6Ad3wQ=
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240515162549-69ee4f765313 h1:3RsFsshW5j6I2GcEg7Qy//ZOFBqmwTqQ/KIFpU8kiIM=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240515162549-69ee4f765313/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
|
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495 h1:+0U9qX8Pv8KiYgRxfBvORRjgBzLgHMjtElP4O0PyKYA=
|
||||||
|
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495/go.mod h1:qeR6w1zITbkF7vEhcx0CqX5GfnIiQloJWQghN6HfP+c=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/ethanefung/bubble-datepicker v0.1.0 h1:dOD6msw3cWZv8O8fvHIPwFWIldtfWT6AfiSsVvZgWWo=
|
||||||
|
github.com/ethanefung/bubble-datepicker v0.1.0/go.mod h1:8nxOYB9Oqays5U0JHKcIsbT7ZP/TwuJz8Uju9n5ueVU=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/moustachioed/go-taskwarrior v0.0.0-20220111032313-0ea4f466b47c h1:9w8HAhnAJKvNrus/zZM0yMSSeOIh6fdY2vInYPLqZJE=
|
||||||
|
github.com/moustachioed/go-taskwarrior v0.0.0-20220111032313-0ea4f466b47c/go.mod h1:/sBiMzFAOI/NxkONwgm9omrpBjYlmkmdmZ074a9PbX0=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
|
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||||
|
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||||
|
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
42
main.go
Normal file
42
main.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"tasksquire/common"
|
||||||
|
"tasksquire/model"
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ts := taskwarrior.NewTaskSquire("./test/taskrc")
|
||||||
|
ctx := context.Background()
|
||||||
|
common := common.NewCommon(ctx, ts)
|
||||||
|
|
||||||
|
// form := huh.NewForm(
|
||||||
|
// huh.NewGroup(
|
||||||
|
// huh.NewSelect[string]().
|
||||||
|
// Options(huh.NewOptions(config.Reports...)...).
|
||||||
|
// Title("Report").
|
||||||
|
// Description("Choose the report to display").
|
||||||
|
// Value(&report),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// err = form.Run()
|
||||||
|
// if err != nil {
|
||||||
|
// slog.Error("Uh oh:", err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
m := model.NewMainModel(common)
|
||||||
|
|
||||||
|
if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
|
||||||
|
fmt.Println("Error running program:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
53
model/model.go
Normal file
53
model/model.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
"tasksquire/common"
|
||||||
|
"tasksquire/pages"
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MainModel struct {
|
||||||
|
common *common.Common
|
||||||
|
selectedPage tea.Model
|
||||||
|
selectedTask *taskwarrior.Task
|
||||||
|
selectedReport *taskwarrior.Report
|
||||||
|
selectedContext *taskwarrior.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMainModel(common *common.Common) *MainModel {
|
||||||
|
m := &MainModel{
|
||||||
|
common: common,
|
||||||
|
selectedReport: common.TW.GetReport("next"),
|
||||||
|
selectedContext: common.TW.GetActiveContext(),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.selectedPage = pages.NewReportPage(common, m.selectedReport)
|
||||||
|
|
||||||
|
return m
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MainModel) Init() tea.Cmd {
|
||||||
|
return m.selectedPage.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
// switch msg := msg.(type) {
|
||||||
|
// case tea.KeyMsg:
|
||||||
|
// switch {
|
||||||
|
// case key.Matches(msg, m.common.Keymap.Add):
|
||||||
|
// case key.Matches(msg, m.common.Keymap.Edit):
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
m.selectedPage, cmd = m.selectedPage.Update(msg)
|
||||||
|
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MainModel) View() string {
|
||||||
|
return m.selectedPage.View()
|
||||||
|
}
|
||||||
105
pages/contextPicker.go
Normal file
105
pages/contextPicker.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"slices"
|
||||||
|
"tasksquire/common"
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContextPickerPage struct {
|
||||||
|
common *common.Common
|
||||||
|
contexts taskwarrior.Contexts
|
||||||
|
form *huh.Form
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContextPickerPage(common *common.Common) *ContextPickerPage {
|
||||||
|
p := &ContextPickerPage{
|
||||||
|
common: common,
|
||||||
|
contexts: common.TW.GetContexts(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// if allowAdd {
|
||||||
|
// fields = append(fields, huh.NewInput().
|
||||||
|
// Key("input").
|
||||||
|
// Title("Input").
|
||||||
|
// Prompt(fmt.Sprintf("Enter a new %s", header)).
|
||||||
|
// Inline(false),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
selected := common.TW.GetActiveContext().Name
|
||||||
|
options := make([]string, 0)
|
||||||
|
for _, c := range p.contexts {
|
||||||
|
options = append(options, c.Name)
|
||||||
|
}
|
||||||
|
slices.Sort(options)
|
||||||
|
|
||||||
|
p.form = huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[string]().
|
||||||
|
Key("context").
|
||||||
|
Options(huh.NewOptions(options...)...).
|
||||||
|
Title("Contexts").
|
||||||
|
Description("Choose a context").
|
||||||
|
Value(&selected),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithShowHelp(false).
|
||||||
|
WithShowErrors(true)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ContextPickerPage) Init() tea.Cmd {
|
||||||
|
return p.form.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ContextPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, p.common.Keymap.Back):
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, BackCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, cmd := p.form.Update(msg)
|
||||||
|
if f, ok := f.(*huh.Form); ok {
|
||||||
|
p.form = f
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.form.State == huh.StateCompleted {
|
||||||
|
cmds = append(cmds, p.updateContextCmd)
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ContextPickerPage) View() string {
|
||||||
|
return p.common.Styles.Main.Render(p.form.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ContextPickerPage) updateContextCmd() tea.Msg {
|
||||||
|
return UpdateContextMsg(p.common.TW.GetContext(p.form.GetString("context")))
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateContextMsg *taskwarrior.Context
|
||||||
122
pages/datePicker.go
Normal file
122
pages/datePicker.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "tasksquire/common"
|
||||||
|
|
||||||
|
// "github.com/charmbracelet/bubbles/textinput"
|
||||||
|
// tea "github.com/charmbracelet/bubbletea"
|
||||||
|
// "github.com/charmbracelet/lipgloss"
|
||||||
|
// datepicker "github.com/ethanefung/bubble-datepicker"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// type Model struct {
|
||||||
|
// focus focus
|
||||||
|
// input textinput.Model
|
||||||
|
// datepicker datepicker.Model
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var inputStyles = lipgloss.NewStyle().Padding(1, 1, 0)
|
||||||
|
|
||||||
|
// func initializeModel() tea.Model {
|
||||||
|
// dp := datepicker.New(time.Now())
|
||||||
|
|
||||||
|
// input := textinput.New()
|
||||||
|
// input.Placeholder = "YYYY-MM-DD (enter date)"
|
||||||
|
// input.Focus()
|
||||||
|
// input.Width = 20
|
||||||
|
|
||||||
|
// return Model{
|
||||||
|
// focus: FocusInput,
|
||||||
|
// input: input,
|
||||||
|
// datepicker: dp,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (m Model) Init() tea.Cmd {
|
||||||
|
// return textinput.Blink
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
// var cmd tea.Cmd
|
||||||
|
|
||||||
|
// switch msg := msg.(type) {
|
||||||
|
// case tea.WindowSizeMsg:
|
||||||
|
// // TODO figure out how we want to size things
|
||||||
|
// // we'll probably want both bubbles to be vertically stacked
|
||||||
|
// // and to take as much room as the can
|
||||||
|
// return m, nil
|
||||||
|
// case tea.KeyMsg:
|
||||||
|
// switch msg.String() {
|
||||||
|
// case "ctrl+c", "q":
|
||||||
|
// return m, tea.Quit
|
||||||
|
// case "tab":
|
||||||
|
// if m.focus == FocusInput {
|
||||||
|
// m.focus = FocusDatePicker
|
||||||
|
// m.input.Blur()
|
||||||
|
// m.input.SetValue(m.datepicker.Time.Format(time.DateOnly))
|
||||||
|
|
||||||
|
// m.datepicker.SelectDate()
|
||||||
|
// m.datepicker.SetFocus(datepicker.FocusHeaderMonth)
|
||||||
|
// m.datepicker = m.datepicker
|
||||||
|
// return m, nil
|
||||||
|
|
||||||
|
// }
|
||||||
|
// case "shift+tab":
|
||||||
|
// if m.focus == FocusDatePicker && m.datepicker.Focused == datepicker.FocusHeaderMonth {
|
||||||
|
// m.focus = FocusInput
|
||||||
|
// m.datepicker.Blur()
|
||||||
|
|
||||||
|
// m.input.Focus()
|
||||||
|
// return m, nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// switch m.focus {
|
||||||
|
// case FocusInput:
|
||||||
|
// m.input, cmd = m.UpdateInput(msg)
|
||||||
|
// case FocusDatePicker:
|
||||||
|
// m.datepicker, cmd = m.UpdateDatepicker(msg)
|
||||||
|
// case FocusNone:
|
||||||
|
// // do nothing
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return m, cmd
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (m Model) View() string {
|
||||||
|
// return lipgloss.JoinVertical(lipgloss.Left, inputStyles.Render(m.input.View()), m.datepicker.View())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (m *Model) UpdateInput(msg tea.Msg) (textinput.Model, tea.Cmd) {
|
||||||
|
// var cmd tea.Cmd
|
||||||
|
|
||||||
|
// m.input, cmd = m.input.Update(msg)
|
||||||
|
|
||||||
|
// val := m.input.Value()
|
||||||
|
// t, err := time.Parse(time.DateOnly, strings.TrimSpace(val))
|
||||||
|
// if err == nil {
|
||||||
|
// m.datepicker.SetTime(t)
|
||||||
|
// m.datepicker.SelectDate()
|
||||||
|
// m.datepicker.Blur()
|
||||||
|
// }
|
||||||
|
// if err != nil && m.datepicker.Selected {
|
||||||
|
// m.datepicker.UnselectDate()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return m.input, cmd
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (m *Model) UpdateDatepicker(msg tea.Msg) (datepicker.Model, tea.Cmd) {
|
||||||
|
// var cmd tea.Cmd
|
||||||
|
|
||||||
|
// prev := m.datepicker.Time
|
||||||
|
|
||||||
|
// m.datepicker, cmd = m.datepicker.Update(msg)
|
||||||
|
|
||||||
|
// if prev != m.datepicker.Time {
|
||||||
|
// m.input.SetValue(m.datepicker.Time.Format(time.DateOnly))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return m.datepicker, cmd
|
||||||
|
// }
|
||||||
11
pages/page.go
Normal file
11
pages/page.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BackCmd() tea.Msg {
|
||||||
|
return BackMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackMsg struct{}
|
||||||
100
pages/projectPicker.go
Normal file
100
pages/projectPicker.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"tasksquire/common"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectPickerPage struct {
|
||||||
|
common *common.Common
|
||||||
|
form *huh.Form
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectPickerPage(common *common.Common, activeProject string) *ProjectPickerPage {
|
||||||
|
p := &ProjectPickerPage{
|
||||||
|
common: common,
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected string
|
||||||
|
if activeProject == "" {
|
||||||
|
selected = "(none)"
|
||||||
|
} else {
|
||||||
|
selected = activeProject
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := common.TW.GetProjects()
|
||||||
|
options := []string{"(none)"}
|
||||||
|
options = append(options, projects...)
|
||||||
|
|
||||||
|
p.form = huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[string]().
|
||||||
|
Key("project").
|
||||||
|
Options(huh.NewOptions(options...)...).
|
||||||
|
Title("Projects").
|
||||||
|
Description("Choose a project").
|
||||||
|
Value(&selected),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithShowHelp(false).
|
||||||
|
WithShowErrors(false)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectPickerPage) Init() tea.Cmd {
|
||||||
|
return p.form.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, p.common.Keymap.Back):
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, BackCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, cmd := p.form.Update(msg)
|
||||||
|
if f, ok := f.(*huh.Form); ok {
|
||||||
|
p.form = f
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.form.State == huh.StateCompleted {
|
||||||
|
cmds = append(cmds, p.updateProjectCmd)
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectPickerPage) View() string {
|
||||||
|
return p.common.Styles.Main.Render(p.form.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectPickerPage) updateProjectCmd() tea.Msg {
|
||||||
|
project := p.form.GetString("project")
|
||||||
|
if project == "(none)" {
|
||||||
|
project = ""
|
||||||
|
}
|
||||||
|
return UpdateProjectMsg(project)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProjectMsg string
|
||||||
201
pages/report.go
Normal file
201
pages/report.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tasksquire/common"
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReportPage struct {
|
||||||
|
common *common.Common
|
||||||
|
|
||||||
|
activeReport *taskwarrior.Report
|
||||||
|
activeContext *taskwarrior.Context
|
||||||
|
activeProject string
|
||||||
|
selectedTask *taskwarrior.Task
|
||||||
|
|
||||||
|
tasks taskwarrior.Tasks
|
||||||
|
|
||||||
|
taskTable table.Model
|
||||||
|
tableStyle table.Styles
|
||||||
|
keymap ReportKeys
|
||||||
|
|
||||||
|
subpage tea.Model
|
||||||
|
subpageActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportKeys struct {
|
||||||
|
Quit key.Binding
|
||||||
|
Up key.Binding
|
||||||
|
Down key.Binding
|
||||||
|
Select key.Binding
|
||||||
|
ToggleFocus key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage {
|
||||||
|
s := table.DefaultStyles()
|
||||||
|
s.Header = s.Header.
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("240")).
|
||||||
|
BorderBottom(true).
|
||||||
|
Bold(false)
|
||||||
|
s.Selected = s.Selected.
|
||||||
|
Foreground(lipgloss.Color("229")).
|
||||||
|
Background(lipgloss.Color("57")).
|
||||||
|
Bold(false)
|
||||||
|
|
||||||
|
keys := ReportKeys{
|
||||||
|
Quit: key.NewBinding(
|
||||||
|
key.WithKeys("q", "ctrl+c"),
|
||||||
|
key.WithHelp("q, ctrl+c", "Quit"),
|
||||||
|
),
|
||||||
|
Up: key.NewBinding(
|
||||||
|
key.WithKeys("k", "up"),
|
||||||
|
key.WithHelp("↑/k", "Up"),
|
||||||
|
),
|
||||||
|
Down: key.NewBinding(
|
||||||
|
key.WithKeys("j", "down"),
|
||||||
|
key.WithHelp("↓/j", "Down"),
|
||||||
|
),
|
||||||
|
Select: key.NewBinding(
|
||||||
|
key.WithKeys("enter"),
|
||||||
|
key.WithHelp("enter", "Select"),
|
||||||
|
),
|
||||||
|
ToggleFocus: key.NewBinding(
|
||||||
|
key.WithKeys("esc"),
|
||||||
|
key.WithHelp("esc", "Toggle focus"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReportPage{
|
||||||
|
common: com,
|
||||||
|
activeReport: report,
|
||||||
|
activeContext: com.TW.GetActiveContext(),
|
||||||
|
activeProject: "",
|
||||||
|
tableStyle: s,
|
||||||
|
keymap: keys,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ReportPage) Init() tea.Cmd {
|
||||||
|
return p.getTasks()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case BackMsg:
|
||||||
|
p.subpageActive = false
|
||||||
|
case TaskMsg:
|
||||||
|
p.populateTaskTable(msg)
|
||||||
|
case UpdateReportMsg:
|
||||||
|
p.activeReport = msg
|
||||||
|
cmds = append(cmds, p.getTasks())
|
||||||
|
case UpdateContextMsg:
|
||||||
|
p.activeContext = msg
|
||||||
|
p.common.TW.SetContext(msg)
|
||||||
|
cmds = append(cmds, p.getTasks())
|
||||||
|
case UpdateProjectMsg:
|
||||||
|
p.activeProject = string(msg)
|
||||||
|
cmds = append(cmds, p.getTasks())
|
||||||
|
case AddedTaskMsg:
|
||||||
|
cmds = append(cmds, p.getTasks())
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
p.taskTable.SetWidth(msg.Width - 2)
|
||||||
|
p.taskTable.SetHeight(msg.Height - 4)
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, p.common.Keymap.Quit):
|
||||||
|
return p, tea.Quit
|
||||||
|
case key.Matches(msg, p.common.Keymap.SetReport):
|
||||||
|
p.subpage = NewReportPickerPage(p.common, p.activeReport)
|
||||||
|
p.subpage.Init()
|
||||||
|
p.subpageActive = true
|
||||||
|
p.common.PageStack.Push(p)
|
||||||
|
return p.subpage, nil
|
||||||
|
case key.Matches(msg, p.common.Keymap.SetContext):
|
||||||
|
p.subpage = NewContextPickerPage(p.common)
|
||||||
|
p.subpage.Init()
|
||||||
|
p.subpageActive = true
|
||||||
|
p.common.PageStack.Push(p)
|
||||||
|
return p.subpage, nil
|
||||||
|
case key.Matches(msg, p.common.Keymap.Add):
|
||||||
|
p.subpage = NewTaskEditorPage(p.common, taskwarrior.Task{})
|
||||||
|
p.subpage.Init()
|
||||||
|
p.subpageActive = true
|
||||||
|
p.common.PageStack.Push(p)
|
||||||
|
return p.subpage, nil
|
||||||
|
case key.Matches(msg, p.common.Keymap.SetProject):
|
||||||
|
p.subpage = NewProjectPickerPage(p.common, p.activeProject)
|
||||||
|
p.subpage.Init()
|
||||||
|
p.subpageActive = true
|
||||||
|
p.common.PageStack.Push(p)
|
||||||
|
return p.subpage, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
p.taskTable, cmd = p.taskTable.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
if p.tasks != nil {
|
||||||
|
p.selectedTask = (*taskwarrior.Task)(p.tasks[p.taskTable.Cursor()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ReportPage) View() string {
|
||||||
|
return p.common.Styles.Main.Render(p.taskTable.View()) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReportPage) populateTaskTable(tasks []*taskwarrior.Task) {
|
||||||
|
columns := []table.Column{
|
||||||
|
{Title: "ID", Width: 4},
|
||||||
|
{Title: "Project", Width: 10},
|
||||||
|
{Title: "Tags", Width: 10},
|
||||||
|
{Title: "Prio", Width: 2},
|
||||||
|
{Title: "Due", Width: 10},
|
||||||
|
{Title: "Task", Width: 50},
|
||||||
|
}
|
||||||
|
var rows []table.Row
|
||||||
|
for _, task := range tasks {
|
||||||
|
rows = append(rows, table.Row{
|
||||||
|
strconv.FormatInt(task.Id, 10),
|
||||||
|
task.Project,
|
||||||
|
strings.Join(task.Tags, ", "),
|
||||||
|
task.Priority,
|
||||||
|
task.Due,
|
||||||
|
task.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
p.taskTable = table.New(
|
||||||
|
table.WithColumns(columns),
|
||||||
|
table.WithRows(rows),
|
||||||
|
table.WithFocused(true),
|
||||||
|
// table.WithHeight(7),
|
||||||
|
// table.WithWidth(100),
|
||||||
|
)
|
||||||
|
p.taskTable.SetStyles(p.tableStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReportPage) getTasks() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
filters := []string{}
|
||||||
|
if p.activeProject != "" {
|
||||||
|
filters = append(filters, "project:"+p.activeProject)
|
||||||
|
}
|
||||||
|
p.tasks = p.common.TW.GetTasks(p.activeReport, filters...)
|
||||||
|
return TaskMsg(p.tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskMsg taskwarrior.Tasks
|
||||||
97
pages/reportPicker.go
Normal file
97
pages/reportPicker.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"slices"
|
||||||
|
"tasksquire/common"
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReportPickerPage struct {
|
||||||
|
common *common.Common
|
||||||
|
reports taskwarrior.Reports
|
||||||
|
form *huh.Form
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReportPickerPage(common *common.Common, activeReport *taskwarrior.Report) *ReportPickerPage {
|
||||||
|
p := &ReportPickerPage{
|
||||||
|
common: common,
|
||||||
|
reports: common.TW.GetReports(),
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := activeReport.Name
|
||||||
|
|
||||||
|
options := make([]string, 0)
|
||||||
|
for _, r := range p.reports {
|
||||||
|
options = append(options, r.Name)
|
||||||
|
}
|
||||||
|
slices.Sort(options)
|
||||||
|
|
||||||
|
p.form = huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[string]().
|
||||||
|
Key("report").
|
||||||
|
Options(huh.NewOptions(options...)...).
|
||||||
|
Title("Reports").
|
||||||
|
Description("Choose a report").
|
||||||
|
Value(&selected),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithShowHelp(false).
|
||||||
|
WithShowErrors(false)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReportPickerPage) Init() tea.Cmd {
|
||||||
|
return p.form.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReportPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, p.common.Keymap.Back):
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, BackCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, cmd := p.form.Update(msg)
|
||||||
|
if f, ok := f.(*huh.Form); ok {
|
||||||
|
p.form = f
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.form.State == huh.StateCompleted {
|
||||||
|
cmds = append(cmds, []tea.Cmd{BackCmd, p.updateReportCmd}...)
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReportPickerPage) View() string {
|
||||||
|
return p.common.Styles.Main.Render(p.form.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReportPickerPage) updateReportCmd() tea.Msg {
|
||||||
|
return UpdateReportMsg(p.common.TW.GetReport(p.form.GetString("report")))
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateReportMsg *taskwarrior.Report
|
||||||
233
pages/taskEditor.go
Normal file
233
pages/taskEditor.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"tasksquire/common"
|
||||||
|
"tasksquire/taskwarrior"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskEditorPage struct {
|
||||||
|
common *common.Common
|
||||||
|
task taskwarrior.Task
|
||||||
|
form *huh.Form
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskEditorKeys struct {
|
||||||
|
Quit key.Binding
|
||||||
|
Up key.Binding
|
||||||
|
Down key.Binding
|
||||||
|
Select key.Binding
|
||||||
|
ToggleFocus key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditorPage {
|
||||||
|
p := &TaskEditorPage{
|
||||||
|
common: common,
|
||||||
|
task: task,
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.task.Priority == "" {
|
||||||
|
p.task.Priority = "(none)"
|
||||||
|
}
|
||||||
|
if p.task.Project == "" {
|
||||||
|
p.task.Project = "(none)"
|
||||||
|
}
|
||||||
|
|
||||||
|
priorityOptions := append([]string{"(none)"}, common.TW.GetPriorities()...)
|
||||||
|
projectOptions := append([]string{"(none)"}, common.TW.GetProjects()...)
|
||||||
|
tagOptions := common.TW.GetTags()
|
||||||
|
|
||||||
|
p.form = huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewInput().
|
||||||
|
Title("Task").
|
||||||
|
Value(&p.task.Description).
|
||||||
|
Inline(true),
|
||||||
|
|
||||||
|
huh.NewSelect[string]().
|
||||||
|
Options(huh.NewOptions(priorityOptions...)...).
|
||||||
|
Title("Priority").
|
||||||
|
Value(&p.task.Priority),
|
||||||
|
|
||||||
|
huh.NewSelect[string]().
|
||||||
|
Options(huh.NewOptions(projectOptions...)...).
|
||||||
|
Title("Project").
|
||||||
|
Value(&p.task.Project),
|
||||||
|
|
||||||
|
huh.NewMultiSelect[string]().
|
||||||
|
Options(huh.NewOptions(tagOptions...)...).
|
||||||
|
Title("Tags").
|
||||||
|
Value(&p.task.Tags),
|
||||||
|
|
||||||
|
huh.NewInput().
|
||||||
|
Title("Due").
|
||||||
|
Value(&p.task.Due).
|
||||||
|
Validate(validateDate).
|
||||||
|
Inline(true),
|
||||||
|
|
||||||
|
huh.NewInput().
|
||||||
|
Title("Scheduled").
|
||||||
|
Value(&p.task.Scheduled).
|
||||||
|
Validate(validateDate).
|
||||||
|
Inline(true),
|
||||||
|
|
||||||
|
huh.NewInput().
|
||||||
|
Title("Wait").
|
||||||
|
Value(&p.task.Wait).
|
||||||
|
Validate(validateDate).
|
||||||
|
Inline(true),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithShowHelp(false).
|
||||||
|
WithShowErrors(false)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TaskEditorPage) Init() tea.Cmd {
|
||||||
|
return p.form.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, p.common.Keymap.Back):
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, BackCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, cmd := p.form.Update(msg)
|
||||||
|
if f, ok := f.(*huh.Form); ok {
|
||||||
|
p.form = f
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.form.State == huh.StateCompleted {
|
||||||
|
cmds = append(cmds, p.addTaskCmd)
|
||||||
|
model, err := p.common.PageStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("page stack empty")
|
||||||
|
return nil, tea.Quit
|
||||||
|
}
|
||||||
|
return model, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TaskEditorPage) View() string {
|
||||||
|
return p.common.Styles.Main.Render(p.form.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TaskEditorPage) addTaskCmd() tea.Msg {
|
||||||
|
p.common.TW.AddTask(&p.task)
|
||||||
|
return AddedTaskMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddedTaskMsg struct{}
|
||||||
|
|
||||||
|
// TODO: move this to taskwarrior; add missing date formats
|
||||||
|
func validateDate(s string) error {
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02T15:04",
|
||||||
|
"20060102T150405Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
otherFormats := []string{
|
||||||
|
"",
|
||||||
|
"now",
|
||||||
|
"today",
|
||||||
|
"sod",
|
||||||
|
"eod",
|
||||||
|
"yesterday",
|
||||||
|
"tomorrow",
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
"sunday",
|
||||||
|
"mon",
|
||||||
|
"tue",
|
||||||
|
"wed",
|
||||||
|
"thu",
|
||||||
|
"fri",
|
||||||
|
"sat",
|
||||||
|
"sun",
|
||||||
|
"soy",
|
||||||
|
"eoy",
|
||||||
|
"soq",
|
||||||
|
"eoq",
|
||||||
|
"som",
|
||||||
|
"eom",
|
||||||
|
"socm",
|
||||||
|
"eocm",
|
||||||
|
"sow",
|
||||||
|
"eow",
|
||||||
|
"socw",
|
||||||
|
"eocw",
|
||||||
|
"soww",
|
||||||
|
"eoww",
|
||||||
|
"1st",
|
||||||
|
"2nd",
|
||||||
|
"3rd",
|
||||||
|
"4th",
|
||||||
|
"5th",
|
||||||
|
"6th",
|
||||||
|
"7th",
|
||||||
|
"8th",
|
||||||
|
"9th",
|
||||||
|
"10th",
|
||||||
|
"11th",
|
||||||
|
"12th",
|
||||||
|
"13th",
|
||||||
|
"14th",
|
||||||
|
"15th",
|
||||||
|
"16th",
|
||||||
|
"17th",
|
||||||
|
"18th",
|
||||||
|
"19th",
|
||||||
|
"20th",
|
||||||
|
"21st",
|
||||||
|
"22nd",
|
||||||
|
"23rd",
|
||||||
|
"24th",
|
||||||
|
"25th",
|
||||||
|
"26th",
|
||||||
|
"27th",
|
||||||
|
"28th",
|
||||||
|
"29th",
|
||||||
|
"30th",
|
||||||
|
"31st",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range formats {
|
||||||
|
if _, err := time.Parse(f, s); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range otherFormats {
|
||||||
|
if s == f {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid date")
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
test/taskchampion.sqlite3
Normal file
BIN
test/taskchampion.sqlite3
Normal file
Binary file not shown.
4
test/taskrc
Normal file
4
test/taskrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
data.location=/Users/moustachioed/projects/tasksquire/test
|
||||||
|
context.test1.read=+test
|
||||||
|
context.test1.write=+test
|
||||||
|
context=test1
|
||||||
Reference in New Issue
Block a user