Compare commits
4 Commits
feat/task
...
2e33893e29
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e33893e29 | |||
| 46ce91196a | |||
| 2b31d9bc2b | |||
| 70b6ee9bc7 |
@ -83,6 +83,12 @@ func (a *Autocomplete) SetMinChars(min int) {
|
||||
a.minChars = min
|
||||
}
|
||||
|
||||
// SetSuggestions updates the available suggestions
|
||||
func (a *Autocomplete) SetSuggestions(suggestions []string) {
|
||||
a.allSuggestions = suggestions
|
||||
a.updateFilteredSuggestions()
|
||||
}
|
||||
|
||||
// Init initializes the autocomplete
|
||||
func (a *Autocomplete) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
|
||||
@ -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 = ""
|
||||
}
|
||||
|
||||
@ -5,10 +5,12 @@ import (
|
||||
"strings"
|
||||
"tasksquire/common"
|
||||
"tasksquire/components/autocomplete"
|
||||
"tasksquire/components/picker"
|
||||
"tasksquire/components/timestampeditor"
|
||||
"tasksquire/timewarrior"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
@ -18,17 +20,65 @@ type TimeEditorPage struct {
|
||||
interval *timewarrior.Interval
|
||||
|
||||
// Fields
|
||||
projectPicker *picker.Picker
|
||||
startEditor *timestampeditor.TimestampEditor
|
||||
endEditor *timestampeditor.TimestampEditor
|
||||
tagsInput *autocomplete.Autocomplete
|
||||
adjust bool
|
||||
|
||||
// State
|
||||
selectedProject string
|
||||
currentField int
|
||||
totalFields int
|
||||
}
|
||||
|
||||
type timeEditorProjectSelectedMsg struct {
|
||||
project string
|
||||
}
|
||||
|
||||
func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *TimeEditorPage {
|
||||
// Extract project from tags if it exists
|
||||
projects := com.TW.GetProjects()
|
||||
selectedProject, remainingTags := extractProjectFromTags(interval.Tags, projects)
|
||||
if selectedProject == "" && len(projects) > 0 {
|
||||
selectedProject = projects[0] // Default to first project (required)
|
||||
}
|
||||
|
||||
// Create project picker with onCreate support for new projects
|
||||
projectItemProvider := func() []list.Item {
|
||||
projects := com.TW.GetProjects()
|
||||
items := make([]list.Item, len(projects))
|
||||
for i, proj := range projects {
|
||||
items[i] = picker.NewItem(proj)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
projectOnSelect := func(item list.Item) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return timeEditorProjectSelectedMsg{project: item.FilterValue()}
|
||||
}
|
||||
}
|
||||
|
||||
projectOnCreate := func(name string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return timeEditorProjectSelectedMsg{project: name}
|
||||
}
|
||||
}
|
||||
|
||||
projectPicker := picker.New(
|
||||
com,
|
||||
"Project",
|
||||
projectItemProvider,
|
||||
projectOnSelect,
|
||||
picker.WithOnCreate(projectOnCreate),
|
||||
picker.WithFilterByDefault(true),
|
||||
)
|
||||
projectPicker.SetSize(50, 10) // Compact size for inline use
|
||||
if selectedProject != "" {
|
||||
projectPicker.SelectItemByFilterValue(selectedProject)
|
||||
}
|
||||
|
||||
// Create start timestamp editor
|
||||
startEditor := timestampeditor.New(com).
|
||||
Title("Start").
|
||||
@ -39,38 +89,50 @@ func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *Time
|
||||
Title("End").
|
||||
ValueFromString(interval.End)
|
||||
|
||||
// Create tags autocomplete with combinations from past intervals
|
||||
tagCombinations := com.TimeW.GetTagCombinations()
|
||||
// Get tag combinations filtered by selected project
|
||||
tagCombinations := filterTagCombinationsByProject(
|
||||
com.TimeW.GetTagCombinations(),
|
||||
selectedProject,
|
||||
)
|
||||
|
||||
tagsInput := autocomplete.New(tagCombinations, 3, 10)
|
||||
tagsInput.SetPlaceholder("Space separated, use \"\" for tags with spaces")
|
||||
tagsInput.SetValue(formatTags(interval.Tags))
|
||||
tagsInput.SetValue(formatTags(remainingTags)) // Use remaining tags (without project)
|
||||
tagsInput.SetWidth(50)
|
||||
|
||||
p := &TimeEditorPage{
|
||||
common: com,
|
||||
interval: interval,
|
||||
projectPicker: projectPicker,
|
||||
startEditor: startEditor,
|
||||
endEditor: endEditor,
|
||||
tagsInput: tagsInput,
|
||||
adjust: true, // Enable :adjust by default
|
||||
selectedProject: selectedProject,
|
||||
currentField: 0,
|
||||
totalFields: 4, // Updated to include adjust field
|
||||
totalFields: 5, // Now 5 fields: project, tags, start, end, adjust
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *TimeEditorPage) Init() tea.Cmd {
|
||||
// Focus the first field (tags)
|
||||
// Focus the first field (project picker)
|
||||
p.currentField = 0
|
||||
p.tagsInput.Focus()
|
||||
return p.tagsInput.Init()
|
||||
return p.projectPicker.Init()
|
||||
}
|
||||
|
||||
func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case timeEditorProjectSelectedMsg:
|
||||
// Update selected project
|
||||
p.selectedProject = msg.project
|
||||
// Refresh tag autocomplete with filtered combinations
|
||||
cmds = append(cmds, p.updateTagSuggestions())
|
||||
return p, tea.Batch(cmds...)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, p.common.Keymap.Back):
|
||||
@ -82,6 +144,8 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return model, BackCmd
|
||||
|
||||
case key.Matches(msg, p.common.Keymap.Ok):
|
||||
// Don't save if the project picker is focused - let it handle Enter
|
||||
if p.currentField != 0 {
|
||||
// Save and exit
|
||||
p.saveInterval()
|
||||
model, err := p.common.PopPage()
|
||||
@ -90,6 +154,8 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return nil, tea.Quit
|
||||
}
|
||||
return model, tea.Batch(tea.Batch(cmds...), refreshIntervals)
|
||||
}
|
||||
// If picker is focused, let it handle the key below
|
||||
|
||||
case key.Matches(msg, p.common.Keymap.Next):
|
||||
// Move to next field
|
||||
@ -111,25 +177,31 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Update the currently focused field
|
||||
var cmd tea.Cmd
|
||||
switch p.currentField {
|
||||
case 0:
|
||||
case 0: // Project picker
|
||||
var model tea.Model
|
||||
model, cmd = p.projectPicker.Update(msg)
|
||||
if pk, ok := model.(*picker.Picker); ok {
|
||||
p.projectPicker = pk
|
||||
}
|
||||
case 1: // Tags (was 0)
|
||||
var model tea.Model
|
||||
model, cmd = p.tagsInput.Update(msg)
|
||||
if ac, ok := model.(*autocomplete.Autocomplete); ok {
|
||||
p.tagsInput = ac
|
||||
}
|
||||
case 1:
|
||||
case 2: // Start (was 1)
|
||||
var model tea.Model
|
||||
model, cmd = p.startEditor.Update(msg)
|
||||
if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
|
||||
p.startEditor = editor
|
||||
}
|
||||
case 2:
|
||||
case 3: // End (was 2)
|
||||
var model tea.Model
|
||||
model, cmd = p.endEditor.Update(msg)
|
||||
if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
|
||||
p.endEditor = editor
|
||||
}
|
||||
case 3:
|
||||
case 4: // Adjust (was 3)
|
||||
// Handle adjust toggle with space/enter
|
||||
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||
if msg.String() == " " || msg.String() == "enter" {
|
||||
@ -145,13 +217,15 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (p *TimeEditorPage) focusCurrentField() tea.Cmd {
|
||||
switch p.currentField {
|
||||
case 0:
|
||||
return p.projectPicker.Init() // Picker doesn't have explicit Focus()
|
||||
case 1:
|
||||
p.tagsInput.Focus()
|
||||
return p.tagsInput.Init()
|
||||
case 1:
|
||||
return p.startEditor.Focus()
|
||||
case 2:
|
||||
return p.endEditor.Focus()
|
||||
return p.startEditor.Focus()
|
||||
case 3:
|
||||
return p.endEditor.Focus()
|
||||
case 4:
|
||||
// Adjust checkbox doesn't need focus action
|
||||
return nil
|
||||
}
|
||||
@ -161,12 +235,14 @@ func (p *TimeEditorPage) focusCurrentField() tea.Cmd {
|
||||
func (p *TimeEditorPage) blurCurrentField() {
|
||||
switch p.currentField {
|
||||
case 0:
|
||||
p.tagsInput.Blur()
|
||||
// Picker doesn't have explicit Blur(), state handled by Update
|
||||
case 1:
|
||||
p.startEditor.Blur()
|
||||
p.tagsInput.Blur()
|
||||
case 2:
|
||||
p.endEditor.Blur()
|
||||
p.startEditor.Blur()
|
||||
case 3:
|
||||
p.endEditor.Blur()
|
||||
case 4:
|
||||
// Adjust checkbox doesn't need blur action
|
||||
}
|
||||
}
|
||||
@ -179,10 +255,22 @@ func (p *TimeEditorPage) View() string {
|
||||
sections = append(sections, titleStyle.Render("Edit Time Interval"))
|
||||
sections = append(sections, "")
|
||||
|
||||
// Tags input (now first)
|
||||
// Project picker (field 0)
|
||||
if p.currentField == 0 {
|
||||
sections = append(sections, p.projectPicker.View())
|
||||
} else {
|
||||
blurredLabelStyle := p.common.Styles.Form.Blurred.Title
|
||||
sections = append(sections, blurredLabelStyle.Render("Project"))
|
||||
sections = append(sections, lipgloss.NewStyle().Faint(true).Render(p.selectedProject))
|
||||
}
|
||||
|
||||
sections = append(sections, "")
|
||||
sections = append(sections, "")
|
||||
|
||||
// Tags input (now field 1, was first)
|
||||
tagsLabelStyle := p.common.Styles.Form.Focused.Title
|
||||
tagsLabel := tagsLabelStyle.Render("Tags")
|
||||
if p.currentField == 0 {
|
||||
if p.currentField == 1 { // Changed from 0
|
||||
sections = append(sections, tagsLabel)
|
||||
sections = append(sections, p.tagsInput.View())
|
||||
descStyle := p.common.Styles.Form.Focused.Description
|
||||
@ -204,7 +292,7 @@ func (p *TimeEditorPage) View() string {
|
||||
sections = append(sections, p.endEditor.View())
|
||||
sections = append(sections, "")
|
||||
|
||||
// Adjust checkbox
|
||||
// Adjust checkbox (now field 4, was 3)
|
||||
adjustLabelStyle := p.common.Styles.Form.Focused.Title
|
||||
adjustLabel := adjustLabelStyle.Render("Adjust overlaps")
|
||||
|
||||
@ -215,7 +303,7 @@ func (p *TimeEditorPage) View() string {
|
||||
checkbox = "[ ]"
|
||||
}
|
||||
|
||||
if p.currentField == 3 {
|
||||
if p.currentField == 4 { // Changed from 3
|
||||
focusedStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
sections = append(sections, adjustLabel)
|
||||
sections = append(sections, focusedStyle.Render(checkbox+" Auto-adjust overlapping intervals"))
|
||||
@ -254,8 +342,24 @@ func (p *TimeEditorPage) saveInterval() {
|
||||
p.interval.Start = p.startEditor.GetValueString()
|
||||
p.interval.End = p.endEditor.GetValueString()
|
||||
|
||||
// Parse tags
|
||||
p.interval.Tags = parseTags(p.tagsInput.GetValue())
|
||||
// Parse tags from input
|
||||
tags := parseTags(p.tagsInput.GetValue())
|
||||
|
||||
// Add project to tags if not already present
|
||||
if p.selectedProject != "" {
|
||||
projectExists := false
|
||||
for _, tag := range tags {
|
||||
if tag == p.selectedProject {
|
||||
projectExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !projectExists {
|
||||
tags = append([]string{p.selectedProject}, tags...) // Prepend project
|
||||
}
|
||||
}
|
||||
|
||||
p.interval.Tags = tags
|
||||
|
||||
err := p.common.TimeW.ModifyInterval(p.interval, p.adjust)
|
||||
if err != nil {
|
||||
@ -298,3 +402,79 @@ func formatTags(tags []string) string {
|
||||
}
|
||||
return strings.Join(formatted, " ")
|
||||
}
|
||||
|
||||
// extractProjectFromTags finds and removes the first tag that matches a known project
|
||||
// Returns the found project (or empty string) and the remaining tags
|
||||
func extractProjectFromTags(tags []string, projects []string) (string, []string) {
|
||||
projectSet := make(map[string]bool)
|
||||
for _, p := range projects {
|
||||
projectSet[p] = true
|
||||
}
|
||||
|
||||
var foundProject string
|
||||
var remaining []string
|
||||
|
||||
for _, tag := range tags {
|
||||
if foundProject == "" && projectSet[tag] {
|
||||
foundProject = tag // First matching project
|
||||
} else {
|
||||
remaining = append(remaining, tag)
|
||||
}
|
||||
}
|
||||
|
||||
return foundProject, remaining
|
||||
}
|
||||
|
||||
// filterTagCombinationsByProject filters tag combinations to only show those
|
||||
// containing the exact project tag, and removes the project from the displayed combination
|
||||
func filterTagCombinationsByProject(combinations []string, project string) []string {
|
||||
if project == "" {
|
||||
return combinations
|
||||
}
|
||||
|
||||
var filtered []string
|
||||
for _, combo := range combinations {
|
||||
// Parse the combination into individual tags
|
||||
tags := parseTags(combo)
|
||||
|
||||
// Check if project exists in this combination
|
||||
for _, tag := range tags {
|
||||
if tag == project {
|
||||
// Found the project - now remove it from display
|
||||
var displayTags []string
|
||||
for _, t := range tags {
|
||||
if t != project {
|
||||
displayTags = append(displayTags, t)
|
||||
}
|
||||
}
|
||||
if len(displayTags) > 0 {
|
||||
filtered = append(filtered, formatTags(displayTags))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// updateTagSuggestions refreshes the tag autocomplete with filtered combinations
|
||||
func (p *TimeEditorPage) updateTagSuggestions() tea.Cmd {
|
||||
combinations := filterTagCombinationsByProject(
|
||||
p.common.TimeW.GetTagCombinations(),
|
||||
p.selectedProject,
|
||||
)
|
||||
|
||||
// Update autocomplete suggestions
|
||||
currentValue := p.tagsInput.GetValue()
|
||||
p.tagsInput.SetSuggestions(combinations)
|
||||
p.tagsInput.SetValue(currentValue)
|
||||
|
||||
// If tags field is focused, refocus it
|
||||
if p.currentField == 1 {
|
||||
p.tagsInput.Focus()
|
||||
return p.tagsInput.Init()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user