diff --git a/common/keymap.go b/common/keymap.go index 9988d55..3ff5b42 100644 --- a/common/keymap.go +++ b/common/keymap.go @@ -21,9 +21,12 @@ type Keymap struct { SetProject key.Binding Select key.Binding Insert key.Binding + Tag key.Binding Undo key.Binding + StartStop key.Binding } +// TODO: use config values for key bindings // NewKeymap creates a new Keymap. func NewKeymap() *Keymap { return &Keymap{ @@ -102,9 +105,19 @@ func NewKeymap() *Keymap { key.WithHelp("insert", "Insert mode"), ), + Tag: key.NewBinding( + key.WithKeys("t"), + key.WithHelp("tag", "Tag"), + ), + Undo: key.NewBinding( key.WithKeys("u"), key.WithHelp("undo", "Undo"), ), + + StartStop: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("start/stop", "Start/Stop"), + ), } } diff --git a/common/styles.go b/common/styles.go index 3e149c7..29266ea 100644 --- a/common/styles.go +++ b/common/styles.go @@ -25,6 +25,11 @@ type Styles struct { Form *huh.Theme TableStyle TableStyle + ColumnFocused lipgloss.Style + ColumnBlurred lipgloss.Style + ColumnInsert lipgloss.Style + + // TODO: make color config completely dynamic to account for keyword., project., tag. and uda. colors Active lipgloss.Style Alternate lipgloss.Style Blocked lipgloss.Style @@ -100,6 +105,10 @@ func NewStyles(config *taskwarrior.TWConfig) *Styles { styles.Form = formTheme + styles.ColumnFocused = lipgloss.NewStyle().Width(30).Height(50).Border(lipgloss.DoubleBorder(), true) + styles.ColumnBlurred = lipgloss.NewStyle().Width(30).Height(50).Border(lipgloss.HiddenBorder(), true) + styles.ColumnInsert = lipgloss.NewStyle().Width(30).Height(50).Border(lipgloss.DoubleBorder(), true).BorderForeground(styles.Active.GetForeground()) + return styles } diff --git a/go.mod b/go.mod index d4f02d1..f716149 100644 --- a/go.mod +++ b/go.mod @@ -2,32 +2,31 @@ module tasksquire go 1.22.2 +require ( + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.26.1 + github.com/charmbracelet/huh v0.3.0 + github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb + github.com/mattn/go-runewidth v0.0.15 + golang.org/x/term v0.20.0 +) + 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 + github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // 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 ) diff --git a/go.sum b/go.sum index d540a20..22a7ee2 100644 --- a/go.sum +++ b/go.sum @@ -4,34 +4,20 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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= @@ -41,8 +27,6 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei 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= @@ -53,26 +37,17 @@ 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= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 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= diff --git a/pages/main.go b/pages/main.go index b1c17a1..4b048fc 100644 --- a/pages/main.go +++ b/pages/main.go @@ -17,6 +17,7 @@ func NewMainPage(common *common.Common) *MainPage { } m.activePage = NewReportPage(common, common.TW.GetReport(common.TW.GetConfig().Get("uda.tasksquire.report.default"))) + // m.activePage = NewTaskEditorPage(common, taskwarrior.Task{}) return m diff --git a/pages/messaging.go b/pages/messaging.go new file mode 100644 index 0000000..f7165a1 --- /dev/null +++ b/pages/messaging.go @@ -0,0 +1,69 @@ +package pages + +import tea "github.com/charmbracelet/bubbletea" + +type UpdatedTasksMsg struct{} + +type nextColumnMsg struct{} + +func nextColumn() tea.Cmd { + return func() tea.Msg { + return nextColumnMsg{} + } +} + +type prevColumnMsg struct{} + +func prevColumn() tea.Cmd { + return func() tea.Msg { + return prevColumnMsg{} + } +} + +type nextFieldMsg struct{} + +func nextField() tea.Cmd { + return func() tea.Msg { + return nextFieldMsg{} + } +} + +type prevFieldMsg struct{} + +func prevField() tea.Cmd { + return func() tea.Msg { + return prevFieldMsg{} + } +} + +type nextAreaMsg struct{} + +func nextArea() tea.Cmd { + return func() tea.Msg { + return nextAreaMsg{} + } +} + +type prevAreaMsg struct{} + +func prevArea() tea.Cmd { + return func() tea.Msg { + return prevAreaMsg{} + } +} + +type changeAreaMsg area + +func changeArea(a area) tea.Cmd { + return func() tea.Msg { + return changeAreaMsg(a) + } +} + +func changeMode(mode mode) tea.Cmd { + return func() tea.Msg { + return changeModeMsg(mode) + } +} + +type changeModeMsg mode diff --git a/pages/report.go b/pages/report.go index 998d9a8..4bab187 100644 --- a/pages/report.go +++ b/pages/report.go @@ -23,18 +23,31 @@ type ReportPage struct { taskTable table.Model - subpage tea.Model - subpageActive bool + subpage tea.Model } func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage { - return &ReportPage{ + // return &ReportPage{ + // common: com, + // activeReport: report, + // activeContext: com.TW.GetActiveContext(), + // activeProject: "", + // taskTable: table.New(com), + // } + + p := &ReportPage{ common: com, activeReport: report, activeContext: com.TW.GetActiveContext(), activeProject: "", taskTable: table.New(com), } + + p.subpage = NewTaskEditorPage(p.common, taskwarrior.Task{}) + p.subpage.Init() + p.common.PushPage(p) + + return p } func (p *ReportPage) SetSize(width int, height int) { @@ -53,8 +66,7 @@ func (p *ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: p.SetSize(msg.Width, msg.Height) - case BackMsg: - p.subpageActive = false + // case BackMsg: case TaskMsg: p.tasks = taskwarrior.Tasks(msg) p.populateTaskTable(p.tasks) @@ -77,25 +89,21 @@ func (p *ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, p.common.Keymap.SetReport): p.subpage = NewReportPickerPage(p.common, p.activeReport) p.subpage.Init() - p.subpageActive = true p.common.PushPage(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.PushPage(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.PushPage(p) return p.subpage, nil case key.Matches(msg, p.common.Keymap.Edit): p.subpage = NewTaskEditorPage(p.common, *p.selectedTask) p.subpage.Init() - p.subpageActive = true p.common.PushPage(p) return p.subpage, nil case key.Matches(msg, p.common.Keymap.Ok): @@ -104,12 +112,32 @@ func (p *ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, p.common.Keymap.SetProject): p.subpage = NewProjectPickerPage(p.common, p.activeProject) p.subpage.Init() - p.subpageActive = true p.common.PushPage(p) return p.subpage, nil + case key.Matches(msg, p.common.Keymap.Tag): + if p.selectedTask != nil { + tag := p.common.TW.GetConfig().Get("uda.tasksquire.tag.default") + if p.selectedTask.HasTag(tag) { + p.selectedTask.RemoveTag(tag) + } else { + p.selectedTask.AddTag(tag) + } + p.common.TW.ImportTask(p.selectedTask) + return p, p.getTasks() + } + return p, nil case key.Matches(msg, p.common.Keymap.Undo): p.common.TW.Undo() return p, p.getTasks() + case key.Matches(msg, p.common.Keymap.StartStop): + if p.selectedTask != nil && p.selectedTask.Status == "pending" { + if p.selectedTask.Start == "" { + p.common.TW.StartTask(p.selectedTask) + } else { + p.common.TW.StopTask(p.selectedTask) + } + return p, p.getTasks() + } } } diff --git a/pages/taskEditor.go b/pages/taskEditor.go index ae1ce24..b4d5f14 100644 --- a/pages/taskEditor.go +++ b/pages/taskEditor.go @@ -3,61 +3,43 @@ package pages import ( "fmt" "log/slog" - "strings" "tasksquire/common" + "tasksquire/taskwarrior" - "time" "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" ) -type Mode int +type mode int const ( - ModeNormal Mode = iota - ModeInsert - ModeAddTag - ModeAddProject + modeNormal mode = iota + modeInsert ) type TaskEditorPage struct { - common *common.Common - task taskwarrior.Task - form *huh.Form - mode Mode - statusline tea.Model - nFields int - currentField int + common *common.Common + task taskwarrior.Task - // TODO: rework support for adding tags and projects - additionalTags string - additionalProject string + mode mode + + columnCursor int + + area area + areaPicker *areaPicker + areas map[area]tea.Model } -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, +func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPage { + p := TaskEditorPage{ + common: com, task: task, } - if p.task.Uuid == "" { - p.mode = ModeInsert - } else { - p.mode = ModeNormal - } - if p.task.Priority == "" { p.task.Priority = "(none)" } @@ -65,87 +47,30 @@ func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditor p.task.Project = "(none)" } - priorityOptions := append([]string{"(none)"}, common.TW.GetPriorities()...) - projectOptions := append([]string{"(none)"}, common.TW.GetProjects()...) - tagOptions := common.TW.GetTags() + priorityOptions := append([]string{"(none)"}, p.common.TW.GetPriorities()...) + projectOptions := append([]string{"(none)"}, p.common.TW.GetProjects()...) + tagOptions := p.common.TW.GetTags() - p.form = huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title("Task"). - Value(&p.task.Description). - Validate(func(desc string) error { - if desc == "" { - return fmt.Errorf("task description is required") - } - return nil - }). - Inline(true), + p.areas = map[area]tea.Model{ + areaTask: NewTaskEdit(p.common, &p.task.Description, &p.task.Priority, &p.task.Project, priorityOptions, projectOptions), + areaTags: NewTagEdit(p.common, &p.task.Tags, tagOptions), + areaTime: NewTimeEdit(p.common, &p.task.Due, &p.task.Scheduled, &p.task.Wait, &p.task.Until), + } - huh.NewSelect[string](). - Options(huh.NewOptions(priorityOptions...)...). - Title("Priority"). - Value(&p.task.Priority), + // p.areaList = NewAreaList(common, areaItems) + // p.selectedArea = areaTask + // p.columns = append([]tea.Model{p.areaList}, p.areas[p.selectedArea]...) - huh.NewSelect[string](). - Options(huh.NewOptions(projectOptions...)...). - Title("Project"). - Value(&p.task.Project), + p.areaPicker = NewAreaPicker(com, []string{"Task", "Tags", "Dates"}) - huh.NewInput(). - Title("Project"). - Value(&p.additionalProject). - Validate(func(project string) error { - if strings.Contains(project, " ") { - return fmt.Errorf("project name cannot contain spaces") - } - return nil - }). - Inline(true), + if p.task.Uuid == "" { + // p.mode = modeInsert + p.mode = modeNormal + } else { + p.mode = modeNormal + } - huh.NewMultiSelect[string](). - Options(huh.NewOptions(tagOptions...)...). - // Key("tags"). - Title("Tags"). - Value(&p.task.Tags), - - huh.NewInput(). - Title("Tags"). - Value(&p.additionalTags). - Inline(true), - - 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(true). - // use styles from common - WithHeight(40). - WithWidth(50). - WithTheme(p.common.Styles.Form) - - p.nFields = 6 - p.currentField = 0 - - p.statusline = NewStatusLine(common, p.mode) - - return p + return &p } func (p *TaskEditorPage) SetSize(width, height int) { @@ -153,23 +78,39 @@ func (p *TaskEditorPage) SetSize(width, height int) { } func (p *TaskEditorPage) Init() tea.Cmd { - return p.form.Init() + return nil } func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case SwitchModeMsg: - switch Mode(msg) { - case ModeNormal: - p.mode = ModeNormal - case ModeInsert: - p.mode = ModeInsert + case changeAreaMsg: + p.area = area(msg) + case changeModeMsg: + p.mode = mode(msg) + case prevColumnMsg: + p.columnCursor-- + if p.columnCursor < 0 { + p.columnCursor = len(p.areas) - 1 + } + case nextColumnMsg: + p.columnCursor++ + if p.columnCursor > len(p.areas)-1 { + p.columnCursor = 0 + } + case prevAreaMsg: + p.area-- + if p.area < 0 { + p.area = 2 + } + case nextAreaMsg: + p.area++ + if p.area > 2 { + p.area = 0 } } + switch p.mode { - case ModeNormal: + case modeNormal: switch msg := msg.(type) { case tea.KeyMsg: switch { @@ -181,58 +122,665 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return model, BackCmd case key.Matches(msg, p.common.Keymap.Insert): - return p, p.switchModeCmd(ModeInsert) + return p, changeMode(modeInsert) case key.Matches(msg, p.common.Keymap.Ok): - p.form.State = huh.StateCompleted - case key.Matches(msg, p.common.Keymap.Down): - p.form.NextField() + model, err := p.common.PopPage() + if err != nil { + slog.Error("page stack empty") + return nil, tea.Quit + } + return model, p.updateTasksCmd + case key.Matches(msg, p.common.Keymap.Left): + return p, prevColumn() + case key.Matches(msg, p.common.Keymap.Right): + return p, nextColumn() case key.Matches(msg, p.common.Keymap.Up): - p.form.PrevField() + var cmd tea.Cmd + if p.columnCursor == 0 { + picker, cmd := p.areaPicker.Update(msg) + p.areaPicker = picker.(*areaPicker) + return p, cmd + } else { + p.areas[p.area], cmd = p.areas[p.area].Update(prevFieldMsg{}) + return p, cmd + } + case key.Matches(msg, p.common.Keymap.Down): + var cmd tea.Cmd + if p.columnCursor == 0 { + picker, cmd := p.areaPicker.Update(msg) + p.areaPicker = picker.(*areaPicker) + return p, cmd + } else { + p.areas[p.area], cmd = p.areas[p.area].Update(nextFieldMsg{}) + return p, cmd + } } } - case ModeInsert: + + // var cmd tea.Cmd + // if p.columnCursor == 0 { + // p., cmd = p.areaList.Update(msg) + // p.selectedArea = p.areaList.(areaList).Area() + // cmds = append(cmds, cmd) + // } else { + // p.areas[p.selectedArea][p.columnCursor-1], cmd = p.areas[p.selectedArea][p.columnCursor-1].Update(msg) + // cmds = append(cmds, cmd) + // } + case modeInsert: switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, p.common.Keymap.Back): - return p, p.switchModeCmd(ModeNormal) + return p, changeMode(modeNormal) + case key.Matches(msg, p.common.Keymap.Ok): + return p, nextField() } } - f, cmd := p.form.Update(msg) - if f, ok := f.(*huh.Form); ok { - p.form = f - cmds = append(cmds, cmd) + var cmd tea.Cmd + if p.columnCursor == 0 { + picker, cmd := p.areaPicker.Update(msg) + p.areaPicker = picker.(*areaPicker) + return p, cmd + } else { + p.areas[p.area], cmd = p.areas[p.area].Update(msg) + return p, cmd } - } - - var cmd tea.Cmd - p.statusline, cmd = p.statusline.Update(msg) - cmds = append(cmds, cmd) - - if p.form.State == huh.StateCompleted { - cmds = append(cmds, p.updateTasksCmd) - model, err := p.common.PopPage() - if err != nil { - slog.Error("page stack empty") - return nil, tea.Quit - } - return model, tea.Batch(cmds...) - } - - return p, tea.Batch(cmds...) + return p, nil } func (p *TaskEditorPage) View() string { - return lipgloss.JoinVertical( + var focusStyle lipgloss.Style + if p.mode == modeInsert { + focusStyle = p.common.Styles.ColumnInsert + } else { + focusStyle = p.common.Styles.ColumnFocused + } + var picker, area string + if p.columnCursor == 0 { + picker = focusStyle.Render(p.areaPicker.View()) + area = p.common.Styles.ColumnBlurred.Render(p.areas[p.area].View()) + } else { + picker = p.common.Styles.ColumnBlurred.Render(p.areaPicker.View()) + area = focusStyle.Render(p.areas[p.area].View()) + + } + return lipgloss.JoinHorizontal( lipgloss.Left, - // lipgloss.Place(p.common.Width(), p.common.Height()-1, 0.5, 0.5, p.form.View(), lipgloss.WithWhitespaceBackground(p.common.Styles.Warning.GetForeground())), - lipgloss.Place(p.common.Width(), p.common.Height()-1, 0.5, 0.5, p.form.View(), lipgloss.WithWhitespaceChars(" . ")), - p.statusline.View(), + picker, + area, ) } +// import ( +// "fmt" +// "io" +// "log/slog" +// "strings" +// "tasksquire/common" +// "tasksquire/taskwarrior" +// "time" + +// "github.com/charmbracelet/bubbles/list" +// "github.com/charmbracelet/bubbles/textinput" +// tea "github.com/charmbracelet/bubbletea" +// "github.com/charmbracelet/huh" +// "github.com/charmbracelet/lipgloss" +// ) + +// type Field int + +// const ( +// FieldDescription Field = iota +// FieldPriority +// FieldProject +// FieldNewProject +// FieldTags +// FieldNewTags +// FieldDue +// FieldScheduled +// FieldWait +// FieldUntil +// ) + +// type column int + +// const ( +// column1 column = iota +// column2 +// column3 +// ) + +// func changeColumn(c column) tea.Cmd { +// return func() tea.Msg { +// return changeColumnMsg(c) +// } +// } + +// type changeColumnMsg column + +// type mode int + +// const ( +// modeNormal mode = iota +// modeInsert +// modeAddTag +// modeAddProject +// ) + +// type TaskEditorPage struct { +// common *common.Common +// task taskwarrior.Task +// areaList tea.Model +// mode mode +// statusline tea.Model + +// // TODO: rework support for adding tags and projects +// additionalTags string +// additionalProject string + +// columnCursor int +// columns []tea.Model + +// areas map[area][]tea.Model +// selectedArea area +// } + +// 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.Uuid == "" { +// p.mode = modeInsert +// } else { +// p.mode = modeNormal +// } + +// areaItems := []list.Item{ +// item("Task"), +// item("Tags"), +// item("Time"), +// } + +// } + +// p.statusline = NewStatusLine(common, p.mode) + +// return p +// } + +type area int + +const ( + areaTask area = iota + areaTags + areaTime +) + +type areaPicker struct { + common *common.Common + list list.Model +} + +type item string + +func (i item) Title() string { return string(i) } +func (i item) Description() string { return "test" } +func (i item) FilterValue() string { return "" } + +func NewAreaPicker(common *common.Common, items []string) *areaPicker { + listItems := make([]list.Item, len(items)) + for i, itm := range items { + listItems[i] = item(itm) + } + + list := list.New(listItems, list.DefaultDelegate{}, 20, 50) + list.SetFilteringEnabled(false) + list.SetShowStatusBar(false) + + return &areaPicker{ + common: common, + list: list, + } +} + +func (a *areaPicker) Area() area { + switch a.list.SelectedItem() { + case item("Task"): + return areaTask + case item("Tags"): + return areaTags + case item("Dates"): + return areaTime + default: + return areaTask + } +} + +func (a *areaPicker) Init() tea.Cmd { + return nil +} + +func (a *areaPicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + cursor := a.list.Cursor() + // switch msg.(type) { + // case nextFieldMsg: + // a.list, cmd = a.list.Update(a.list.KeyMap.CursorDown) + // case prevFieldMsg: + // a.list, cmd = a.list.Update(a.list.KeyMap.CursorUp) + // } + a.list, cmd = a.list.Update(msg) + cmds = append(cmds, cmd) + if cursor != a.list.Cursor() { + cmds = append(cmds, changeArea(a.Area())) + } + + return a, tea.Batch(cmds...) +} + +func (a *areaPicker) View() string { + return a.list.View() +} + +type taskEdit struct { + common *common.Common + fields []huh.Field + cursor int + + newProjectName *string +} + +func NewTaskEdit(common *common.Common, description *string, priority *string, project *string, priorityOptions []string, projectOptions []string) *taskEdit { + newProject := "" + + defaultKeymap := huh.NewDefaultKeyMap() + + t := taskEdit{ + common: common, + fields: []huh.Field{ + huh.NewInput(). + Title("Task"). + Value(description). + Validate(func(desc string) error { + if desc == "" { + return fmt.Errorf("task description is required") + } + return nil + }). + Inline(true). + WithTheme(common.Styles.Form), + + huh.NewSelect[string](). + Options(huh.NewOptions(priorityOptions...)...). + Title("Priority"). + Key("priority"). + Value(priority). + WithKeyMap(defaultKeymap). + WithTheme(common.Styles.Form), + + huh.NewSelect[string](). + Options(huh.NewOptions(projectOptions...)...). + Title("Project"). + Value(project). + WithKeyMap(defaultKeymap). + WithTheme(common.Styles.Form), + + huh.NewInput(). + Title("New Project"). + Value(&newProject). + WithTheme(common.Styles.Form), + }, + + newProjectName: &newProject, + } + + t.fields[0].Focus() + + return &t +} + +func (t taskEdit) Init() tea.Cmd { + return nil +} + +func (t taskEdit) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg.(type) { + case nextFieldMsg: + if t.cursor == len(t.fields)-1 { + t.fields[t.cursor].Blur() + return t, nextArea() + } + t.fields[t.cursor].Blur() + t.cursor++ + t.fields[t.cursor].Focus() + case prevFieldMsg: + if t.cursor == 0 { + t.fields[t.cursor].Blur() + return t, prevArea() + } + t.fields[t.cursor].Blur() + t.cursor-- + t.fields[t.cursor].Focus() + default: + field, cmd := t.fields[t.cursor].Update(msg) + t.fields[t.cursor] = field.(huh.Field) + return t, cmd + } + + return t, nil +} + +func (t taskEdit) View() string { + views := make([]string, len(t.fields)) + for i, field := range t.fields { + views[i] = field.View() + } + return lipgloss.JoinVertical( + lipgloss.Left, + views..., + ) +} + +type tagEdit struct { + common *common.Common + fields []huh.Field + + cursor int + + newTagsValue *string +} + +func NewTagEdit(common *common.Common, selected *[]string, options []string) *tagEdit { + newTags := "" + + defaultKeymap := huh.NewDefaultKeyMap() + + t := tagEdit{ + common: common, + fields: []huh.Field{ + huh.NewMultiSelect[string](). + Options(huh.NewOptions(options...)...). + // Key("tags"). + Title("Tags"). + Value(selected). + Filterable(true). + WithKeyMap(defaultKeymap). + WithTheme(common.Styles.Form), + huh.NewInput(). + Title("New Tags"). + Value(&newTags). + Inline(true). + WithTheme(common.Styles.Form), + }, + newTagsValue: &newTags, + } + + t.fields[0].Focus() + + return &t +} + +func (t tagEdit) Init() tea.Cmd { + return nil +} + +func (t tagEdit) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg.(type) { + case nextFieldMsg: + if t.cursor == len(t.fields)-1 { + t.fields[t.cursor].Blur() + return t, nextArea() + } + t.fields[t.cursor].Blur() + t.cursor++ + t.fields[t.cursor].Focus() + case prevFieldMsg: + if t.cursor == 0 { + t.fields[t.cursor].Blur() + return t, prevArea() + } + t.fields[t.cursor].Blur() + t.cursor-- + t.fields[t.cursor].Focus() + default: + field, cmd := t.fields[t.cursor].Update(msg) + t.fields[t.cursor] = field.(huh.Field) + return t, cmd + } + return t, nil +} + +func (t tagEdit) View() string { + views := make([]string, len(t.fields)) + for i, field := range t.fields { + views[i] = field.View() + } + return lipgloss.JoinVertical( + lipgloss.Left, + views..., + ) +} + +type timeEdit struct { + common *common.Common + fields []huh.Field + + cursor int +} + +func NewTimeEdit(common *common.Common, due *string, scheduled *string, wait *string, until *string) *timeEdit { + // defaultKeymap := huh.NewDefaultKeyMap() + t := timeEdit{ + common: common, + fields: []huh.Field{ + huh.NewInput(). + Title("Due"). + Value(due). + Validate(taskwarrior.ValidateDate). + Inline(true). + WithTheme(common.Styles.Form), + huh.NewInput(). + Title("Scheduled"). + Value(scheduled). + Validate(taskwarrior.ValidateDate). + Inline(true). + WithTheme(common.Styles.Form), + huh.NewInput(). + Title("Wait"). + Value(wait). + Validate(taskwarrior.ValidateDate). + Inline(true). + WithTheme(common.Styles.Form), + huh.NewInput(). + Title("Until"). + Value(until). + Validate(taskwarrior.ValidateDate). + Inline(true). + WithTheme(common.Styles.Form), + }, + } + + t.fields[0].Focus() + + return &t +} + +func (t timeEdit) Init() tea.Cmd { + return nil +} + +func (t timeEdit) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg.(type) { + case nextFieldMsg: + if t.cursor == len(t.fields)-1 { + t.fields[t.cursor].Blur() + return t, nextArea() + } + t.fields[t.cursor].Blur() + t.cursor++ + t.fields[t.cursor].Focus() + case prevFieldMsg: + if t.cursor == 0 { + t.fields[t.cursor].Blur() + return t, prevArea() + } + t.fields[t.cursor].Blur() + t.cursor-- + t.fields[t.cursor].Focus() + default: + field, cmd := t.fields[t.cursor].Update(msg) + t.fields[t.cursor] = field.(huh.Field) + return t, cmd + } + return t, nil +} + +func (t timeEdit) View() string { + views := make([]string, len(t.fields)) + for i, field := range t.fields { + views[i] = field.View() + } + return lipgloss.JoinVertical( + lipgloss.Left, + views..., + ) +} + +// func (p *TaskEditorPage) SetSize(width, height int) { +// p.common.SetSize(width, height) +// } + +// func (p *TaskEditorPage) Init() tea.Cmd { +// // return p.form.Init() +// return nil +// } + +// func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +// var cmds []tea.Cmd + +// switch msg := msg.(type) { +// case SwitchModeMsg: +// switch mode(msg) { +// case modeNormal: +// p.mode = modeNormal +// case modeInsert: +// p.mode = modeInsert +// } +// case changeAreaMsg: +// p.selectedArea = area(msg) +// p.columns = append([]tea.Model{p.areaList}, p.areas[p.selectedArea]...) +// case nextColumnMsg: +// p.columnCursor++ +// if p.columnCursor > len(p.columns)-1 { +// p.columnCursor = 0 +// } +// case prevColumnMsg: +// p.columnCursor-- +// if p.columnCursor < 0 { +// p.columnCursor = len(p.columns) - 1 +// } +// } + +// switch p.mode { +// case modeNormal: +// switch msg := msg.(type) { +// case tea.KeyMsg: +// switch { +// case key.Matches(msg, p.common.Keymap.Back): +// model, err := p.common.PopPage() +// if err != nil { +// slog.Error("page stack empty") +// return nil, tea.Quit +// } +// return model, BackCmd +// case key.Matches(msg, p.common.Keymap.Insert): +// return p, p.switchModeCmd(modeInsert) +// // case key.Matches(msg, p.common.Keymap.Ok): +// // p.form.State = huh.StateCompleted +// case key.Matches(msg, p.common.Keymap.Left): +// return p, prevColumn() +// case key.Matches(msg, p.common.Keymap.Right): +// return p, nextColumn() +// } +// } +// case modeInsert: +// switch msg := msg.(type) { +// case tea.KeyMsg: +// switch { +// case key.Matches(msg, p.common.Keymap.Back): +// return p, p.switchModeCmd(modeNormal) +// } +// } + +// var cmd tea.Cmd +// if p.columnCursor == 0 { +// p.areaList, cmd = p.areaList.Update(msg) +// p.selectedArea = p.areaList.(areaList).Area() +// cmds = append(cmds, cmd) +// } else { +// p.areas[p.selectedArea][p.columnCursor-1], cmd = p.areas[p.selectedArea][p.columnCursor-1].Update(msg) +// cmds = append(cmds, cmd) +// } + +// } + +// var cmd tea.Cmd +// if p.columnCursor == 0 { +// p.areaList, cmd = p.areaList.Update(msg) +// p.selectedArea = p.areaList.(areaList).Area() +// cmds = append(cmds, cmd) +// } else { +// p.areas[p.selectedArea][p.columnCursor-1], cmd = p.areas[p.selectedArea][p.columnCursor-1].Update(msg) +// cmds = append(cmds, cmd) +// } + +// p.statusline, cmd = p.statusline.Update(msg) +// cmds = append(cmds, cmd) + +// // if p.form.State == huh.StateCompleted { +// // cmds = append(cmds, p.updateTasksCmd) +// // model, err := p.common.PopPage() +// // 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 { +// columns := make([]string, len(p.columns)) +// for i, c := range p.columns { +// columns[i] = c.View() +// } + +// return lipgloss.JoinVertical( +// lipgloss.Left, +// lipgloss.JoinHorizontal( +// lipgloss.Top, +// // lipgloss.Place(p.common.Width(), p.common.Height()-1, 0.5, 0.5, p.form.View(), lipgloss.WithWhitespaceBackground(p.common.Styles.Warning.GetForeground())), +// // lipgloss.Place(p.common.Width(), p.common.Height()-1, 0.5, 0.5, p.form.View(), lipgloss.WithWhitespaceChars(" . ")), +// columns..., +// ), +// p.statusline.View(), +// ) +// } + func (p *TaskEditorPage) updateTasksCmd() tea.Msg { if p.task.Project == "(none)" { p.task.Project = "" @@ -240,175 +788,112 @@ func (p *TaskEditorPage) updateTasksCmd() tea.Msg { if p.task.Priority == "(none)" { p.task.Priority = "" } - if p.additionalTags != "" { - p.task.Tags = append(p.task.Tags, strings.Split(p.additionalTags, " ")...) - } - if p.additionalProject != "" { - p.task.Project = p.additionalProject - } + // if p.additionalTags != "" { + // p.task.Tags = append(p.task.Tags, strings.Split(p.additionalTags, " ")...) + // } + // if p.additionalProject != "" { + // p.task.Project = p.additionalProject + // } // tags := p.form.Get("tags").([]string) // p.task.Tags = tags p.common.TW.ImportTask(&p.task) return UpdatedTasksMsg{} } -func (p *TaskEditorPage) switchModeCmd(mode Mode) tea.Cmd { - return func() tea.Msg { - return SwitchModeMsg(mode) - } -} +// type StatusLine struct { +// common *common.Common +// mode mode +// input textinput.Model +// } -type UpdatedTasksMsg struct{} -type SwitchModeMsg Mode +// func NewStatusLine(common *common.Common, mode mode) *StatusLine { +// input := textinput.New() +// input.Placeholder = "" +// input.Prompt = "" +// input.Blur() -type StatusLine struct { - common *common.Common - mode Mode - input textinput.Model -} +// return &StatusLine{ +// input: textinput.New(), +// common: common, +// mode: mode, +// } +// } -func NewStatusLine(common *common.Common, mode Mode) *StatusLine { - input := textinput.New() - input.Placeholder = "" - input.Prompt = "" - input.Blur() +// func (s *StatusLine) Init() tea.Cmd { +// s.input.Blur() +// return nil +// } - return &StatusLine{ - input: textinput.New(), - common: common, - mode: mode, - } -} +// func (s *StatusLine) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +// var cmd tea.Cmd -func (s *StatusLine) Init() tea.Cmd { - s.input.Blur() - return nil -} +// switch msg := msg.(type) { +// case SwitchModeMsg: +// s.mode = mode(msg) +// switch s.mode { +// case modeNormal: +// s.input.Blur() +// case modeInsert: +// s.input.Focus() +// } +// case tea.KeyMsg: +// switch { +// case key.Matches(msg, s.common.Keymap.Back): +// s.input.Blur() +// case key.Matches(msg, s.common.Keymap.Input): +// s.input.Focus() +// } +// } -func (s *StatusLine) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd +// s.input, cmd = s.input.Update(msg) +// return s, cmd +// } - switch msg := msg.(type) { - case SwitchModeMsg: - s.mode = Mode(msg) - switch s.mode { - case ModeNormal: - s.input.Blur() - case ModeInsert: - s.input.Focus() - } - case tea.KeyMsg: - switch { - case key.Matches(msg, s.common.Keymap.Back): - s.input.Blur() - case key.Matches(msg, s.common.Keymap.Input): - s.input.Focus() - } - } +// func (s *StatusLine) View() string { +// var mode string +// switch s.mode { +// case modeNormal: +// mode = s.common.Styles.Base.Render("NORMAL") +// case modeInsert: +// mode = s.common.Styles.Active.Inline(true).Reverse(true).Render("INSERT") +// } +// return lipgloss.JoinHorizontal(lipgloss.Left, mode, s.input.View()) +// } - s.input, cmd = s.input.Update(msg) - return s, cmd -} +// // TODO: move this to taskwarrior; add missing date formats -func (s *StatusLine) View() string { - var mode string - switch s.mode { - case ModeNormal: - mode = s.common.Styles.Base.Render("NORMAL") - case ModeInsert: - mode = s.common.Styles.Active.Inline(true).Render("INSERT") - } - return lipgloss.JoinHorizontal(lipgloss.Left, mode, s.input.View()) -} +// type itemDelegate 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", - } +// func (d itemDelegate) Height() int { return 1 } +// func (d itemDelegate) Spacing() int { return 0 } +// func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +// func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { +// i, ok := listItem.(item) +// if !ok { +// return +// } - 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", - } +// str := fmt.Sprintf("%s", i) - for _, f := range formats { - if _, err := time.Parse(f, s); err == nil { - return nil - } - } +// fn := itemStyle.Render +// if index == m.Index() { +// fn = func(s ...string) string { +// return selectedItemStyle.Render("> " + strings.Join(s, " ")) +// } +// } - for _, f := range otherFormats { - if s == f { - return nil - } - } +// fmt.Fprint(w, fn(str)) +// } - return fmt.Errorf("invalid date") -} +// var ( +// titleStyle = lipgloss.NewStyle().MarginLeft(2) +// itemStyle = lipgloss.NewStyle().PaddingLeft(4) +// selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) +// paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) +// helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) +// quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) +// ) + +// type item string + +// func (i item) FilterValue() string { return "" } diff --git a/taskwarrior/config.go b/taskwarrior/config.go index ed92678..1d134c5 100644 --- a/taskwarrior/config.go +++ b/taskwarrior/config.go @@ -13,6 +13,7 @@ type TWConfig struct { var ( defaultConfig = map[string]string{ "uda.tasksquire.report.default": "next", + "uda.tasksquire.tag.default": "next", } ) diff --git a/taskwarrior/models.go b/taskwarrior/models.go index 7ef4a0f..d61b65a 100644 --- a/taskwarrior/models.go +++ b/taskwarrior/models.go @@ -197,6 +197,30 @@ func (t *Task) GetDate(dateString string) time.Time { return dt } +func (t *Task) HasTag(tag string) bool { + for _, t := range t.Tags { + if t == tag { + return true + } + } + return false +} + +func (t *Task) AddTag(tag string) { + if !t.HasTag(tag) { + t.Tags = append(t.Tags, tag) + } +} + +func (t *Task) RemoveTag(tag string) { + for i, ttag := range t.Tags { + if ttag == tag { + t.Tags = append(t.Tags[:i], t.Tags[i+1:]...) + return + } + } +} + type Tasks []*Task type Context struct { @@ -291,3 +315,97 @@ func parseCountdown(d time.Duration) string { return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds) } + +var ( + dateFormats = []string{ + "2006-01-02", + "2006-01-02T15:04", + "20060102T150405Z", + } + + specialDateFormats = []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", + } +) + +func ValidateDate(s string) error { + + for _, f := range dateFormats { + if _, err := time.Parse(f, s); err == nil { + return nil + } + } + + for _, f := range specialDateFormats { + if s == f { + return nil + } + } + + return fmt.Errorf("invalid date") +} diff --git a/taskwarrior/taskwarrior.go b/taskwarrior/taskwarrior.go index 539d857..71d0256 100644 --- a/taskwarrior/taskwarrior.go +++ b/taskwarrior/taskwarrior.go @@ -93,6 +93,8 @@ type TaskWarrior interface { AddTask(task *Task) error ImportTask(task *Task) SetTaskDone(task *Task) + StartTask(task *Task) + StopTask(task *Task) Undo() } @@ -403,6 +405,28 @@ func (ts *TaskSquire) Undo() { } } +func (ts *TaskSquire) StartTask(task *Task) { + ts.mutex.Lock() + defer ts.mutex.Unlock() + + cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"start", task.Uuid}...)...) + err := cmd.Run() + if err != nil { + slog.Error("Failed starting task:", err) + } +} + +func (ts *TaskSquire) StopTask(task *Task) { + ts.mutex.Lock() + defer ts.mutex.Unlock() + + cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"stop", task.Uuid}...)...) + err := cmd.Run() + if err != nil { + slog.Error("Failed stopping task:", err) + } +} + func (ts *TaskSquire) extractConfig() *TWConfig { cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...) output, err := cmd.CombinedOutput()