Compare commits
2 Commits
2b31d9bc2b
...
feat/task
| Author | SHA1 | Date | |
|---|---|---|---|
| 2baf3859fd | |||
| 2940711b26 |
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 := ""
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user