git/dew/main

chore: mechanical cleanup (gofmt, go mod tidy, gitignore)

diff --git a/.gitignore b/.gitignore
index 43a63fb..8b23f6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,5 @@
-dew
+/dew
+*.test
+*.exe
+*.log
+/out/
diff --git a/actions.go b/actions.go
index 4809a2c..1eaebe0 100644
--- a/actions.go
+++ b/actions.go
@@ -23,18 +23,24 @@ func compileSmartDate(val string, now time.Time) (string, error) {
return "", err
}
switch unit {
- case "d": return now.AddDate(0, 0, amount).Format("2006-01-02"), nil
- case "w": return now.AddDate(0, 0, amount*7).Format("2006-01-02"), nil
- case "m": return now.AddDate(0, amount, 0).Format("2006-01-02"), nil
- case "y": return now.AddDate(amount, 0, 0).Format("2006-01-02"), nil
+ case "d":
+ return now.AddDate(0, 0, amount).Format("2006-01-02"), nil
+ case "w":
+ return now.AddDate(0, 0, amount*7).Format("2006-01-02"), nil
+ case "m":
+ return now.AddDate(0, amount, 0).Format("2006-01-02"), nil
+ case "y":
+ return now.AddDate(amount, 0, 0).Format("2006-01-02"), nil
}
}
return "", fmt.Errorf("not a smart date")
}
func AddTask(filePath string, args []string) (string, error) {
- if len(args) == 0 { return "", fmt.Errorf("no task description provided") }
-
+ if len(args) == 0 {
+ return "", fmt.Errorf("no task description provided")
+ }
+
now := time.Now()
for i, arg := range args {
if strings.HasPrefix(arg, "due:") {
@@ -46,24 +52,34 @@ func AddTask(filePath string, args []string) (string, error) {
}
taskLine := strings.Join(args, " ")
-
+
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil { return "", err }
+ if err != nil {
+ return "", err
+ }
defer file.Close()
-
+
_, err = file.WriteString(taskLine + "\n")
- if err != nil { return "", err }
-
+ if err != nil {
+ return "", err
+ }
+
return fmt.Sprintf("added task: %s", taskLine), nil
}
func CompleteTask(cfg *Config, args []string) (string, error) {
- if len(args) == 0 { return "", fmt.Errorf("no task ID provided") }
+ if len(args) == 0 {
+ return "", fmt.Errorf("no task ID provided")
+ }
taskID, err := strconv.Atoi(args[0])
- if err != nil { return "", fmt.Errorf("invalid task ID: %s", args[0]) }
+ if err != nil {
+ return "", fmt.Errorf("invalid task ID: %s", args[0])
+ }
tasks, err := ReadTasks(cfg.TodoFile)
- if err != nil { return "", err }
+ if err != nil {
+ return "", err
+ }
var targetTask *Task
for i, t := range tasks {
@@ -73,7 +89,9 @@ func CompleteTask(cfg *Config, args []string) (string, error) {
}
}
- if targetTask == nil { return "", fmt.Errorf("task %d not found", taskID) }
+ if targetTask == nil {
+ return "", fmt.Errorf("task %d not found", taskID)
+ }
now := time.Now()
dateString := now.Format("2006-01-02")
@@ -82,20 +100,28 @@ func CompleteTask(cfg *Config, args []string) (string, error) {
var recurMsg string
if targetTask.Recurrence != "" {
newRecurLine, err = generateRecurringTask(*targetTask, now)
- if err != nil { return "", fmt.Errorf("error generating recurring task: %v", err) }
+ if err != nil {
+ return "", fmt.Errorf("error generating recurring task: %v", err)
+ }
recurMsg = fmt.Sprintf("\ngenerated next recurring task for %s", targetTask.Recurrence)
}
doneLine := fmt.Sprintf("x %s %s\n", dateString, targetTask.RawLine)
doneFile, err := os.OpenFile(cfg.DoneFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil { return "", fmt.Errorf("could not open done file: %v", err) }
+ if err != nil {
+ return "", fmt.Errorf("could not open done file: %v", err)
+ }
defer doneFile.Close()
-
+
_, err = doneFile.WriteString(doneLine)
- if err != nil { return "", fmt.Errorf("could not write to done file: %v", err) }
+ if err != nil {
+ return "", fmt.Errorf("could not write to done file: %v", err)
+ }
todoFile, err := os.Create(cfg.TodoFile)
- if err != nil { return "", fmt.Errorf("could not rewrite todo file: %v", err) }
+ if err != nil {
+ return "", fmt.Errorf("could not rewrite todo file: %v", err)
+ }
defer todoFile.Close()
for _, t := range tasks {
@@ -103,16 +129,20 @@ func CompleteTask(cfg *Config, args []string) (string, error) {
todoFile.WriteString(t.RawLine + "\n")
}
}
-
- if newRecurLine != "" { todoFile.WriteString(newRecurLine + "\n") }
-
+
+ if newRecurLine != "" {
+ todoFile.WriteString(newRecurLine + "\n")
+ }
+
finalMsg := fmt.Sprintf("marked task %d as done and archived to done.txt!%s", taskID, recurMsg)
return finalMsg, nil
}
func generateRecurringTask(t Task, now time.Time) (string, error) {
recStr := strings.Trim(t.Recurrence, "[]+")
- if len(recStr) == 0 { return "", fmt.Errorf("invalid recurrence format") }
+ if len(recStr) == 0 {
+ return "", fmt.Errorf("invalid recurrence format")
+ }
var amount int
var unit string
@@ -126,15 +156,22 @@ func generateRecurringTask(t Task, now time.Time) (string, error) {
}
baseDate := now
- if strings.Contains(t.Recurrence, "+") && t.HasDueDate { baseDate = t.DueDate }
+ if strings.Contains(t.Recurrence, "+") && t.HasDueDate {
+ baseDate = t.DueDate
+ }
var nextDate time.Time
switch unit {
- case "d": nextDate = baseDate.AddDate(0, 0, amount)
- case "w": nextDate = baseDate.AddDate(0, 0, amount*7)
- case "m": nextDate = baseDate.AddDate(0, amount, 0)
- case "y": nextDate = baseDate.AddDate(amount, 0, 0)
- default: return "", fmt.Errorf("unknown recurrence unit: %s", unit)
+ case "d":
+ nextDate = baseDate.AddDate(0, 0, amount)
+ case "w":
+ nextDate = baseDate.AddDate(0, 0, amount*7)
+ case "m":
+ nextDate = baseDate.AddDate(0, amount, 0)
+ case "y":
+ nextDate = baseDate.AddDate(amount, 0, 0)
+ default:
+ return "", fmt.Errorf("unknown recurrence unit: %s", unit)
}
newLine := t.RawLine
@@ -149,16 +186,24 @@ func generateRecurringTask(t Task, now time.Time) (string, error) {
}
func DeleteTask(cfg *Config, args []string) (string, error) {
- if len(args) == 0 { return "", fmt.Errorf("no task ID provided") }
+ if len(args) == 0 {
+ return "", fmt.Errorf("no task ID provided")
+ }
taskID, err := strconv.Atoi(args[0])
- if err != nil { return "", fmt.Errorf("invalid task ID: %s", args[0]) }
+ if err != nil {
+ return "", fmt.Errorf("invalid task ID: %s", args[0])
+ }
tasks, err := ReadTasks(cfg.TodoFile)
- if err != nil { return "", err }
+ if err != nil {
+ return "", err
+ }
found := false
todoFile, err := os.Create(cfg.TodoFile)
- if err != nil { return "", fmt.Errorf("could not rewrite todo file: %v", err) }
+ if err != nil {
+ return "", fmt.Errorf("could not rewrite todo file: %v", err)
+ }
defer todoFile.Close()
for _, t := range tasks {
@@ -171,17 +216,25 @@ func DeleteTask(cfg *Config, args []string) (string, error) {
}
}
- if !found { return "", fmt.Errorf("task %d not found", taskID) }
+ if !found {
+ return "", fmt.Errorf("task %d not found", taskID)
+ }
return fmt.Sprintf("deleted task %d completely.", taskID), nil
}
func ToggleModifier(cfg *Config, args []string, mod string) (string, error) {
- if len(args) == 0 { return "", fmt.Errorf("no task ID provided") }
+ if len(args) == 0 {
+ return "", fmt.Errorf("no task ID provided")
+ }
taskID, err := strconv.Atoi(args[0])
- if err != nil { return "", fmt.Errorf("invalid task ID: %s", args[0]) }
+ if err != nil {
+ return "", fmt.Errorf("invalid task ID: %s", args[0])
+ }
tasks, err := ReadTasks(cfg.TodoFile)
- if err != nil { return "", err }
+ if err != nil {
+ return "", err
+ }
var targetIndex = -1
for i, t := range tasks {
@@ -191,7 +244,9 @@ func ToggleModifier(cfg *Config, args []string, mod string) (string, error) {
}
}
- if targetIndex == -1 { return "", fmt.Errorf("task %d not found", taskID) }
+ if targetIndex == -1 {
+ return "", fmt.Errorf("task %d not found", taskID)
+ }
targetTask := tasks[targetIndex]
words := strings.Fields(targetTask.RawLine)
@@ -212,8 +267,12 @@ func ToggleModifier(cfg *Config, args []string, mod string) (string, error) {
}
wasAlreadyMod := false
- if mod == "*" && targetTask.IsPinned { wasAlreadyMod = true }
- if mod == "~" && targetTask.IsWaiting { wasAlreadyMod = true }
+ if mod == "*" && targetTask.IsPinned {
+ wasAlreadyMod = true
+ }
+ if mod == "~" && targetTask.IsWaiting {
+ wasAlreadyMod = true
+ }
if !wasAlreadyMod {
newWords := make([]string, 0, len(words)+1)
@@ -226,20 +285,26 @@ func ToggleModifier(cfg *Config, args []string, mod string) (string, error) {
tasks[targetIndex].RawLine = strings.Join(words, " ")
todoFile, err := os.Create(cfg.TodoFile)
- if err != nil { return "", err }
+ if err != nil {
+ return "", err
+ }
defer todoFile.Close()
for _, t := range tasks {
- if t.IsValid { todoFile.WriteString(t.RawLine + "\n") }
+ if t.IsValid {
+ todoFile.WriteString(t.RawLine + "\n")
+ }
}
-
+
return fmt.Sprintf("toggled %s for task %d", mod, taskID), nil
}
// EditTask replaces the exact raw string of a given task ID
func EditTask(cfg *Config, taskID int, newText string) (string, error) {
tasks, err := ReadTasks(cfg.TodoFile)
- if err != nil { return "", err }
+ if err != nil {
+ return "", err
+ }
found := false
for i, t := range tasks {
@@ -250,10 +315,14 @@ func EditTask(cfg *Config, taskID int, newText string) (string, error) {
}
}
- if !found { return "", fmt.Errorf("task %d not found", taskID) }
+ if !found {
+ return "", fmt.Errorf("task %d not found", taskID)
+ }
todoFile, err := os.Create(cfg.TodoFile)
- if err != nil { return "", err }
+ if err != nil {
+ return "", err
+ }
defer todoFile.Close()
for _, t := range tasks {
diff --git a/config.go b/config.go
index f42e5f0..54c6981 100644
--- a/config.go
+++ b/config.go
@@ -14,25 +14,25 @@ type Config struct {
DoneFile string `json:"done_file"`
CenterTUI bool `json:"center_tui"`
DefaultView string `json:"default_view"`
-
- BackendMode string `json:"backend_mode"`
- APIURL string `json:"api_url"`
- APIToken string `json:"api_token"`
-
- CursorIcon string `json:"cursor_icon"`
- ProjectIcon string `json:"project_icon"`
- HideWaiting bool `json:"hide_waiting_tasks"`
- HideHints bool `json:"hide_hints"`
-
- ColorAccent string `json:"color_accent"`
- ColorMuted string `json:"color_muted"`
- ColorProject string `json:"color_project"`
- ColorContext string `json:"color_context"`
- ColorOverdue string `json:"color_overdue"`
- ColorToday string `json:"color_today"`
- ColorSoon string `json:"color_soon"`
- ColorFar string `json:"color_far"`
- ColorVeryFar string `json:"color_very_far"`
+
+ BackendMode string `json:"backend_mode"`
+ APIURL string `json:"api_url"`
+ APIToken string `json:"api_token"`
+
+ CursorIcon string `json:"cursor_icon"`
+ ProjectIcon string `json:"project_icon"`
+ HideWaiting bool `json:"hide_waiting_tasks"`
+ HideHints bool `json:"hide_hints"`
+
+ ColorAccent string `json:"color_accent"`
+ ColorMuted string `json:"color_muted"`
+ ColorProject string `json:"color_project"`
+ ColorContext string `json:"color_context"`
+ ColorOverdue string `json:"color_overdue"`
+ ColorToday string `json:"color_today"`
+ ColorSoon string `json:"color_soon"`
+ ColorFar string `json:"color_far"`
+ ColorVeryFar string `json:"color_very_far"`
}
func getConfigPath() (string, error) {
@@ -66,7 +66,9 @@ func loadConfig() (*Config, error) {
}
configPath, err := getConfigPath()
- if err != nil { return nil, err }
+ if err != nil {
+ return nil, err
+ }
file, err := os.Open(configPath)
if err != nil {
@@ -81,7 +83,7 @@ func loadConfig() (*Config, error) {
if cfg.DefaultView != "flat" && cfg.DefaultView != "grouped" {
cfg.DefaultView = "grouped"
}
-
+
if cfg.BackendMode != "local" && cfg.BackendMode != "server" {
cfg.BackendMode = "local"
}
@@ -91,10 +93,14 @@ func loadConfig() (*Config, error) {
func saveConfig(cfg *Config) error {
configPath, err := getConfigPath()
- if err != nil { return err }
+ if err != nil {
+ return err
+ }
file, err := os.Create(configPath)
- if err != nil { return err }
+ if err != nil {
+ return err
+ }
defer file.Close()
encoder := json.NewEncoder(file)
diff --git a/go.mod b/go.mod
index ede7c1e..b51a779 100644
--- a/go.mod
+++ b/go.mod
@@ -2,13 +2,16 @@ module dew
go 1.26.4
+require (
+ github.com/charmbracelet/bubbles v1.0.0
+ github.com/charmbracelet/bubbletea v1.3.10
+ github.com/charmbracelet/lipgloss v1.1.0
+)
+
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
- github.com/charmbracelet/bubbles v1.0.0 // indirect
- github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
- github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
diff --git a/go.sum b/go.sum
index 48170d0..e92ed90 100644
--- a/go.sum
+++ b/go.sum
@@ -6,22 +6,14 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
-github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
-github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
-github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
-github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
-github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
-github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
-github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
-github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
@@ -32,16 +24,12 @@ github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
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/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/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
-github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -50,15 +38,14 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
-golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
diff --git a/main.go b/main.go
index c1fb6bd..44c443e 100644
--- a/main.go
+++ b/main.go
@@ -36,7 +36,10 @@ func main() {
if len(os.Args) < 2 {
err := LaunchTUI(cfg)
- if err != nil { fmt.Fprintf(os.Stderr, "error launching tui: %v\n", err); os.Exit(1) }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error launching tui: %v\n", err)
+ os.Exit(1)
+ }
return
}
@@ -45,23 +48,38 @@ func main() {
switch command {
case "add", "a":
msg, err := AddTask(cfg.TodoFile, os.Args[2:])
- if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err); os.Exit(1) }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
fmt.Println(msg)
case "do", "done":
msg, err := CompleteTask(cfg, os.Args[2:])
- if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err); os.Exit(1) }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
fmt.Println(msg)
case "rm", "del":
msg, err := DeleteTask(cfg, os.Args[2:])
- if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err); os.Exit(1) }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
fmt.Println(msg)
case "pin":
msg, err := ToggleModifier(cfg, os.Args[2:], "*")
- if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err); os.Exit(1) }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
fmt.Println(msg)
case "wait":
msg, err := ToggleModifier(cfg, os.Args[2:], "~")
- if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err); os.Exit(1) }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
fmt.Println(msg)
case "ls", "list":
listTasks(cfg, os.Args[2:], "grouped")
@@ -69,7 +87,10 @@ func main() {
listTasks(cfg, os.Args[2:], "flat")
case "tui", "ui":
err := LaunchTUI(cfg)
- if err != nil { fmt.Fprintf(os.Stderr, "error launching tui: %v\n", err); os.Exit(1) }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error launching tui: %v\n", err)
+ os.Exit(1)
+ }
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", command)
os.Exit(1)
@@ -85,9 +106,13 @@ func listTasks(cfg *Config, filters []string, viewMode string) {
var tasks []Task
for _, t := range allTasks {
- if !t.IsValid { continue }
- if cfg.HideWaiting && t.IsWaiting && len(filters) == 0 { continue }
-
+ if !t.IsValid {
+ continue
+ }
+ if cfg.HideWaiting && t.IsWaiting && len(filters) == 0 {
+ continue
+ }
+
match := true
rawLower := strings.ToLower(t.RawLine)
for _, f := range filters {
@@ -96,54 +121,92 @@ func listTasks(cfg *Config, filters []string, viewMode string) {
break
}
}
- if match { tasks = append(tasks, t) }
+ if match {
+ tasks = append(tasks, t)
+ }
}
sort.Slice(tasks, func(i, j int) bool {
t1, t2 := tasks[i], tasks[j]
-
+
s1Weight, s2Weight := 1, 1
- if t1.IsPinned { s1Weight = 0 }
- if t1.IsWaiting { s1Weight = 2 }
- if t2.IsPinned { s2Weight = 0 }
- if t2.IsWaiting { s2Weight = 2 }
+ if t1.IsPinned {
+ s1Weight = 0
+ }
+ if t1.IsWaiting {
+ s1Weight = 2
+ }
+ if t2.IsPinned {
+ s2Weight = 0
+ }
+ if t2.IsWaiting {
+ s2Weight = 2
+ }
if viewMode == "flat" {
- if s1Weight != s2Weight { return s1Weight < s2Weight }
+ if s1Weight != s2Weight {
+ return s1Weight < s2Weight
+ }
return t1.ID < t2.ID
}
p1Weight := getProjectWeight(t1, cfg.ProjectOrder)
p2Weight := getProjectWeight(t2, cfg.ProjectOrder)
- if p1Weight != p2Weight { return p1Weight < p2Weight }
-
+ if p1Weight != p2Weight {
+ return p1Weight < p2Weight
+ }
+
c1Weight := getContextWeight(t1, cfg.ContextOrder)
c2Weight := getContextWeight(t2, cfg.ContextOrder)
- if c1Weight != c2Weight { return c1Weight < c2Weight }
+ if c1Weight != c2Weight {
+ return c1Weight < c2Weight
+ }
- if s1Weight != s2Weight { return s1Weight < s2Weight }
+ if s1Weight != s2Weight {
+ return s1Weight < s2Weight
+ }
d1, d2 := 999999, 999999
- if t1.HasDueDate { d1 = t1.DaysUntilDue() }
- if t2.HasDueDate { d2 = t2.DaysUntilDue() }
- if d1 != d2 { return d1 < d2 }
+ if t1.HasDueDate {
+ d1 = t1.DaysUntilDue()
+ }
+ if t2.HasDueDate {
+ d2 = t2.DaysUntilDue()
+ }
+ if d1 != d2 {
+ return d1 < d2
+ }
return t1.ID < t2.ID
})
mDesc, mProj, mCtx, mDue := 4, 7, 7, 3
for _, t := range tasks {
- if len(t.Description) > mDesc { mDesc = len(t.Description) }
+ if len(t.Description) > mDesc {
+ mDesc = len(t.Description)
+ }
pStr := strings.Join(t.Projects, " ")
- if len(pStr) > mProj { mProj = len(pStr) }
+ if len(pStr) > mProj {
+ mProj = len(pStr)
+ }
cStr := strings.Join(t.Contexts, " ")
- if len(cStr) > mCtx { mCtx = len(cStr) }
+ if len(cStr) > mCtx {
+ mCtx = len(cStr)
+ }
dueLen := 5
if t.HasDueDate {
d := t.DaysUntilDue()
- if d < 0 { dueLen = len(fmt.Sprintf("(%dd overdue)", -d)) } else if d == 0 { dueLen = 7 } else { dueLen = len(fmt.Sprintf("(%dd)", d)) }
+ if d < 0 {
+ dueLen = len(fmt.Sprintf("(%dd overdue)", -d))
+ } else if d == 0 {
+ dueLen = 7
+ } else {
+ dueLen = len(fmt.Sprintf("(%dd)", d))
+ }
+ }
+ if dueLen > mDue {
+ mDue = dueLen
}
- if dueLen > mDue { mDue = dueLen }
}
if len(tasks) == 0 {
@@ -156,7 +219,7 @@ func listTasks(cfg *Config, filters []string, viewMode string) {
projHead := fmt.Sprintf("%-*s", mProj, "PROJECT")
ctxHead := fmt.Sprintf("%-*s", mCtx, "CONTEXT")
dueHead := fmt.Sprintf("%-*s", mDue, "DUE")
-
+
headerLine := fmt.Sprintf("ID %s %s %s %s RECUR", descHead, projHead, ctxHead, dueHead)
fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.ColorMuted)).Underline(true).Render(headerLine))
@@ -165,10 +228,12 @@ func listTasks(cfg *Config, filters []string, viewMode string) {
for _, t := range tasks {
total++
-
+
if viewMode != "flat" {
pPrimary := "untagged"
- if len(t.Projects) > 0 { pPrimary = strings.ToLower(t.Projects[0]) }
+ if len(t.Projects) > 0 {
+ pPrimary = strings.ToLower(t.Projects[0])
+ }
if pPrimary != currProj {
pLine := fmt.Sprintf("%s %s", pPrimary, cfg.ProjectIcon)
@@ -182,7 +247,9 @@ func listTasks(cfg *Config, filters []string, viewMode string) {
styleCtx := lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.ColorContext))
styleDue := lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.ColorMuted))
- if t.IsPinned { styleDesc = styleDesc.Bold(true) }
+ if t.IsPinned {
+ styleDesc = styleDesc.Bold(true)
+ }
dueText := "(n/a)"
if t.IsWaiting {
@@ -214,9 +281,11 @@ func listTasks(cfg *Config, filters []string, viewMode string) {
projFmt := styleProj.Render(fmt.Sprintf("%-*s", mProj, strings.Join(t.Projects, " ")))
ctxFmt := styleCtx.Render(fmt.Sprintf("%-*s", mCtx, strings.Join(t.Contexts, " ")))
dueFmt := styleDue.Render(fmt.Sprintf("%-*s", mDue, dueText))
-
+
recFmt := ""
- if t.Recurrence != "" { recFmt = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.ColorMuted)).Render(t.Recurrence) }
+ if t.Recurrence != "" {
+ recFmt = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.ColorMuted)).Render(t.Recurrence)
+ }
strID := lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.ColorMuted)).Render(fmt.Sprintf("%02d", t.ID))
fmt.Printf("%s %s %s %s %s %s\n", strID, descFmt, projFmt, ctxFmt, dueFmt, recFmt)
@@ -224,27 +293,41 @@ func listTasks(cfg *Config, filters []string, viewMode string) {
fmt.Println()
footer := []string{fmt.Sprintf("%d open tasks", total)}
- if len(filters) > 0 { footer[0] = fmt.Sprintf("%d filtered tasks", total) }
- if today > 0 { footer = append(footer, fmt.Sprintf("%d due today", today)) }
- if overdue > 0 { footer = append(footer, fmt.Sprintf("%d overdue", overdue)) }
-
+ if len(filters) > 0 {
+ footer[0] = fmt.Sprintf("%d filtered tasks", total)
+ }
+ if today > 0 {
+ footer = append(footer, fmt.Sprintf("%d due today", today))
+ }
+ if overdue > 0 {
+ footer = append(footer, fmt.Sprintf("%d overdue", overdue))
+ }
+
fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.ColorMuted)).Render(strings.Join(footer, " | ")))
}
func getProjectWeight(t Task, order []string) int {
- if len(t.Projects) == 0 { return 999 }
+ if len(t.Projects) == 0 {
+ return 999
+ }
primary := strings.ToLower(t.Projects[0])
for i, p := range order {
- if strings.ToLower(p) == primary { return i }
+ if strings.ToLower(p) == primary {
+ return i
+ }
}
return 999
}
func getContextWeight(t Task, order []string) int {
- if len(t.Contexts) == 0 { return 999 }
+ if len(t.Contexts) == 0 {
+ return 999
+ }
primary := strings.ToLower(t.Contexts[0])
for i, c := range order {
- if strings.ToLower(c) == primary { return i }
+ if strings.ToLower(c) == primary {
+ return i
+ }
}
return 999
}
diff --git a/task.go b/task.go
index 21d33e5..d35a1dd 100644
--- a/task.go
+++ b/task.go
@@ -27,11 +27,11 @@ func (t Task) DaysUntilDue() int {
return 9999
}
now := time.Now()
-
+
// strip timezones by forcing both today and the due date into raw UTC midnights
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
due := time.Date(t.DueDate.Year(), t.DueDate.Month(), t.DueDate.Day(), 0, 0, 0, 0, time.UTC)
-
+
return int(due.Sub(today).Hours() / 24)
}
diff --git a/tui.go b/tui.go
index 9cadd83..10c03ac 100644
--- a/tui.go
+++ b/tui.go
@@ -25,23 +25,23 @@ type model struct {
cursor int
status string
viewMode string
- inputMode string
+ inputMode string
filterStr string
textInput textinput.Model
undoBuffer []byte
pendingG bool
viewingHistory bool
-
- selected map[int]bool
- bulkEditFile string
-
- activeModal string
- managerCursor int
- helpTab int
-
- width int
- height int
- viewportStart int
+
+ selected map[int]bool
+ bulkEditFile string
+
+ activeModal string
+ managerCursor int
+ helpTab int
+
+ width int
+ height int
+ viewportStart int
}
func LaunchTUI(cfg *Config) error {
@@ -57,7 +57,7 @@ func LaunchTUI(cfg *Config) error {
textInput: ti,
selected: make(map[int]bool),
}
-
+
if cfg.BackendMode != "server" {
refreshTasks(&m)
}
@@ -68,7 +68,9 @@ func LaunchTUI(cfg *Config) error {
}
func refreshTasks(m *model) {
- if m.cfg.BackendMode == "server" { return }
+ if m.cfg.BackendMode == "server" {
+ return
+ }
filePath := m.cfg.TodoFile
if m.viewingHistory {
@@ -81,8 +83,12 @@ func refreshTasks(m *model) {
filterWords := strings.Fields(strings.ToLower(m.filterStr))
for _, t := range tasks {
- if !t.IsValid { continue }
- if m.cfg.HideWaiting && t.IsWaiting && len(filterWords) == 0 && !m.viewingHistory { continue }
+ if !t.IsValid {
+ continue
+ }
+ if m.cfg.HideWaiting && t.IsWaiting && len(filterWords) == 0 && !m.viewingHistory {
+ continue
+ }
match := true
rawLower := strings.ToLower(t.RawLine)
@@ -92,37 +98,61 @@ func refreshTasks(m *model) {
break
}
}
- if match { validTasks = append(validTasks, t) }
+ if match {
+ validTasks = append(validTasks, t)
+ }
}
sort.Slice(validTasks, func(i, j int) bool {
t1, t2 := validTasks[i], validTasks[j]
s1Weight, s2Weight := 1, 1
- if t1.IsPinned { s1Weight = 0 }
- if t1.IsWaiting { s1Weight = 2 }
- if t2.IsPinned { s2Weight = 0 }
- if t2.IsWaiting { s2Weight = 2 }
+ if t1.IsPinned {
+ s1Weight = 0
+ }
+ if t1.IsWaiting {
+ s1Weight = 2
+ }
+ if t2.IsPinned {
+ s2Weight = 0
+ }
+ if t2.IsWaiting {
+ s2Weight = 2
+ }
if m.viewMode == "flat" {
- if s1Weight != s2Weight { return s1Weight < s2Weight }
+ if s1Weight != s2Weight {
+ return s1Weight < s2Weight
+ }
return t1.ID < t2.ID
}
p1Weight := getProjectWeight(t1, m.cfg.ProjectOrder)
p2Weight := getProjectWeight(t2, m.cfg.ProjectOrder)
- if p1Weight != p2Weight { return p1Weight < p2Weight }
-
+ if p1Weight != p2Weight {
+ return p1Weight < p2Weight
+ }
+
c1Weight := getContextWeight(t1, m.cfg.ContextOrder)
c2Weight := getContextWeight(t2, m.cfg.ContextOrder)
- if c1Weight != c2Weight { return c1Weight < c2Weight }
+ if c1Weight != c2Weight {
+ return c1Weight < c2Weight
+ }
- if s1Weight != s2Weight { return s1Weight < s2Weight }
+ if s1Weight != s2Weight {
+ return s1Weight < s2Weight
+ }
d1, d2 := 999999, 999999
- if t1.HasDueDate { d1 = t1.DaysUntilDue() }
- if t2.HasDueDate { d2 = t2.DaysUntilDue() }
- if d1 != d2 { return d1 < d2 }
+ if t1.HasDueDate {
+ d1 = t1.DaysUntilDue()
+ }
+ if t2.HasDueDate {
+ d2 = t2.DaysUntilDue()
+ }
+ if d1 != d2 {
+ return d1 < d2
+ }
return t1.ID < t2.ID
})
@@ -140,7 +170,9 @@ func refreshTasks(m *model) {
func (m *model) saveUndoState() {
b, err := os.ReadFile(m.cfg.TodoFile)
- if err == nil { m.undoBuffer = b }
+ if err == nil {
+ m.undoBuffer = b
+ }
}
func (m model) Init() tea.Cmd {
@@ -180,7 +212,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.saveUndoState()
tasks, _ := ReadTasks(m.cfg.TodoFile)
f, _ := os.Create(m.cfg.TodoFile)
-
+
for _, t := range tasks {
if !m.selected[t.ID] && t.IsValid {
f.WriteString(t.RawLine + "\n")
@@ -220,7 +252,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.helpTab = (m.helpTab + 1) % 3
case "shift+tab", "left", "h":
m.helpTab--
- if m.helpTab < 0 { m.helpTab = 2 }
+ if m.helpTab < 0 {
+ m.helpTab = 2
+ }
}
return m, nil
}
@@ -230,17 +264,21 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "q", "esc", "s", "enter", "ctrl+c":
m.activeModal = ""
case "j", "down":
- if m.managerCursor < len(m.cfg.ProjectOrder)-1 { m.managerCursor++ }
+ if m.managerCursor < len(m.cfg.ProjectOrder)-1 {
+ m.managerCursor++
+ }
case "k", "up":
- if m.managerCursor > 0 { m.managerCursor-- }
- case "J":
+ if m.managerCursor > 0 {
+ m.managerCursor--
+ }
+ case "J":
if m.managerCursor < len(m.cfg.ProjectOrder)-1 {
m.cfg.ProjectOrder[m.managerCursor], m.cfg.ProjectOrder[m.managerCursor+1] = m.cfg.ProjectOrder[m.managerCursor+1], m.cfg.ProjectOrder[m.managerCursor]
m.managerCursor++
saveConfig(m.cfg)
refreshTasks(&m)
}
- case "K":
+ case "K":
if m.managerCursor > 0 {
m.cfg.ProjectOrder[m.managerCursor], m.cfg.ProjectOrder[m.managerCursor-1] = m.cfg.ProjectOrder[m.managerCursor-1], m.cfg.ProjectOrder[m.managerCursor]
m.managerCursor--
@@ -265,20 +303,30 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.saveUndoState()
statusMsg, _ := AddTask(m.cfg.TodoFile, strings.Fields(val))
m.status = statusMsg
- } else { m.status = "add cancelled" }
+ } else {
+ m.status = "add cancelled"
+ }
} else if m.inputMode == "filter" {
m.status = "filter locked. press esc to clear."
} else if strings.HasPrefix(m.inputMode, "bulk_") {
m.saveUndoState()
tasks, _ := ReadTasks(m.cfg.TodoFile)
f, _ := os.Create(m.cfg.TodoFile)
-
+
prefix := ""
mode := m.inputMode
- if mode == "bulk_project" { prefix = "+" }
- if mode == "bulk_context" { prefix = "@" }
- if mode == "bulk_due" { prefix = "due:" }
- if mode == "bulk_recur" { prefix = "rec:" }
+ if mode == "bulk_project" {
+ prefix = "+"
+ }
+ if mode == "bulk_context" {
+ prefix = "@"
+ }
+ if mode == "bulk_due" {
+ prefix = "due:"
+ }
+ if mode == "bulk_recur" {
+ prefix = "rec:"
+ }
finalVal := strings.TrimSpace(val)
@@ -288,17 +336,23 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
finalVal = compiled
}
}
-
+
for _, t := range tasks {
if m.selected[t.ID] {
var newWords []string
for _, w := range strings.Fields(t.RawLine) {
if mode == "bulk_due" {
- if !strings.HasPrefix(strings.ToLower(w), "due:") { newWords = append(newWords, w) }
+ if !strings.HasPrefix(strings.ToLower(w), "due:") {
+ newWords = append(newWords, w)
+ }
} else if mode == "bulk_recur" {
- if !strings.HasPrefix(strings.ToLower(w), "rec:") { newWords = append(newWords, w) }
+ if !strings.HasPrefix(strings.ToLower(w), "rec:") {
+ newWords = append(newWords, w)
+ }
} else {
- if !strings.HasPrefix(w, prefix) { newWords = append(newWords, w) }
+ if !strings.HasPrefix(w, prefix) {
+ newWords = append(newWords, w)
+ }
}
}
if finalVal != "" {
@@ -320,7 +374,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case tea.KeyEsc, tea.KeyCtrlC:
- if m.inputMode == "filter" { m.filterStr = "" }
+ if m.inputMode == "filter" {
+ m.filterStr = ""
+ }
m.inputMode = ""
m.textInput.Blur()
m.status = "cancelled"
@@ -343,7 +399,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "ctrl+c", "q":
return m, tea.Quit
case " ":
- if m.viewingHistory { return m, nil }
+ if m.viewingHistory {
+ return m, nil
+ }
if len(m.tasks) > 0 {
id := m.tasks[m.cursor].ID
if m.selected[id] {
@@ -351,11 +409,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} else {
m.selected[id] = true
}
- if m.cursor < len(m.tasks)-1 { m.cursor++ }
+ if m.cursor < len(m.tasks)-1 {
+ m.cursor++
+ }
}
case "P", "C", "D", "R":
- if m.viewingHistory { return m, nil }
-
+ if m.viewingHistory {
+ return m, nil
+ }
+
if len(m.selected) == 0 && len(m.tasks) > 0 {
m.selected[m.tasks[m.cursor].ID] = true
}
@@ -383,14 +445,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, textinput.Blink
}
case "up", "k":
- if m.cursor > 0 { m.cursor-- }
+ if m.cursor > 0 {
+ m.cursor--
+ }
case "down", "j":
- if m.cursor < len(m.tasks)-1 { m.cursor++ }
+ if m.cursor < len(m.tasks)-1 {
+ m.cursor++
+ }
case "g":
if m.pendingG {
m.cursor = 0
m.pendingG = false
- } else { m.pendingG = true }
+ } else {
+ m.pendingG = true
+ }
case "G":
m.cursor = len(m.tasks) - 1
case "?":
@@ -399,16 +467,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "s":
m.activeModal = "manager"
m.managerCursor = 0
-
+
allTasks, _ := ReadTasks(m.cfg.TodoFile)
existing := make(map[string]bool)
for _, p := range m.cfg.ProjectOrder {
existing[strings.ToLower(p)] = true
}
-
+
addedNew := false
for _, t := range allTasks {
- if !t.IsValid { continue }
+ if !t.IsValid {
+ continue
+ }
for _, p := range t.Projects {
pLower := strings.ToLower(p)
if !existing[pLower] {
@@ -418,29 +488,35 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
}
- if addedNew { saveConfig(m.cfg) }
-
+ if addedNew {
+ saveConfig(m.cfg)
+ }
+
case "E":
if m.viewingHistory {
m.status = "editor disabled in history view"
return m, nil
}
editor := os.Getenv("EDITOR")
- if editor == "" { editor = "vi" }
+ if editor == "" {
+ editor = "vi"
+ }
c := exec.Command(editor, m.cfg.TodoFile)
return m, tea.ExecProcess(c, func(err error) tea.Msg {
return editorFinishedMsg{err: err, isBulk: false}
})
case "e":
- if m.viewingHistory { return m, nil }
+ if m.viewingHistory {
+ return m, nil
+ }
if len(m.tasks) > 0 {
tempFile, _ := os.CreateTemp("", "dew-*.txt")
m.bulkEditFile = tempFile.Name()
-
+
if len(m.selected) == 0 {
m.selected[m.tasks[m.cursor].ID] = true
}
-
+
for _, t := range m.tasks {
if m.selected[t.ID] {
tempFile.WriteString(t.RawLine + "\n")
@@ -448,7 +524,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
tempFile.Close()
editor := os.Getenv("EDITOR")
- if editor == "" { editor = "vi" }
+ if editor == "" {
+ editor = "vi"
+ }
c := exec.Command(editor, m.bulkEditFile)
return m, tea.ExecProcess(c, func(err error) tea.Msg {
return editorFinishedMsg{err: err, isBulk: true}
@@ -466,12 +544,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewingHistory = !m.viewingHistory
if m.viewingHistory {
m.status = "viewing history (read-only)"
- } else { m.status = "viewing active tasks" }
+ } else {
+ m.status = "viewing active tasks"
+ }
m.cursor = 0
m.selected = make(map[int]bool)
refreshTasks(&m)
case "v":
- if m.viewMode == "grouped" { m.viewMode = "flat" } else { m.viewMode = "grouped" }
+ if m.viewMode == "grouped" {
+ m.viewMode = "flat"
+ } else {
+ m.viewMode = "grouped"
+ }
m.status = "switched to " + m.viewMode + " view"
refreshTasks(&m)
case "tab":
@@ -489,7 +573,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.filterStr = "@" + strings.ToLower(t.Contexts[0])
m.status = "pivoted to " + m.filterStr
refreshTasks(&m)
- } else { m.status = "no tags to pivot on" }
+ } else {
+ m.status = "no tags to pivot on"
+ }
}
case "esc":
if m.filterStr != "" {
@@ -512,7 +598,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.selected = make(map[int]bool)
refreshTasks(&m)
}
- } else { m.status = "nothing to undo." }
+ } else {
+ m.status = "nothing to undo."
+ }
case "a":
if m.viewingHistory {
m.status = "actions are disabled in history view"
@@ -535,7 +623,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.status = ""
return m, textinput.Blink
case "d":
- if m.viewingHistory { return m, nil }
+ if m.viewingHistory {
+ return m, nil
+ }
if len(m.selected) > 0 {
m.saveUndoState()
for id := range m.selected {
@@ -553,7 +643,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
refreshTasks(&m)
}
case "p":
- if m.viewingHistory { return m, nil }
+ if m.viewingHistory {
+ return m, nil
+ }
if len(m.selected) > 0 {
m.saveUndoState()
for id := range m.selected {
@@ -571,7 +663,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
refreshTasks(&m)
}
case "w":
- if m.viewingHistory { return m, nil }
+ if m.viewingHistory {
+ return m, nil
+ }
if len(m.selected) > 0 {
m.saveUndoState()
for id := range m.selected {
@@ -589,7 +683,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
refreshTasks(&m)
}
case "r", "x":
- if m.viewingHistory { return m, nil }
+ if m.viewingHistory {
+ return m, nil
+ }
if len(m.selected) > 0 {
m.saveUndoState()
for id := range m.selected {
@@ -627,13 +723,15 @@ func (m model) View() string {
cat := ` /\_/\
( ^.^ )
> ^ <`
- msg := lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorAccent)).Render(cat) +
- "\n\n " + styleTitle.Render("server mode coming soon!") +
- "\n " + styleDesc.Render("dew api integration is currently under construction.") +
- "\n " + styleDesc.Render("please set backend_mode to 'local' in config.json.") +
+ msg := lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorAccent)).Render(cat) +
+ "\n\n " + styleTitle.Render("server mode coming soon!") +
+ "\n " + styleDesc.Render("dew api integration is currently under construction.") +
+ "\n " + styleDesc.Render("please set backend_mode to 'local' in config.json.") +
"\n\n " + styleDesc.Render("[q] quit")
- if !m.cfg.CenterTUI { return "\n" + msg + "\n" }
+ if !m.cfg.CenterTUI {
+ return "\n" + msg + "\n"
+ }
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, lipgloss.NewStyle().Width(lipgloss.Width(msg)).Align(lipgloss.Left).Render(msg))
}
@@ -644,7 +742,7 @@ func (m model) View() string {
tabs := []string{" NAV ", " EDIT ", " META "}
activeTabStyle := lipgloss.NewStyle().Background(lipgloss.Color(m.cfg.ColorAccent)).Foreground(lipgloss.Color("0")).Bold(true)
-
+
for i, t := range tabs {
if i == m.helpTab {
b.WriteString(activeTabStyle.Render(t) + " ")
@@ -695,11 +793,13 @@ func (m model) View() string {
b.WriteString("\n")
}
}
-
+
b.WriteString("\n" + styleDesc.Render("[tab] switch • [esc] close"))
styledBox := styleBox.Render(b.String())
- if !m.cfg.CenterTUI { return "\n" + styledBox + "\n" }
+ if !m.cfg.CenterTUI {
+ return "\n" + styledBox + "\n"
+ }
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, styledBox)
}
@@ -713,19 +813,25 @@ func (m model) View() string {
} else {
for i, item := range m.cfg.ProjectOrder {
cursorLine := " "
- if i == m.managerCursor { cursorLine = styleCursor.Render(fmt.Sprintf("%-2s", m.cfg.CursorIcon)) }
-
+ if i == m.managerCursor {
+ cursorLine = styleCursor.Render(fmt.Sprintf("%-2s", m.cfg.CursorIcon))
+ }
+
itemName := styleDesc.Render(item)
- if i == m.managerCursor { itemName = lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Render(item) }
-
+ if i == m.managerCursor {
+ itemName = lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Render(item)
+ }
+
b.WriteString(fmt.Sprintf("%s%s\n", cursorLine, itemName))
}
}
-
+
b.WriteString("\n" + styleDesc.Render("[J/K] shift up/down • [esc] close"))
styledBox := styleBox.Render(b.String())
- if !m.cfg.CenterTUI { return "\n" + styledBox + "\n" }
+ if !m.cfg.CenterTUI {
+ return "\n" + styledBox + "\n"
+ }
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, styledBox)
}
@@ -733,25 +839,29 @@ func (m model) View() string {
cat := ` /\_/\
( o.o )
> ^ <`
- emptyView := lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorAccent)).Render(cat) +
- "\n\n " + styleTitle.Render("all caught up!") +
+ emptyView := lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorAccent)).Render(cat) +
+ "\n\n " + styleTitle.Render("all caught up!") +
"\n " + styleDesc.Render("press 'a' to add a task, or 'q' to exit.") + "\n"
-
+
if m.viewingHistory {
cat = ` /\_/\
( -.- )
> ^ <`
- emptyView = lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorMuted)).Render(cat) +
- "\n\n " + styleTitle.Render("no history yet.") +
+ emptyView = lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorMuted)).Render(cat) +
+ "\n\n " + styleTitle.Render("no history yet.") +
"\n " + styleDesc.Render("press 'h' to return to active tasks.") + "\n"
}
- if !m.cfg.CenterTUI { return "\n" + emptyView }
+ if !m.cfg.CenterTUI {
+ return "\n" + emptyView
+ }
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, lipgloss.NewStyle().Width(lipgloss.Width(emptyView)).Align(lipgloss.Left).Render(emptyView))
}
maxVisible := m.height - 12
- if maxVisible < 5 { maxVisible = 5 }
+ if maxVisible < 5 {
+ maxVisible = 5
+ }
if m.cursor < m.viewportStart {
m.viewportStart = m.cursor
@@ -759,16 +869,24 @@ func (m model) View() string {
m.viewportStart = m.cursor - maxVisible + 1
}
- if m.viewportStart > len(m.tasks)-maxVisible && len(m.tasks) > maxVisible { m.viewportStart = len(m.tasks) - maxVisible }
- if m.viewportStart < 0 { m.viewportStart = 0 }
+ if m.viewportStart > len(m.tasks)-maxVisible && len(m.tasks) > maxVisible {
+ m.viewportStart = len(m.tasks) - maxVisible
+ }
+ if m.viewportStart < 0 {
+ m.viewportStart = 0
+ }
endIdx := m.viewportStart + maxVisible
- if endIdx > len(m.tasks) { endIdx = len(m.tasks) }
+ if endIdx > len(m.tasks) {
+ endIdx = len(m.tasks)
+ }
visibleTasks := m.tasks[m.viewportStart:endIdx]
mDesc, mProj, mCtx, mDue := 4, 7, 7, 3
- if m.viewingHistory { mDue = 9 }
+ if m.viewingHistory {
+ mDue = 9
+ }
for _, t := range m.tasks {
desc := t.Description
@@ -781,43 +899,67 @@ func (m model) View() string {
}
}
}
- if len(desc) > mDesc { mDesc = len(desc) }
-
+ if len(desc) > mDesc {
+ mDesc = len(desc)
+ }
+
pStr := strings.Join(t.Projects, " ")
- if len(pStr) > mProj { mProj = len(pStr) }
+ if len(pStr) > mProj {
+ mProj = len(pStr)
+ }
cStr := strings.Join(t.Contexts, " ")
- if len(cStr) > mCtx { mCtx = len(cStr) }
-
+ if len(cStr) > mCtx {
+ mCtx = len(cStr)
+ }
+
if m.viewingHistory {
- if 10 > mDue { mDue = 10 }
+ if 10 > mDue {
+ mDue = 10
+ }
} else {
dueLen := 5
if t.HasDueDate {
d := t.DaysUntilDue()
- if d < 0 { dueLen = len(fmt.Sprintf("(%dd overdue)", -d)) } else if d == 0 { dueLen = 7 } else { dueLen = len(fmt.Sprintf("(%dd)", d)) }
+ if d < 0 {
+ dueLen = len(fmt.Sprintf("(%dd overdue)", -d))
+ } else if d == 0 {
+ dueLen = 7
+ } else {
+ dueLen = len(fmt.Sprintf("(%dd)", d))
+ }
+ }
+ if dueLen > mDue {
+ mDue = dueLen
}
- if dueLen > mDue { mDue = dueLen }
}
}
- reservedSpace := mProj + mCtx + mDue + 30
+ reservedSpace := mProj + mCtx + mDue + 30
maxDesc := m.width - reservedSpace
- if maxDesc < 15 { maxDesc = 15 }
- if mDesc > maxDesc { mDesc = maxDesc }
+ if maxDesc < 15 {
+ maxDesc = 15
+ }
+ if mDesc > maxDesc {
+ mDesc = maxDesc
+ }
var b strings.Builder
descStr := "TASK"
- if m.viewingHistory { descStr = "TASK (HISTORY)" }
+ if m.viewingHistory {
+ descStr = "TASK (HISTORY)"
+ }
descHead := fmt.Sprintf("%-*s", mDesc, descStr)
projHead := fmt.Sprintf("%-*s", mProj, "PROJECT")
ctxHead := fmt.Sprintf("%-*s", mCtx, "CONTEXT")
-
+
dueLabel := "DUE"
- if m.viewingHistory { dueLabel = "COMPLETED" }
+ if m.viewingHistory {
+ dueLabel = "COMPLETED"
+ }
dueHead := fmt.Sprintf("%-*s", mDue, dueLabel)
-
+
headerLine := fmt.Sprintf("ID %s %s %s %s RECUR", descHead, projHead, ctxHead, dueHead)
b.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorMuted)).Underline(true).Render(headerLine) + "\n")
@@ -836,7 +978,9 @@ func (m model) View() string {
if m.viewMode != "flat" {
pPrimary := "untagged"
- if len(t.Projects) > 0 { pPrimary = strings.ToLower(t.Projects[0]) }
+ if len(t.Projects) > 0 {
+ pPrimary = strings.ToLower(t.Projects[0])
+ }
if pPrimary != currProj || actualIdx == m.viewportStart {
pLine := fmt.Sprintf("%s %s", pPrimary, m.cfg.ProjectIcon)
@@ -850,7 +994,9 @@ func (m model) View() string {
styleItemCtx := lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorContext))
styleItemDue := lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorMuted))
- if t.IsPinned { styleItemDesc = styleItemDesc.Bold(true) }
+ if t.IsPinned {
+ styleItemDesc = styleItemDesc.Bold(true)
+ }
descText := t.Description
dueText := "(n/a)"
@@ -901,7 +1047,9 @@ func (m model) View() string {
ctxFmt := styleItemCtx.Render(fmt.Sprintf("%-*s", mCtx, strings.Join(t.Contexts, " ")))
dueFmt := styleItemDue.Render(fmt.Sprintf("%-*s", mDue, dueText))
recFmt := ""
- if t.Recurrence != "" { recFmt = lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorMuted)).Render(t.Recurrence) }
+ if t.Recurrence != "" {
+ recFmt = lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorMuted)).Render(t.Recurrence)
+ }
strID := lipgloss.NewStyle().Foreground(lipgloss.Color(m.cfg.ColorMuted)).Render(fmt.Sprintf("%02d", t.ID))
if m.selected[t.ID] {
@@ -910,7 +1058,9 @@ func (m model) View() string {
projFmt = selStyle.Render(fmt.Sprintf("%-*s", mProj, strings.Join(t.Projects, " ")))
ctxFmt = selStyle.Render(fmt.Sprintf("%-*s", mCtx, strings.Join(t.Contexts, " ")))
dueFmt = selStyle.Render(fmt.Sprintf("%-*s", mDue, dueText))
- if t.Recurrence != "" { recFmt = selStyle.Render(t.Recurrence) }
+ if t.Recurrence != "" {
+ recFmt = selStyle.Render(t.Recurrence)
+ }
strID = selStyle.Render(fmt.Sprintf("%02d", t.ID))
}
@@ -925,7 +1075,7 @@ func (m model) View() string {
b.WriteString("\n\n")
tableWidth := mDesc + mProj + mCtx + mDue + 21
-
+
if m.inputMode != "" {
b.WriteString(" " + m.textInput.View())
} else {
@@ -935,14 +1085,16 @@ func (m model) View() string {
} else if !m.cfg.HideHints {
footerLeft = styleHelp.Render("[?] help • [a] add • [e] edit • [d] done • [u] undo")
}
-
+
posStr := styleStatus.Render(fmt.Sprintf("[%d/%d]", m.cursor+1, len(m.tasks)))
-
+
gap := tableWidth - lipgloss.Width(footerLeft) - lipgloss.Width(posStr)
- if gap < 1 { gap = 1 }
-
+ if gap < 1 {
+ gap = 1
+ }
+
b.WriteString(" " + footerLeft + strings.Repeat(" ", gap) + posStr)
-
+
if m.filterStr != "" {
b.WriteString("\n ")
b.WriteString(styleStatus.Render(fmt.Sprintf("active filter: '%s' (press esc to clear)", m.filterStr)))
@@ -955,16 +1107,16 @@ func (m model) View() string {
b.WriteString("\n")
content := b.String()
-
+
if !m.cfg.CenterTUI {
return content
}
contentWidth := lipgloss.Width(content)
-
+
if contentWidth < 90 {
contentWidth = 90
}
-
+
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, lipgloss.NewStyle().Width(contentWidth).Align(lipgloss.Left).Render(content))
}