Add things
This commit is contained in:
465
pages/projectTaskPicker.go
Normal file
465
pages/projectTaskPicker.go
Normal file
@ -0,0 +1,465 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tasksquire/common"
|
||||
"tasksquire/components/picker"
|
||||
"tasksquire/taskwarrior"
|
||||
"tasksquire/timewarrior"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type ProjectTaskPickerPage struct {
|
||||
common *common.Common
|
||||
|
||||
// Both pickers visible simultaneously
|
||||
projectPicker *picker.Picker
|
||||
taskPicker *picker.Picker
|
||||
selectedProject string
|
||||
selectedTask *taskwarrior.Task
|
||||
|
||||
// Focus tracking: 0 = project picker, 1 = task picker
|
||||
focusedPicker int
|
||||
}
|
||||
|
||||
type projectTaskPickerProjectSelectedMsg struct {
|
||||
project string
|
||||
}
|
||||
|
||||
type projectTaskPickerTaskSelectedMsg struct {
|
||||
task *taskwarrior.Task
|
||||
}
|
||||
|
||||
func NewProjectTaskPickerPage(com *common.Common) *ProjectTaskPickerPage {
|
||||
p := &ProjectTaskPickerPage{
|
||||
common: com,
|
||||
focusedPicker: 0,
|
||||
}
|
||||
|
||||
// Create project picker
|
||||
projectItemProvider := func() []list.Item {
|
||||
projects := com.TW.GetProjects()
|
||||
items := make([]list.Item, 0, len(projects))
|
||||
|
||||
for _, proj := range projects {
|
||||
items = append(items, picker.NewItem(proj))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
projectOnSelect := func(item list.Item) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return projectTaskPickerProjectSelectedMsg{project: item.FilterValue()}
|
||||
}
|
||||
}
|
||||
|
||||
p.projectPicker = picker.New(
|
||||
com,
|
||||
"Projects",
|
||||
projectItemProvider,
|
||||
projectOnSelect,
|
||||
)
|
||||
|
||||
// Initialize with the first project's tasks
|
||||
projects := com.TW.GetProjects()
|
||||
if len(projects) > 0 {
|
||||
p.selectedProject = projects[0]
|
||||
p.createTaskPicker(projects[0])
|
||||
} else {
|
||||
// No projects - create empty task picker
|
||||
p.createTaskPicker("")
|
||||
}
|
||||
|
||||
p.SetSize(com.Width(), com.Height())
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *ProjectTaskPickerPage) createTaskPicker(project string) {
|
||||
// Build filters for tasks
|
||||
filters := []string{"+track", "status:pending"}
|
||||
|
||||
if project != "" {
|
||||
// Tasks in the selected project
|
||||
filters = append(filters, "project:"+project)
|
||||
}
|
||||
|
||||
taskItemProvider := func() []list.Item {
|
||||
tasks := p.common.TW.GetTasks(nil, filters...)
|
||||
items := make([]list.Item, 0, len(tasks))
|
||||
|
||||
for i := range tasks {
|
||||
// Just use the description as the item text
|
||||
// picker.NewItem creates a simple item with title and filter value
|
||||
items = append(items, picker.NewItem(tasks[i].Description))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
taskOnSelect := func(item list.Item) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Find the task by description
|
||||
tasks := p.common.TW.GetTasks(nil, filters...)
|
||||
for _, task := range tasks {
|
||||
if task.Description == item.FilterValue() {
|
||||
// tasks is already []*Task, so task is already *Task
|
||||
return projectTaskPickerTaskSelectedMsg{task: task}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
title := "Tasks with +track"
|
||||
if project != "" {
|
||||
title = fmt.Sprintf("Tasks: %s", project)
|
||||
}
|
||||
|
||||
p.taskPicker = picker.New(
|
||||
p.common,
|
||||
title,
|
||||
taskItemProvider,
|
||||
taskOnSelect,
|
||||
picker.WithFilterByDefault(false), // Start in list mode, not filter mode
|
||||
)
|
||||
}
|
||||
|
||||
func (p *ProjectTaskPickerPage) Init() tea.Cmd {
|
||||
// Focus the project picker initially
|
||||
p.projectPicker.Focus()
|
||||
return tea.Batch(p.projectPicker.Init(), p.taskPicker.Init())
|
||||
}
|
||||
|
||||
func (p *ProjectTaskPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
p.SetSize(msg.Width, msg.Height)
|
||||
|
||||
case projectTaskPickerProjectSelectedMsg:
|
||||
// Project selected - update task picker
|
||||
p.selectedProject = msg.project
|
||||
p.createTaskPicker(msg.project)
|
||||
// Move focus to task picker
|
||||
p.projectPicker.Blur()
|
||||
p.taskPicker.Focus()
|
||||
p.focusedPicker = 1
|
||||
p.SetSize(p.common.Width(), p.common.Height())
|
||||
return p, p.taskPicker.Init()
|
||||
|
||||
case projectTaskPickerTaskSelectedMsg:
|
||||
// Task selected - emit TaskPickedMsg and return to parent
|
||||
p.selectedTask = msg.task
|
||||
model, err := p.common.PopPage()
|
||||
if err != nil {
|
||||
return p, tea.Quit
|
||||
}
|
||||
return model, func() tea.Msg {
|
||||
return TaskPickedMsg{Task: p.selectedTask}
|
||||
}
|
||||
|
||||
case UpdatedTasksMsg:
|
||||
// Task was edited - refresh the task list and recreate the task picker
|
||||
if p.selectedProject != "" {
|
||||
p.createTaskPicker(p.selectedProject)
|
||||
p.SetSize(p.common.Width(), p.common.Height())
|
||||
// Keep the task picker focused
|
||||
p.taskPicker.Focus()
|
||||
p.focusedPicker = 1
|
||||
return p, p.taskPicker.Init()
|
||||
}
|
||||
return p, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
// Check if the focused picker is in filtering mode BEFORE handling any keys
|
||||
var focusedPickerFiltering bool
|
||||
if p.focusedPicker == 0 {
|
||||
focusedPickerFiltering = p.projectPicker.IsFiltering()
|
||||
} else {
|
||||
focusedPickerFiltering = p.taskPicker.IsFiltering()
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, p.common.Keymap.Back):
|
||||
// If the focused picker is filtering, let it handle the escape key to dismiss the filter
|
||||
// and don't exit the page
|
||||
if focusedPickerFiltering {
|
||||
// Don't handle the Back key - let it fall through to the picker
|
||||
break
|
||||
}
|
||||
// Exit picker completely
|
||||
model, err := p.common.PopPage()
|
||||
if err != nil {
|
||||
return p, tea.Quit
|
||||
}
|
||||
return model, BackCmd
|
||||
|
||||
case key.Matches(msg, p.common.Keymap.Add):
|
||||
// Don't handle 'a' if focused picker is filtering - let the picker handle it for typing
|
||||
if focusedPickerFiltering {
|
||||
break
|
||||
}
|
||||
// Create new task with selected project and track tag pre-filled
|
||||
newTask := taskwarrior.NewTask()
|
||||
newTask.Project = p.selectedProject
|
||||
newTask.Tags = []string{"track"}
|
||||
|
||||
// Open task editor with pre-populated task
|
||||
taskEditor := NewTaskEditorPage(p.common, newTask)
|
||||
p.common.PushPage(p)
|
||||
return taskEditor, taskEditor.Init()
|
||||
|
||||
case key.Matches(msg, p.common.Keymap.Edit):
|
||||
// Don't handle 'e' if focused picker is filtering - let the picker handle it for typing
|
||||
if focusedPickerFiltering {
|
||||
break
|
||||
}
|
||||
// Edit task when task picker is focused and a task is selected
|
||||
if p.focusedPicker == 1 && p.selectedProject != "" {
|
||||
// Get the currently highlighted task
|
||||
selectedItemText := p.taskPicker.GetValue()
|
||||
if selectedItemText != "" {
|
||||
// Find the task by description
|
||||
filters := []string{"+track", "status:pending"}
|
||||
filters = append(filters, "project:"+p.selectedProject)
|
||||
|
||||
tasks := p.common.TW.GetTasks(nil, filters...)
|
||||
for _, task := range tasks {
|
||||
if task.Description == selectedItemText {
|
||||
// Found the task - open editor
|
||||
p.selectedTask = task
|
||||
taskEditor := NewTaskEditorPage(p.common, *task)
|
||||
p.common.PushPage(p)
|
||||
return taskEditor, taskEditor.Init()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
|
||||
case key.Matches(msg, p.common.Keymap.Tag):
|
||||
// Don't handle 't' if focused picker is filtering - let the picker handle it for typing
|
||||
if focusedPickerFiltering {
|
||||
break
|
||||
}
|
||||
// Open time editor with task pre-filled when task picker is focused
|
||||
if p.focusedPicker == 1 && p.selectedProject != "" {
|
||||
// Get the currently highlighted task
|
||||
selectedItemText := p.taskPicker.GetValue()
|
||||
if selectedItemText != "" {
|
||||
// Find the task by description
|
||||
filters := []string{"+track", "status:pending"}
|
||||
filters = append(filters, "project:"+p.selectedProject)
|
||||
|
||||
tasks := p.common.TW.GetTasks(nil, filters...)
|
||||
for _, task := range tasks {
|
||||
if task.Description == selectedItemText {
|
||||
// Found the task - create new interval with task pre-filled
|
||||
interval := createIntervalFromTask(task)
|
||||
|
||||
// Open time editor with pre-populated interval
|
||||
timeEditor := NewTimeEditorPage(p.common, interval)
|
||||
p.common.PushPage(p)
|
||||
return timeEditor, timeEditor.Init()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
|
||||
case key.Matches(msg, p.common.Keymap.Next):
|
||||
// Tab: switch focus between pickers
|
||||
if p.focusedPicker == 0 {
|
||||
p.projectPicker.Blur()
|
||||
p.taskPicker.Focus()
|
||||
p.focusedPicker = 1
|
||||
} else {
|
||||
p.taskPicker.Blur()
|
||||
p.projectPicker.Focus()
|
||||
p.focusedPicker = 0
|
||||
}
|
||||
return p, nil
|
||||
|
||||
case key.Matches(msg, p.common.Keymap.Prev):
|
||||
// Shift+Tab: switch focus between pickers (reverse)
|
||||
if p.focusedPicker == 1 {
|
||||
p.taskPicker.Blur()
|
||||
p.projectPicker.Focus()
|
||||
p.focusedPicker = 0
|
||||
} else {
|
||||
p.projectPicker.Blur()
|
||||
p.taskPicker.Focus()
|
||||
p.focusedPicker = 1
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Update the focused picker
|
||||
var cmd tea.Cmd
|
||||
if p.focusedPicker == 0 {
|
||||
// Track the previous project selection
|
||||
previousProject := p.selectedProject
|
||||
|
||||
_, cmd = p.projectPicker.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// Check if the highlighted project changed
|
||||
currentProject := p.projectPicker.GetValue()
|
||||
if currentProject != previousProject && currentProject != "" {
|
||||
// Update the selected project and refresh task picker
|
||||
p.selectedProject = currentProject
|
||||
p.createTaskPicker(currentProject)
|
||||
p.SetSize(p.common.Width(), p.common.Height())
|
||||
cmds = append(cmds, p.taskPicker.Init())
|
||||
}
|
||||
} else {
|
||||
_, cmd = p.taskPicker.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return p, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (p *ProjectTaskPickerPage) View() string {
|
||||
// Render both pickers (they handle their own focused/blurred styling)
|
||||
projectView := p.projectPicker.View()
|
||||
taskView := p.taskPicker.View()
|
||||
|
||||
// Create distinct styling for focused vs blurred pickers
|
||||
var projectStyled, taskStyled string
|
||||
|
||||
if p.focusedPicker == 0 {
|
||||
// Project picker is focused
|
||||
projectStyled = lipgloss.NewStyle().
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderForeground(lipgloss.Color("6")). // Cyan for focused
|
||||
Padding(0, 1).
|
||||
Render(projectView)
|
||||
|
||||
taskStyled = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")). // Gray for blurred
|
||||
Padding(0, 1).
|
||||
Render(taskView)
|
||||
} else {
|
||||
// Task picker is focused
|
||||
projectStyled = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")). // Gray for blurred
|
||||
Padding(0, 1).
|
||||
Render(projectView)
|
||||
|
||||
taskStyled = lipgloss.NewStyle().
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderForeground(lipgloss.Color("6")). // Cyan for focused
|
||||
Padding(0, 1).
|
||||
Render(taskView)
|
||||
}
|
||||
|
||||
// Layout side by side if width permits, otherwise stack vertically
|
||||
var content string
|
||||
if p.common.Width() >= 100 {
|
||||
// Side by side layout
|
||||
content = lipgloss.JoinHorizontal(lipgloss.Top, projectStyled, " ", taskStyled)
|
||||
} else {
|
||||
// Vertical stack layout
|
||||
content = lipgloss.JoinVertical(lipgloss.Left, projectStyled, "", taskStyled)
|
||||
}
|
||||
|
||||
// Add help text
|
||||
helpText := "Tab/Shift+Tab: switch focus • Enter: select • a: add task • e: edit • t: track time • Esc: cancel"
|
||||
helpStyled := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("241")).
|
||||
Italic(true).
|
||||
Render(helpText)
|
||||
|
||||
fullContent := lipgloss.JoinVertical(lipgloss.Left, content, "", helpStyled)
|
||||
|
||||
return lipgloss.Place(
|
||||
p.common.Width(),
|
||||
p.common.Height(),
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
fullContent,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *ProjectTaskPickerPage) SetSize(width, height int) {
|
||||
p.common.SetSize(width, height)
|
||||
|
||||
// Calculate sizes based on layout
|
||||
var projectWidth, taskWidth, listHeight int
|
||||
|
||||
if width >= 100 {
|
||||
// Side by side layout
|
||||
projectWidth = 30
|
||||
taskWidth = width - projectWidth - 10 // Account for margins and padding
|
||||
if taskWidth > 60 {
|
||||
taskWidth = 60
|
||||
}
|
||||
} else {
|
||||
// Vertical stack layout
|
||||
projectWidth = width - 8
|
||||
taskWidth = width - 8
|
||||
if projectWidth > 60 {
|
||||
projectWidth = 60
|
||||
}
|
||||
if taskWidth > 60 {
|
||||
taskWidth = 60
|
||||
}
|
||||
}
|
||||
|
||||
// Height for each picker
|
||||
listHeight = height - 10 // Account for help text and padding
|
||||
if listHeight > 25 {
|
||||
listHeight = 25
|
||||
}
|
||||
if listHeight < 10 {
|
||||
listHeight = 10
|
||||
}
|
||||
|
||||
if p.projectPicker != nil {
|
||||
p.projectPicker.SetSize(projectWidth, listHeight)
|
||||
}
|
||||
|
||||
if p.taskPicker != nil {
|
||||
p.taskPicker.SetSize(taskWidth, listHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// createIntervalFromTask creates a new time interval pre-filled with task metadata
|
||||
func createIntervalFromTask(task *taskwarrior.Task) *timewarrior.Interval {
|
||||
interval := timewarrior.NewInterval()
|
||||
|
||||
// Set start time to now (UTC format)
|
||||
interval.Start = time.Now().UTC().Format("20060102T150405Z")
|
||||
// Leave End empty for active tracking
|
||||
interval.End = ""
|
||||
|
||||
// Build tags from task metadata
|
||||
tags := []string{}
|
||||
|
||||
// Add UUID tag for task linking
|
||||
if task.Uuid != "" {
|
||||
tags = append(tags, "uuid:"+task.Uuid)
|
||||
}
|
||||
|
||||
// Add project tag
|
||||
if task.Project != "" {
|
||||
tags = append(tags, "project:"+task.Project)
|
||||
}
|
||||
|
||||
// Add existing task tags (excluding virtual tags)
|
||||
tags = append(tags, task.Tags...)
|
||||
|
||||
interval.Tags = tags
|
||||
|
||||
return interval
|
||||
}
|
||||
Reference in New Issue
Block a user