[WIP] Task editing
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
app.log
|
||||||
|
test/taskchampion.sqlite3
|
||||||
|
|||||||
280
common/task.go
Normal file
280
common/task.go
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Annotation struct {
|
||||||
|
Entry string `json:"entry,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Annotation) String() string {
|
||||||
|
return fmt.Sprintf("%s %s", a.Entry, a.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
Id int64 `json:"id,omitempty"`
|
||||||
|
Uuid string `json:"uuid,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Project string `json:"project"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Depends []string `json:"depends,omitempty"`
|
||||||
|
DependsIds string `json:"-"`
|
||||||
|
Urgency float32 `json:"urgency,omitempty"`
|
||||||
|
Parent string `json:"parent,omitempty"`
|
||||||
|
Due string `json:"due,omitempty"`
|
||||||
|
Wait string `json:"wait,omitempty"`
|
||||||
|
Scheduled string `json:"scheduled,omitempty"`
|
||||||
|
Until string `json:"until,omitempty"`
|
||||||
|
Start string `json:"start,omitempty"`
|
||||||
|
End string `json:"end,omitempty"`
|
||||||
|
Entry string `json:"entry,omitempty"`
|
||||||
|
Modified string `json:"modified,omitempty"`
|
||||||
|
Recur string `json:"recur,omitempty"`
|
||||||
|
Annotations []Annotation `json:"annotations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) GetString(fieldWFormat string) string {
|
||||||
|
field, format, _ := strings.Cut(fieldWFormat, ".")
|
||||||
|
|
||||||
|
switch field {
|
||||||
|
case "id":
|
||||||
|
return strconv.FormatInt(t.Id, 10)
|
||||||
|
|
||||||
|
case "uuid":
|
||||||
|
if format == "short" {
|
||||||
|
return t.Uuid[:8]
|
||||||
|
}
|
||||||
|
return t.Uuid
|
||||||
|
|
||||||
|
case "description":
|
||||||
|
switch format {
|
||||||
|
case "desc":
|
||||||
|
return t.Description
|
||||||
|
case "oneline":
|
||||||
|
if len(t.Annotations) == 0 {
|
||||||
|
return t.Description
|
||||||
|
} else {
|
||||||
|
var annotations []string
|
||||||
|
for _, a := range t.Annotations {
|
||||||
|
annotations = append(annotations, a.String())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", t.Description, strings.Join(annotations, " "))
|
||||||
|
}
|
||||||
|
case "truncated":
|
||||||
|
if len(t.Description) > 20 {
|
||||||
|
return t.Description[:20] + "..."
|
||||||
|
} else {
|
||||||
|
return t.Description
|
||||||
|
}
|
||||||
|
case "count":
|
||||||
|
return fmt.Sprintf("%s [%d]", t.Description, len(t.Annotations))
|
||||||
|
case "truncated_count":
|
||||||
|
if len(t.Description) > 20 {
|
||||||
|
return fmt.Sprintf("%s... [%d]", t.Description[:20], len(t.Annotations))
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%s [%d]", t.Description, len(t.Annotations))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.Annotations) == 0 {
|
||||||
|
return t.Description
|
||||||
|
} else {
|
||||||
|
var annotations []string
|
||||||
|
for _, a := range t.Annotations {
|
||||||
|
annotations = append(annotations, a.String())
|
||||||
|
}
|
||||||
|
// TODO support for multiline?
|
||||||
|
return fmt.Sprintf("%s %s", t.Description, strings.Join(annotations, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
case "project":
|
||||||
|
switch format {
|
||||||
|
case "parent":
|
||||||
|
parent, _, _ := strings.Cut(t.Project, ".")
|
||||||
|
return parent
|
||||||
|
case "indented":
|
||||||
|
return fmt.Sprintf(" %s", t.Project)
|
||||||
|
}
|
||||||
|
return t.Project
|
||||||
|
|
||||||
|
case "priority":
|
||||||
|
return t.Priority
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
return t.Status
|
||||||
|
|
||||||
|
case "tags":
|
||||||
|
switch format {
|
||||||
|
case "count":
|
||||||
|
return strconv.Itoa(len(t.Tags))
|
||||||
|
case "indicator":
|
||||||
|
if len(t.Tags) > 0 {
|
||||||
|
return "+"
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(t.Tags, " ")
|
||||||
|
|
||||||
|
case "parent":
|
||||||
|
if format == "short" {
|
||||||
|
return t.Parent[:8]
|
||||||
|
}
|
||||||
|
return t.Parent
|
||||||
|
|
||||||
|
case "urgency":
|
||||||
|
if format == "integer" {
|
||||||
|
return strconv.Itoa(int(t.Urgency))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f", t.Urgency)
|
||||||
|
|
||||||
|
case "due":
|
||||||
|
return formatDate(t.Due, format)
|
||||||
|
|
||||||
|
case "wait":
|
||||||
|
return formatDate(t.Wait, format)
|
||||||
|
|
||||||
|
case "scheduled":
|
||||||
|
return formatDate(t.Scheduled, format)
|
||||||
|
|
||||||
|
case "end":
|
||||||
|
return formatDate(t.End, format)
|
||||||
|
|
||||||
|
case "entry":
|
||||||
|
return formatDate(t.Entry, format)
|
||||||
|
|
||||||
|
case "modified":
|
||||||
|
return formatDate(t.Modified, format)
|
||||||
|
|
||||||
|
case "start":
|
||||||
|
return formatDate(t.Start, format)
|
||||||
|
|
||||||
|
case "until":
|
||||||
|
return formatDate(t.Until, format)
|
||||||
|
|
||||||
|
case "depends":
|
||||||
|
switch format {
|
||||||
|
case "count":
|
||||||
|
return strconv.Itoa(len(t.Depends))
|
||||||
|
case "indicator":
|
||||||
|
if len(t.Depends) > 0 {
|
||||||
|
return "D"
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.DependsIds
|
||||||
|
|
||||||
|
case "recur":
|
||||||
|
return t.Recur
|
||||||
|
|
||||||
|
default:
|
||||||
|
slog.Error(fmt.Sprintf("Field not implemented: %s", field))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
func formatDate(date string, format string) string {
|
||||||
|
if date == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
dtformat := "20060102T150405Z"
|
||||||
|
dt, err := time.Parse(dtformat, date)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to parse time:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "formatted", "":
|
||||||
|
return dt.Format("2006-01-02 15:04")
|
||||||
|
// TODO: proper julian date formatting
|
||||||
|
case "julian":
|
||||||
|
return dt.Format("060102.1504")
|
||||||
|
case "epoch":
|
||||||
|
return strconv.FormatInt(dt.Unix(), 10)
|
||||||
|
case "iso":
|
||||||
|
return dt.Format("2006-01-02T150405Z")
|
||||||
|
case "age":
|
||||||
|
return parseDurationVague(time.Since(dt))
|
||||||
|
case "relative":
|
||||||
|
return parseDurationVague(time.Until(dt))
|
||||||
|
// TODO: implement remaining
|
||||||
|
case "remaining":
|
||||||
|
return ""
|
||||||
|
case "countdown":
|
||||||
|
return parseCountdown(time.Since(dt))
|
||||||
|
default:
|
||||||
|
slog.Error(fmt.Sprintf("Date format not implemented: %s", format))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDurationVague(d time.Duration) string {
|
||||||
|
dur := d.Round(time.Second).Abs()
|
||||||
|
days := dur.Hours() / 24
|
||||||
|
|
||||||
|
var formatted string
|
||||||
|
if dur >= time.Hour*24*365 {
|
||||||
|
formatted = fmt.Sprintf("%.1fy", days/365)
|
||||||
|
} else if dur >= time.Hour*24*90 {
|
||||||
|
formatted = strconv.Itoa(int(math.Round(days/30))) + "mo"
|
||||||
|
} else if dur >= time.Hour*24*7 {
|
||||||
|
formatted = strconv.Itoa(int(math.Round(days/7))) + "w"
|
||||||
|
} else if dur >= time.Hour*24 {
|
||||||
|
formatted = strconv.Itoa(int(days)) + "d"
|
||||||
|
} else if dur >= time.Hour {
|
||||||
|
formatted = strconv.Itoa(int(dur.Round(time.Hour).Hours())) + "h"
|
||||||
|
} else if dur >= time.Minute {
|
||||||
|
formatted = strconv.Itoa(int(dur.Round(time.Minute).Minutes())) + "min"
|
||||||
|
} else if dur >= time.Second {
|
||||||
|
formatted = strconv.Itoa(int(dur.Round(time.Second).Seconds())) + "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
if d < 0 {
|
||||||
|
formatted = "-" + formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCountdown(d time.Duration) string {
|
||||||
|
hours := int(d.Hours())
|
||||||
|
minutes := int(d.Minutes()) % 60
|
||||||
|
seconds := int(d.Seconds()) % 60
|
||||||
|
|
||||||
|
return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
}
|
||||||
14
main.go
14
main.go
@ -3,6 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"tasksquire/common"
|
"tasksquire/common"
|
||||||
@ -17,6 +19,18 @@ func main() {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
common := common.NewCommon(ctx, ts)
|
common := common.NewCommon(ctx, ts)
|
||||||
|
|
||||||
|
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open log file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Create a new slog handler for the file
|
||||||
|
handler := slog.NewTextHandler(file, &slog.HandlerOptions{})
|
||||||
|
|
||||||
|
// Set the default logger to use the file handler
|
||||||
|
slog.SetDefault(slog.New(handler))
|
||||||
|
|
||||||
// form := huh.NewForm(
|
// form := huh.NewForm(
|
||||||
// huh.NewGroup(
|
// huh.NewGroup(
|
||||||
// huh.NewSelect[string]().
|
// huh.NewSelect[string]().
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
// TODO: update table every second (to show correct relative time)
|
||||||
package pages
|
package pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"tasksquire/common"
|
"tasksquire/common"
|
||||||
"tasksquire/taskwarrior"
|
"tasksquire/taskwarrior"
|
||||||
|
|
||||||
@ -161,7 +163,7 @@ func (p *ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
if p.tasks != nil && len(p.tasks) > 0 {
|
if p.tasks != nil && len(p.tasks) > 0 {
|
||||||
p.selectedTask = (*taskwarrior.Task)(p.tasks[p.taskTable.Cursor()])
|
p.selectedTask = p.tasks[p.taskTable.Cursor()]
|
||||||
} else {
|
} else {
|
||||||
p.selectedTask = nil
|
p.selectedTask = nil
|
||||||
}
|
}
|
||||||
@ -185,6 +187,7 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
|
|||||||
columnSizes := make([]int, nCols)
|
columnSizes := make([]int, nCols)
|
||||||
fullRows := make([]table.Row, len(tasks))
|
fullRows := make([]table.Row, len(tasks))
|
||||||
rows := make([]table.Row, len(tasks))
|
rows := make([]table.Row, len(tasks))
|
||||||
|
descIndex := -1
|
||||||
|
|
||||||
for i, task := range tasks {
|
for i, task := range tasks {
|
||||||
if p.selectedTask != nil && task.Uuid == p.selectedTask.Uuid {
|
if p.selectedTask != nil && task.Uuid == p.selectedTask.Uuid {
|
||||||
@ -193,6 +196,9 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
|
|||||||
|
|
||||||
row := table.Row{}
|
row := table.Row{}
|
||||||
for i, col := range p.activeReport.Columns {
|
for i, col := range p.activeReport.Columns {
|
||||||
|
if strings.Contains(col, "description") {
|
||||||
|
descIndex = i
|
||||||
|
}
|
||||||
field := task.GetString(col)
|
field := task.GetString(col)
|
||||||
columnSizes[i] = max(columnSizes[i], len(field))
|
columnSizes[i] = max(columnSizes[i], len(field))
|
||||||
row = append(row, field)
|
row = append(row, field)
|
||||||
@ -211,12 +217,24 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
|
|||||||
rows[i] = row
|
rows[i] = row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
combinedSize := 0
|
||||||
for i, label := range p.activeReport.Labels {
|
for i, label := range p.activeReport.Labels {
|
||||||
if columnSizes[i] == 0 {
|
if columnSizes[i] == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = append(columns, table.Column{Title: label, Width: max(columnSizes[i], len(label))})
|
width := max(columnSizes[i], len(label))
|
||||||
|
columns = append(columns, table.Column{Title: label, Width: width})
|
||||||
|
|
||||||
|
if i == descIndex {
|
||||||
|
descIndex = len(columns) - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
combinedSize += width
|
||||||
|
}
|
||||||
|
|
||||||
|
if descIndex >= 0 {
|
||||||
|
columns[descIndex].Width = p.taskTable.Width() - combinedSize - 14
|
||||||
}
|
}
|
||||||
|
|
||||||
if selected == 0 {
|
if selected == 0 {
|
||||||
|
|||||||
@ -105,6 +105,7 @@ func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditor
|
|||||||
|
|
||||||
huh.NewMultiSelect[string]().
|
huh.NewMultiSelect[string]().
|
||||||
Options(huh.NewOptions(tagOptions...)...).
|
Options(huh.NewOptions(tagOptions...)...).
|
||||||
|
// Key("tags").
|
||||||
Title("Tags").
|
Title("Tags").
|
||||||
Value(&p.task.Tags),
|
Value(&p.task.Tags),
|
||||||
|
|
||||||
@ -245,6 +246,8 @@ func (p *TaskEditorPage) updateTasksCmd() tea.Msg {
|
|||||||
if p.additionalProject != "" {
|
if p.additionalProject != "" {
|
||||||
p.task.Project = p.additionalProject
|
p.task.Project = p.additionalProject
|
||||||
}
|
}
|
||||||
|
// tags := p.form.Get("tags").([]string)
|
||||||
|
// p.task.Tags = tags
|
||||||
p.common.TW.ImportTask(&p.task)
|
p.common.TW.ImportTask(&p.task)
|
||||||
return UpdatedTasksMsg{}
|
return UpdatedTasksMsg{}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,18 +27,18 @@ type Task struct {
|
|||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Depends []string `json:"depends,omitempty"`
|
Depends []string `json:"depends,omitempty"`
|
||||||
DependsIds string
|
DependsIds string `json:"-"`
|
||||||
Urgency float32 `json:"urgency,omitempty"`
|
Urgency float32 `json:"urgency,omitempty"`
|
||||||
Parent string `json:"parent,omitempty"`
|
Parent string `json:"parent,omitempty"`
|
||||||
Due string `json:"due"`
|
Due string `json:"due,omitempty"`
|
||||||
Wait string `json:"wait"`
|
Wait string `json:"wait,omitempty"`
|
||||||
Scheduled string `json:"scheduled"`
|
Scheduled string `json:"scheduled,omitempty"`
|
||||||
Until string `json:"until"`
|
Until string `json:"until,omitempty"`
|
||||||
Start string `json:"start,omitempty"`
|
Start string `json:"start,omitempty"`
|
||||||
End string `json:"end,omitempty"`
|
End string `json:"end,omitempty"`
|
||||||
Entry string `json:"entry,omitempty"`
|
Entry string `json:"entry,omitempty"`
|
||||||
Modified string `json:"modified,omitempty"`
|
Modified string `json:"modified,omitempty"`
|
||||||
Recur string `json:"recur"`
|
Recur string `json:"recur,omitempty"`
|
||||||
Annotations []Annotation `json:"annotations,omitempty"`
|
Annotations []Annotation `json:"annotations,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -368,6 +368,7 @@ func (ts *TaskSquire) ImportTask(task *Task) {
|
|||||||
defer ts.mutex.Unlock()
|
defer ts.mutex.Unlock()
|
||||||
|
|
||||||
tasks, err := json.Marshal(Tasks{task})
|
tasks, err := json.Marshal(Tasks{task})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed marshalling task:", err)
|
slog.Error("Failed marshalling task:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user