1 Commits

Author SHA1 Message Date
70b6ee9bc7 Add picker to task edit 2026-02-02 20:43:08 +01:00
3 changed files with 139 additions and 28 deletions

View File

@ -637,6 +637,11 @@ func (m *MultiSelect) GetValue() any {
return *m.value
}
// IsFiltering returns true if the multi-select is currently filtering.
func (m *MultiSelect) IsFiltering() bool {
return m.filtering
}
func min(a, b int) int {
if a < b {
return a

View File

@ -37,6 +37,7 @@ type Picker struct {
title string
filterByDefault bool
baseItems []list.Item
focused bool
}
type PickerOption func(*Picker)
@ -53,6 +54,24 @@ func WithOnCreate(onCreate func(string) tea.Cmd) PickerOption {
}
}
func (p *Picker) Focus() tea.Cmd {
p.focused = true
return nil
}
func (p *Picker) Blur() tea.Cmd {
p.focused = false
return nil
}
func (p *Picker) GetValue() string {
item := p.list.SelectedItem()
if item == nil {
return ""
}
return item.FilterValue()
}
func New(
c *common.Common,
title string,
@ -82,6 +101,7 @@ func New(
itemProvider: itemProvider,
onSelect: onSelect,
title: title,
focused: true,
}
if c.TW.GetConfig().Get("uda.tasksquire.picker.filter_by_default") == "yes" {
@ -92,6 +112,14 @@ func New(
opt(p)
}
if p.filterByDefault {
// Manually trigger filter mode on the list so it doesn't require a global key press
var cmd tea.Cmd
p.list, cmd = p.list.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'i'}})
// We can ignore the command here as it's likely just for blinking, which will happen on Init anyway
_ = cmd
}
p.Refresh()
return p
@ -134,27 +162,26 @@ func (p *Picker) SetSize(width, height int) {
}
func (p *Picker) Init() tea.Cmd {
if p.filterByDefault {
return func() tea.Msg {
return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'i'}}
}
}
return nil
}
func (p *Picker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if !p.focused {
return p, nil
}
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
// If filtering, let the list handle keys (including Enter to stop filtering)
if p.list.FilterState() == list.Filtering {
if key.Matches(msg, p.common.Keymap.Ok) {
items := p.list.VisibleItems()
if len(items) == 1 {
return p, p.handleSelect(items[0])
}
}
// if key.Matches(msg, p.common.Keymap.Ok) {
// items := p.list.VisibleItems()
// if len(items) == 1 {
// return p, p.handleSelect(items[0])
// }
// }
break // Pass to list.Update
}
@ -189,7 +216,12 @@ func (p *Picker) handleSelect(item list.Item) tea.Cmd {
}
func (p *Picker) View() string {
title := p.common.Styles.Form.Focused.Title.Render(p.title)
var title string
if p.focused {
title = p.common.Styles.Form.Focused.Title.Render(p.title)
} else {
title = p.common.Styles.Form.Blurred.Title.Render(p.title)
}
return lipgloss.JoinVertical(lipgloss.Left, title, p.list.View())
}

View File

@ -8,6 +8,7 @@ import (
"time"
"tasksquire/components/input"
"tasksquire/components/picker"
"tasksquire/components/timestampeditor"
"tasksquire/taskwarrior"
@ -58,7 +59,7 @@ func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPag
tagOptions := p.common.TW.GetTags()
p.areas = []area{
NewTaskEdit(p.common, &p.task),
NewTaskEdit(p.common, &p.task, p.task.Uuid == ""),
NewTagEdit(p.common, &p.task.Tags, tagOptions),
NewTimeEdit(p.common, &p.task.Due, &p.task.Scheduled, &p.task.Wait, &p.task.Until),
NewDetailsEdit(p.common, &p.task),
@ -110,7 +111,11 @@ func (p *TaskEditorPage) SetSize(width, height int) {
}
func (p *TaskEditorPage) Init() tea.Cmd {
return nil
var cmds []tea.Cmd
for _, a := range p.areas {
cmds = append(cmds, a.Init())
}
return tea.Batch(cmds...)
}
func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@ -257,9 +262,13 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return p, nil
case key.Matches(msg, p.common.Keymap.Ok):
isFiltering := p.areas[p.area].IsFiltering()
model, cmd := p.areas[p.area].Update(msg)
if p.area != 3 {
p.areas[p.area] = model.(area)
if isFiltering {
return p, cmd
}
return p, tea.Batch(cmd, nextField())
}
return p, cmd
@ -340,8 +349,11 @@ type area interface {
tea.Model
SetCursor(c int)
GetName() string
IsFiltering() bool
}
type focusMsg struct{}
type areaPicker struct {
common *common.Common
list list.Model
@ -413,26 +425,54 @@ func (a *areaPicker) View() string {
return a.list.View()
}
type EditableField interface {
tea.Model
Focus() tea.Cmd
Blur() tea.Cmd
}
type taskEdit struct {
common *common.Common
fields []huh.Field
fields []EditableField
cursor int
projectPicker *picker.Picker
// newProjectName *string
newAnnotation *string
udaValues map[string]*string
}
func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
func NewTaskEdit(com *common.Common, task *taskwarrior.Task, isNew bool) *taskEdit {
// newProject := ""
projectOptions := append([]string{"(none)"}, com.TW.GetProjects()...)
if task.Project == "" {
task.Project = "(none)"
}
itemProvider := func() []list.Item {
projects := com.TW.GetProjects()
items := []list.Item{picker.NewItem("(none)")}
for _, proj := range projects {
items = append(items, picker.NewItem(proj))
}
return items
}
onSelect := func(item list.Item) tea.Cmd {
return nil
}
opts := []picker.PickerOption{}
if isNew {
opts = append(opts, picker.WithFilterByDefault(true))
}
projPicker := picker.New(com, "Project", itemProvider, onSelect, opts...)
projPicker.SetSize(70, 8)
projPicker.SelectItemByFilterValue(task.Project)
projPicker.Blur()
defaultKeymap := huh.NewDefaultKeyMap()
fields := []huh.Field{
fields := []EditableField{
huh.NewInput().
Title("Task").
Value(&task.Description).
@ -446,12 +486,7 @@ func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
Prompt(": ").
WithTheme(com.Styles.Form),
input.NewSelect(com).
Options(true, input.NewOptions(projectOptions...)...).
Title("Project").
Value(&task.Project).
WithKeyMap(defaultKeymap).
WithTheme(com.Styles.Form),
projPicker,
// huh.NewInput().
// Title("New Project").
@ -546,6 +581,7 @@ func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
t := taskEdit{
common: com,
fields: fields,
projectPicker: projPicker,
udaValues: udaValues,
@ -562,6 +598,13 @@ func (t *taskEdit) GetName() string {
return "Task"
}
func (t *taskEdit) IsFiltering() bool {
if f, ok := t.fields[t.cursor].(interface{ IsFiltering() bool }); ok {
return f.IsFiltering()
}
return false
}
func (t *taskEdit) SetCursor(c int) {
t.fields[t.cursor].Blur()
if c < 0 {
@ -573,11 +616,25 @@ func (t *taskEdit) SetCursor(c int) {
}
func (t *taskEdit) Init() tea.Cmd {
return nil
var cmds []tea.Cmd
// Ensure focus on the active field (especially for the first one)
if len(t.fields) > 0 {
cmds = append(cmds, func() tea.Msg {
return focusMsg{}
})
}
for _, f := range t.fields {
cmds = append(cmds, f.Init())
}
return tea.Batch(cmds...)
}
func (t *taskEdit) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case focusMsg:
if len(t.fields) > 0 {
return t, t.fields[t.cursor].Focus()
}
case nextFieldMsg:
if t.cursor == len(t.fields)-1 {
t.fields[t.cursor].Blur()
@ -596,7 +653,7 @@ func (t *taskEdit) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
t.fields[t.cursor].Focus()
default:
field, cmd := t.fields[t.cursor].Update(msg)
t.fields[t.cursor] = field.(huh.Field)
t.fields[t.cursor] = field.(EditableField)
return t, cmd
}
@ -659,6 +716,13 @@ func (t *tagEdit) GetName() string {
return "Tags"
}
func (t *tagEdit) IsFiltering() bool {
if f, ok := t.fields[t.cursor].(interface{ IsFiltering() bool }); ok {
return f.IsFiltering()
}
return false
}
func (t *tagEdit) SetCursor(c int) {
t.fields[t.cursor].Blur()
if c < 0 {
@ -771,6 +835,10 @@ func (t *timeEdit) GetName() string {
return "Dates"
}
func (t *timeEdit) IsFiltering() bool {
return false
}
func (t *timeEdit) SetCursor(c int) {
if len(t.fields) == 0 {
return
@ -903,6 +971,10 @@ func (d *detailsEdit) GetName() string {
return "Details"
}
func (d *detailsEdit) IsFiltering() bool {
return false
}
func (d *detailsEdit) SetCursor(c int) {
}
@ -1070,6 +1142,8 @@ func (d *detailsEdit) View() string {
// }
func (p *TaskEditorPage) updateTasksCmd() tea.Msg {
p.task.Project = p.areas[0].(*taskEdit).projectPicker.GetValue()
if p.task.Project == "(none)" {
p.task.Project = ""
}