2 Commits

Author SHA1 Message Date
2baf3859fd Add tab bar 2026-02-02 19:47:18 +01:00
2940711b26 Make task details scrollable 2026-02-02 19:39:02 +01:00
5 changed files with 161 additions and 252 deletions

View File

@ -27,6 +27,10 @@ type Styles struct {
Form *huh.Theme Form *huh.Theme
TableStyle TableStyle TableStyle TableStyle
Tab lipgloss.Style
ActiveTab lipgloss.Style
TabBar lipgloss.Style
ColumnFocused lipgloss.Style ColumnFocused lipgloss.Style
ColumnBlurred lipgloss.Style ColumnBlurred lipgloss.Style
ColumnInsert lipgloss.Style ColumnInsert lipgloss.Style
@ -71,6 +75,19 @@ func NewStyles(config *taskwarrior.TWConfig) *Styles {
styles.Form = formTheme styles.Form = formTheme
styles.Tab = lipgloss.NewStyle().
Padding(0, 1).
Foreground(lipgloss.Color("240"))
styles.ActiveTab = styles.Tab.
Foreground(lipgloss.Color("252")).
Bold(true)
styles.TabBar = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, false, true, false).
BorderForeground(lipgloss.Color("240")).
MarginBottom(1)
styles.ColumnFocused = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1) styles.ColumnFocused = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1)
styles.ColumnBlurred = lipgloss.NewStyle().Border(lipgloss.HiddenBorder(), true).Padding(1) styles.ColumnBlurred = lipgloss.NewStyle().Border(lipgloss.HiddenBorder(), true).Padding(1)
styles.ColumnInsert = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1) styles.ColumnInsert = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1)

View File

@ -83,12 +83,6 @@ func (a *Autocomplete) SetMinChars(min int) {
a.minChars = min a.minChars = min
} }
// SetSuggestions updates the available suggestions
func (a *Autocomplete) SetSuggestions(suggestions []string) {
a.allSuggestions = suggestions
a.updateFilteredSuggestions()
}
// Init initializes the autocomplete // Init initializes the autocomplete
func (a *Autocomplete) Init() tea.Cmd { func (a *Autocomplete) Init() tea.Cmd {
return textinput.Blink return textinput.Blink

View File

@ -5,6 +5,7 @@ import (
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
) )
type MainPage struct { type MainPage struct {
@ -13,6 +14,9 @@ type MainPage struct {
taskPage common.Component taskPage common.Component
timePage common.Component timePage common.Component
currentTab int
width int
height int
} }
func NewMainPage(common *common.Common) *MainPage { func NewMainPage(common *common.Common) *MainPage {
@ -24,6 +28,7 @@ func NewMainPage(common *common.Common) *MainPage {
m.timePage = NewTimePage(common) m.timePage = NewTimePage(common)
m.activePage = m.taskPage m.activePage = m.taskPage
m.currentTab = 0
return m return m
} }
@ -37,17 +42,39 @@ func (m *MainPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.common.SetSize(msg.Width, msg.Height) m.common.SetSize(msg.Width, msg.Height)
tabHeight := lipgloss.Height(m.renderTabBar())
contentHeight := msg.Height - tabHeight
if contentHeight < 0 {
contentHeight = 0
}
newMsg := tea.WindowSizeMsg{Width: msg.Width, Height: contentHeight}
activePage, cmd := m.activePage.Update(newMsg)
m.activePage = activePage.(common.Component)
return m, cmd
case tea.KeyMsg: case tea.KeyMsg:
// Only handle tab key for page switching when at the top level (no subpages active) // Only handle tab key for page switching when at the top level (no subpages active)
if key.Matches(msg, m.common.Keymap.Next) && !m.common.HasSubpages() { if key.Matches(msg, m.common.Keymap.Next) && !m.common.HasSubpages() {
if m.activePage == m.taskPage { if m.activePage == m.taskPage {
m.activePage = m.timePage m.activePage = m.timePage
m.currentTab = 1
} else { } else {
m.activePage = m.taskPage m.activePage = m.taskPage
m.currentTab = 0
} }
// Re-size the new active page just in case
m.activePage.SetSize(m.common.Width(), m.common.Height()) tabHeight := lipgloss.Height(m.renderTabBar())
contentHeight := m.height - tabHeight
if contentHeight < 0 {
contentHeight = 0
}
m.activePage.SetSize(m.width, contentHeight)
// Trigger a refresh/init on switch? Maybe not needed if we keep state. // Trigger a refresh/init on switch? Maybe not needed if we keep state.
// But we might want to refresh data. // But we might want to refresh data.
return m, m.activePage.Init() return m, m.activePage.Init()
@ -60,6 +87,22 @@ func (m *MainPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd return m, cmd
} }
func (m *MainPage) View() string { func (m *MainPage) renderTabBar() string {
return m.activePage.View() var tabs []string
headers := []string{"Tasks", "Time"}
for i, header := range headers {
style := m.common.Styles.Tab
if m.currentTab == i {
style = m.common.Styles.ActiveTab
}
tabs = append(tabs, style.Render(header))
}
row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
return m.common.Styles.TabBar.Width(m.common.Width()).Render(row)
}
func (m *MainPage) View() string {
return lipgloss.JoinVertical(lipgloss.Left, m.renderTabBar(), m.activePage.View())
} }

View File

@ -41,6 +41,8 @@ type TaskEditorPage struct {
area int area int
areaPicker *areaPicker areaPicker *areaPicker
areas []area areas []area
infoViewport viewport.Model
} }
func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPage { func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPage {
@ -68,6 +70,11 @@ func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPag
p.areaPicker = NewAreaPicker(com, []string{"Task", "Tags", "Dates"}) p.areaPicker = NewAreaPicker(com, []string{"Task", "Tags", "Dates"})
p.infoViewport = viewport.New(0, 0)
if p.task.Uuid != "" {
p.infoViewport.SetContent(p.common.TW.GetInformation(&p.task))
}
p.columnCursor = 1 p.columnCursor = 1
if p.task.Uuid == "" { if p.task.Uuid == "" {
p.mode = modeInsert p.mode = modeInsert
@ -94,6 +101,12 @@ func (p *TaskEditorPage) SetSize(width, height int) {
} else { } else {
p.colHeight = height - p.common.Styles.ColumnFocused.GetVerticalFrameSize() p.colHeight = height - p.common.Styles.ColumnFocused.GetVerticalFrameSize()
} }
p.infoViewport.Width = width - p.colWidth - p.common.Styles.ColumnFocused.GetHorizontalFrameSize()*2 - 5
if p.infoViewport.Width < 0 {
p.infoViewport.Width = 0
}
p.infoViewport.Height = p.colHeight
} }
func (p *TaskEditorPage) Init() tea.Cmd { func (p *TaskEditorPage) Init() tea.Cmd {
@ -110,12 +123,20 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.mode = mode(msg) p.mode = mode(msg)
case prevColumnMsg: case prevColumnMsg:
p.columnCursor-- p.columnCursor--
maxCols := 2
if p.task.Uuid != "" {
maxCols = 3
}
if p.columnCursor < 0 { if p.columnCursor < 0 {
p.columnCursor = len(p.areas) - 1 p.columnCursor = maxCols - 1
} }
case nextColumnMsg: case nextColumnMsg:
p.columnCursor++ p.columnCursor++
if p.columnCursor > len(p.areas)-1 { maxCols := 2
if p.task.Uuid != "" {
maxCols = 3
}
if p.columnCursor >= maxCols {
p.columnCursor = 0 p.columnCursor = 0
} }
case prevAreaMsg: case prevAreaMsg:
@ -166,20 +187,26 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } else if p.columnCursor == 1 {
model, cmd := p.areas[p.area].Update(prevFieldMsg{}) model, cmd := p.areas[p.area].Update(prevFieldMsg{})
p.areas[p.area] = model.(area) p.areas[p.area] = model.(area)
return p, cmd return p, cmd
} else if p.columnCursor == 2 {
p.infoViewport.LineUp(1)
return p, nil
} }
case key.Matches(msg, p.common.Keymap.Down): case key.Matches(msg, p.common.Keymap.Down):
if p.columnCursor == 0 { if p.columnCursor == 0 {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } else if p.columnCursor == 1 {
model, cmd := p.areas[p.area].Update(nextFieldMsg{}) model, cmd := p.areas[p.area].Update(nextFieldMsg{})
p.areas[p.area] = model.(area) p.areas[p.area] = model.(area)
return p, cmd return p, cmd
} else if p.columnCursor == 2 {
p.infoViewport.LineDown(1)
return p, nil
} }
} }
} }
@ -212,21 +239,23 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } else if p.columnCursor == 1 {
model, cmd := p.areas[p.area].Update(prevFieldMsg{}) model, cmd := p.areas[p.area].Update(prevFieldMsg{})
p.areas[p.area] = model.(area) p.areas[p.area] = model.(area)
return p, cmd return p, cmd
} }
return p, nil
case key.Matches(msg, p.common.Keymap.Next): case key.Matches(msg, p.common.Keymap.Next):
if p.columnCursor == 0 { if p.columnCursor == 0 {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } else if p.columnCursor == 1 {
model, cmd := p.areas[p.area].Update(nextFieldMsg{}) model, cmd := p.areas[p.area].Update(nextFieldMsg{})
p.areas[p.area] = model.(area) p.areas[p.area] = model.(area)
return p, cmd return p, cmd
} }
return p, nil
case key.Matches(msg, p.common.Keymap.Ok): case key.Matches(msg, p.common.Keymap.Ok):
model, cmd := p.areas[p.area].Update(msg) model, cmd := p.areas[p.area].Update(msg)
if p.area != 3 { if p.area != 3 {
@ -241,6 +270,10 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else if p.columnCursor == 2 {
var cmd tea.Cmd
p.infoViewport, cmd = p.infoViewport.Update(msg)
return p, cmd
} else { } else {
model, cmd := p.areas[p.area].Update(msg) model, cmd := p.areas[p.area].Update(msg)
p.areas[p.area] = model.(area) p.areas[p.area] = model.(area)
@ -253,29 +286,31 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (p *TaskEditorPage) View() string { func (p *TaskEditorPage) View() string {
var focusedStyle, blurredStyle lipgloss.Style var focusedStyle, blurredStyle lipgloss.Style
if p.mode == modeInsert { if p.mode == modeInsert {
focusedStyle = p.common.Styles.ColumnInsert.Width(p.colWidth).Height(p.colHeight) focusedStyle = p.common.Styles.ColumnInsert
} else { } else {
focusedStyle = p.common.Styles.ColumnFocused.Width(p.colWidth).Height(p.colHeight) focusedStyle = p.common.Styles.ColumnFocused
} }
blurredStyle = p.common.Styles.ColumnBlurred.Width(p.colWidth).Height(p.colHeight) blurredStyle = p.common.Styles.ColumnBlurred
// var picker, area string
var area string
if p.columnCursor == 0 {
// picker = focusedStyle.Render(p.areaPicker.View())
area = blurredStyle.Render(p.areas[p.area].View())
} else {
// picker = blurredStyle.Render(p.areaPicker.View())
area = focusedStyle.Render(p.areas[p.area].View())
var area string
if p.columnCursor == 1 {
area = focusedStyle.Copy().Width(p.colWidth).Height(p.colHeight).Render(p.areas[p.area].View())
} else {
area = blurredStyle.Copy().Width(p.colWidth).Height(p.colHeight).Render(p.areas[p.area].View())
} }
if p.task.Uuid != "" { if p.task.Uuid != "" {
var infoView string
if p.columnCursor == 2 {
infoView = focusedStyle.Copy().Width(p.infoViewport.Width).Height(p.infoViewport.Height).Render(p.infoViewport.View())
} else {
infoView = blurredStyle.Copy().Width(p.infoViewport.Width).Height(p.infoViewport.Height).Render(p.infoViewport.View())
}
area = lipgloss.JoinHorizontal( area = lipgloss.JoinHorizontal(
lipgloss.Top, lipgloss.Top,
area, area,
p.common.Styles.ColumnFocused.Render(p.common.TW.GetInformation(&p.task)), infoView,
) )
} }
tabs := "" tabs := ""

View File

@ -5,12 +5,10 @@ import (
"strings" "strings"
"tasksquire/common" "tasksquire/common"
"tasksquire/components/autocomplete" "tasksquire/components/autocomplete"
"tasksquire/components/picker"
"tasksquire/components/timestampeditor" "tasksquire/components/timestampeditor"
"tasksquire/timewarrior" "tasksquire/timewarrior"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@ -20,65 +18,17 @@ type TimeEditorPage struct {
interval *timewarrior.Interval interval *timewarrior.Interval
// Fields // Fields
projectPicker *picker.Picker
startEditor *timestampeditor.TimestampEditor startEditor *timestampeditor.TimestampEditor
endEditor *timestampeditor.TimestampEditor endEditor *timestampeditor.TimestampEditor
tagsInput *autocomplete.Autocomplete tagsInput *autocomplete.Autocomplete
adjust bool adjust bool
// State // State
selectedProject string
currentField int currentField int
totalFields int totalFields int
} }
type timeEditorProjectSelectedMsg struct {
project string
}
func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *TimeEditorPage { 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 // Create start timestamp editor
startEditor := timestampeditor.New(com). startEditor := timestampeditor.New(com).
Title("Start"). Title("Start").
@ -89,50 +39,38 @@ func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *Time
Title("End"). Title("End").
ValueFromString(interval.End) ValueFromString(interval.End)
// Get tag combinations filtered by selected project // Create tags autocomplete with combinations from past intervals
tagCombinations := filterTagCombinationsByProject( tagCombinations := com.TimeW.GetTagCombinations()
com.TimeW.GetTagCombinations(),
selectedProject,
)
tagsInput := autocomplete.New(tagCombinations, 3, 10) tagsInput := autocomplete.New(tagCombinations, 3, 10)
tagsInput.SetPlaceholder("Space separated, use \"\" for tags with spaces") tagsInput.SetPlaceholder("Space separated, use \"\" for tags with spaces")
tagsInput.SetValue(formatTags(remainingTags)) // Use remaining tags (without project) tagsInput.SetValue(formatTags(interval.Tags))
tagsInput.SetWidth(50) tagsInput.SetWidth(50)
p := &TimeEditorPage{ p := &TimeEditorPage{
common: com, common: com,
interval: interval, interval: interval,
projectPicker: projectPicker,
startEditor: startEditor, startEditor: startEditor,
endEditor: endEditor, endEditor: endEditor,
tagsInput: tagsInput, tagsInput: tagsInput,
adjust: true, // Enable :adjust by default adjust: true, // Enable :adjust by default
selectedProject: selectedProject,
currentField: 0, currentField: 0,
totalFields: 5, // Now 5 fields: project, tags, start, end, adjust totalFields: 4, // Updated to include adjust field
} }
return p return p
} }
func (p *TimeEditorPage) Init() tea.Cmd { func (p *TimeEditorPage) Init() tea.Cmd {
// Focus the first field (project picker) // Focus the first field (tags)
p.currentField = 0 p.currentField = 0
return p.projectPicker.Init() p.tagsInput.Focus()
return p.tagsInput.Init()
} }
func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
switch msg := msg.(type) { 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: case tea.KeyMsg:
switch { switch {
case key.Matches(msg, p.common.Keymap.Back): case key.Matches(msg, p.common.Keymap.Back):
@ -144,8 +82,6 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return model, BackCmd return model, BackCmd
case key.Matches(msg, p.common.Keymap.Ok): 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 // Save and exit
p.saveInterval() p.saveInterval()
model, err := p.common.PopPage() model, err := p.common.PopPage()
@ -154,8 +90,6 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return nil, tea.Quit return nil, tea.Quit
} }
return model, tea.Batch(tea.Batch(cmds...), refreshIntervals) 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): case key.Matches(msg, p.common.Keymap.Next):
// Move to next field // Move to next field
@ -177,31 +111,25 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Update the currently focused field // Update the currently focused field
var cmd tea.Cmd var cmd tea.Cmd
switch p.currentField { switch p.currentField {
case 0: // Project picker case 0:
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 var model tea.Model
model, cmd = p.tagsInput.Update(msg) model, cmd = p.tagsInput.Update(msg)
if ac, ok := model.(*autocomplete.Autocomplete); ok { if ac, ok := model.(*autocomplete.Autocomplete); ok {
p.tagsInput = ac p.tagsInput = ac
} }
case 2: // Start (was 1) case 1:
var model tea.Model var model tea.Model
model, cmd = p.startEditor.Update(msg) model, cmd = p.startEditor.Update(msg)
if editor, ok := model.(*timestampeditor.TimestampEditor); ok { if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
p.startEditor = editor p.startEditor = editor
} }
case 3: // End (was 2) case 2:
var model tea.Model var model tea.Model
model, cmd = p.endEditor.Update(msg) model, cmd = p.endEditor.Update(msg)
if editor, ok := model.(*timestampeditor.TimestampEditor); ok { if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
p.endEditor = editor p.endEditor = editor
} }
case 4: // Adjust (was 3) case 3:
// Handle adjust toggle with space/enter // Handle adjust toggle with space/enter
if msg, ok := msg.(tea.KeyMsg); ok { if msg, ok := msg.(tea.KeyMsg); ok {
if msg.String() == " " || msg.String() == "enter" { if msg.String() == " " || msg.String() == "enter" {
@ -217,15 +145,13 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (p *TimeEditorPage) focusCurrentField() tea.Cmd { func (p *TimeEditorPage) focusCurrentField() tea.Cmd {
switch p.currentField { switch p.currentField {
case 0: case 0:
return p.projectPicker.Init() // Picker doesn't have explicit Focus()
case 1:
p.tagsInput.Focus() p.tagsInput.Focus()
return p.tagsInput.Init() return p.tagsInput.Init()
case 2: case 1:
return p.startEditor.Focus() return p.startEditor.Focus()
case 3: case 2:
return p.endEditor.Focus() return p.endEditor.Focus()
case 4: case 3:
// Adjust checkbox doesn't need focus action // Adjust checkbox doesn't need focus action
return nil return nil
} }
@ -235,14 +161,12 @@ func (p *TimeEditorPage) focusCurrentField() tea.Cmd {
func (p *TimeEditorPage) blurCurrentField() { func (p *TimeEditorPage) blurCurrentField() {
switch p.currentField { switch p.currentField {
case 0: case 0:
// Picker doesn't have explicit Blur(), state handled by Update
case 1:
p.tagsInput.Blur() p.tagsInput.Blur()
case 2: case 1:
p.startEditor.Blur() p.startEditor.Blur()
case 3: case 2:
p.endEditor.Blur() p.endEditor.Blur()
case 4: case 3:
// Adjust checkbox doesn't need blur action // Adjust checkbox doesn't need blur action
} }
} }
@ -255,22 +179,10 @@ func (p *TimeEditorPage) View() string {
sections = append(sections, titleStyle.Render("Edit Time Interval")) sections = append(sections, titleStyle.Render("Edit Time Interval"))
sections = append(sections, "") sections = append(sections, "")
// Project picker (field 0) // Tags input (now first)
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 tagsLabelStyle := p.common.Styles.Form.Focused.Title
tagsLabel := tagsLabelStyle.Render("Tags") tagsLabel := tagsLabelStyle.Render("Tags")
if p.currentField == 1 { // Changed from 0 if p.currentField == 0 {
sections = append(sections, tagsLabel) sections = append(sections, tagsLabel)
sections = append(sections, p.tagsInput.View()) sections = append(sections, p.tagsInput.View())
descStyle := p.common.Styles.Form.Focused.Description descStyle := p.common.Styles.Form.Focused.Description
@ -292,7 +204,7 @@ func (p *TimeEditorPage) View() string {
sections = append(sections, p.endEditor.View()) sections = append(sections, p.endEditor.View())
sections = append(sections, "") sections = append(sections, "")
// Adjust checkbox (now field 4, was 3) // Adjust checkbox
adjustLabelStyle := p.common.Styles.Form.Focused.Title adjustLabelStyle := p.common.Styles.Form.Focused.Title
adjustLabel := adjustLabelStyle.Render("Adjust overlaps") adjustLabel := adjustLabelStyle.Render("Adjust overlaps")
@ -303,7 +215,7 @@ func (p *TimeEditorPage) View() string {
checkbox = "[ ]" checkbox = "[ ]"
} }
if p.currentField == 4 { // Changed from 3 if p.currentField == 3 {
focusedStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")) focusedStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
sections = append(sections, adjustLabel) sections = append(sections, adjustLabel)
sections = append(sections, focusedStyle.Render(checkbox+" Auto-adjust overlapping intervals")) sections = append(sections, focusedStyle.Render(checkbox+" Auto-adjust overlapping intervals"))
@ -342,24 +254,8 @@ func (p *TimeEditorPage) saveInterval() {
p.interval.Start = p.startEditor.GetValueString() p.interval.Start = p.startEditor.GetValueString()
p.interval.End = p.endEditor.GetValueString() p.interval.End = p.endEditor.GetValueString()
// Parse tags from input // Parse tags
tags := parseTags(p.tagsInput.GetValue()) p.interval.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) err := p.common.TimeW.ModifyInterval(p.interval, p.adjust)
if err != nil { if err != nil {
@ -402,79 +298,3 @@ func formatTags(tags []string) string {
} }
return strings.Join(formatted, " ") 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
}