// TODO: update table every second (to show correct relative time) package pages import ( "tasksquire/internal/common" "tasksquire/internal/components/tasktable" "tasksquire/internal/taskwarrior" tea "charm.land/bubbletea/v2" // "charm.land/lipgloss/v2" "charm.land/bubbles/v2/key" ) type TaskPage struct { common *common.Common activeReport *taskwarrior.Report activeContext *taskwarrior.Context activeProject string selectedTask *taskwarrior.Task taskCursor int tasks taskwarrior.Tasks taskTable tasktable.Model // Details panel state // detailsPanelActive bool // detailsViewer *detailsviewer.DetailsViewer subpage common.Component } func NewTaskPage(com *common.Common, report *taskwarrior.Report) *TaskPage { p := &TaskPage{ common: com, activeReport: report, activeContext: com.TW.GetActiveContext(), activeProject: "", taskTable: tasktable.New(), // detailsPanelActive: false, // detailsViewer: detailsviewer.New(com), } return p } func (p *TaskPage) SetSize(width int, height int) { p.common.SetSize(width, height) baseHeight := height - p.common.Styles.Base.GetVerticalFrameSize() baseWidth := width - p.common.Styles.Base.GetHorizontalFrameSize() var tableHeight int // if p.detailsPanelActive { // // Allocate 60% for table, 40% for details panel // // Minimum 5 lines for details, minimum 10 lines for table // detailsHeight := max(min(baseHeight*2/5, baseHeight-10), 5) // tableHeight = baseHeight - detailsHeight - 1 // -1 for spacing // // // Set component size (component handles its own border/padding) // // p.detailsViewer.SetSize(baseWidth, detailsHeight) // } else { tableHeight = baseHeight // } p.taskTable.SetWidth(baseWidth) p.taskTable.SetHeight(tableHeight) } func (p *TaskPage) Init() tea.Cmd { return tea.Batch(p.getTasks(), common.DoTick()) } func (p *TaskPage) 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 BackMsg: case common.TickMsg: cmds = append(cmds, p.getTasks()) cmds = append(cmds, common.DoTick()) return p, tea.Batch(cmds...) case common.TaskMsg: p.tasks = taskwarrior.Tasks(msg) // case UpdateReportMsg: // p.activeReport = msg // cmds = append(cmds, p.getTasks()) // case UpdateContextMsg: // p.activeContext = msg // p.common.TW.SetContext(msg) // p.populateTaskTable(p.tasks) // cmds = append(cmds, p.getTasks()) // case UpdateProjectMsg: // p.activeProject = string(msg) // cmds = append(cmds, p.getTasks()) // case TaskPickedMsg: // if msg.Task != nil && msg.Task.Status == "pending" { // p.common.TW.StopActiveTasks() // p.common.TW.StartTask(msg.Task) // } // cmds = append(cmds, p.getTasks()) // case UpdatedTasksMsg: // cmds = append(cmds, p.getTasks()) case tea.KeyPressMsg: // Handle ESC when details panel is active // if p.detailsPanelActive && key.Matches(msg, p.common.Keymap.Back) { // p.detailsPanelActive = false // p.detailsViewer.Blur() // p.SetSize(p.common.Width(), p.common.Height()) // return p, nil // } switch { case key.Matches(msg, p.common.Keymap.Quit): return p, tea.Quit } // case key.Matches(msg, p.common.Keymap.SetReport): // p.subpage = NewReportPickerPage(p.common, p.activeReport) // cmd := p.subpage.Init() // p.common.PushPage(p) // return p.subpage, cmd // case key.Matches(msg, p.common.Keymap.SetContext): // p.subpage = NewContextPickerPage(p.common) // cmd := p.subpage.Init() // p.common.PushPage(p) // return p.subpage, cmd // case key.Matches(msg, p.common.Keymap.Add): // p.subpage = NewTaskEditorPage(p.common, taskwarrior.NewTask()) // cmd := p.subpage.Init() // p.common.PushPage(p) // return p.subpage, cmd // case key.Matches(msg, p.common.Keymap.Edit): // p.subpage = NewTaskEditorPage(p.common, *p.selectedTask) // cmd := p.subpage.Init() // p.common.PushPage(p) // return p.subpage, cmd // case key.Matches(msg, p.common.Keymap.Subtask): // if p.selectedTask != nil { // // Create new task inheriting parent's attributes // newTask := taskwarrior.NewTask() // // // Set parent relationship // newTask.Parent = p.selectedTask.Uuid // // // Copy parent's attributes // newTask.Project = p.selectedTask.Project // newTask.Priority = p.selectedTask.Priority // newTask.Tags = make([]string, len(p.selectedTask.Tags)) // copy(newTask.Tags, p.selectedTask.Tags) // // // Copy UDAs (except "details" which is task-specific) // if p.selectedTask.Udas != nil { // newTask.Udas = make(map[string]any) // for k, v := range p.selectedTask.Udas { // // Skip "details" UDA - it's specific to parent task // if k == "details" { // continue // } // // Deep copy other UDA values // newTask.Udas[k] = v // } // } // // // Open task editor with pre-populated task // p.subpage = NewTaskEditorPage(p.common, newTask) // cmd := p.subpage.Init() // p.common.PushPage(p) // return p.subpage, cmd // } // return p, nil // case key.Matches(msg, p.common.Keymap.Ok): // p.common.TW.SetTaskDone(p.selectedTask) // return p, p.getTasks() // case key.Matches(msg, p.common.Keymap.Delete): // p.common.TW.DeleteTask(p.selectedTask) // return p, p.getTasks() // case key.Matches(msg, p.common.Keymap.SetProject): // p.subpage = NewProjectPickerPage(p.common, p.activeProject) // cmd := p.subpage.Init() // p.common.PushPage(p) // return p.subpage, cmd // case key.Matches(msg, p.common.Keymap.PickProjectTask): // p.subpage = NewProjectTaskPickerPage(p.common) // cmd := p.subpage.Init() // p.common.PushPage(p) // return p.subpage, cmd // case key.Matches(msg, p.common.Keymap.Tag): // if p.selectedTask != nil { // tag := p.common.TW.GetConfig().Get("uda.tasksquire.tag.default") // if p.selectedTask.HasTag(tag) { // p.selectedTask.RemoveTag(tag) // } else { // p.selectedTask.AddTag(tag) // } // p.common.TW.ImportTask(p.selectedTask) // return p, p.getTasks() // } // return p, nil // case key.Matches(msg, p.common.Keymap.Undo): // p.common.TW.Undo() // return p, p.getTasks() // case key.Matches(msg, p.common.Keymap.StartStop): // if p.selectedTask != nil && p.selectedTask.Status == "pending" { // if p.selectedTask.Start == "" { // p.common.TW.StopActiveTasks() // p.common.TW.StartTask(p.selectedTask) // } else { // p.common.TW.StopTask(p.selectedTask) // } // return p, p.getTasks() // } // case key.Matches(msg, p.common.Keymap.ViewDetails): // if p.selectedTask != nil { // // Toggle details panel // p.detailsPanelActive = !p.detailsPanelActive // if p.detailsPanelActive { // p.detailsViewer.SetTask(p.selectedTask) // p.detailsViewer.Focus() // } else { // p.detailsViewer.Blur() // } // p.SetSize(p.common.Width(), p.common.Height()) // return p, nil // } // } // } // // var cmd tea.Cmd // // // Route keyboard messages to details viewer when panel is active // if p.detailsPanelActive { // var viewerCmd tea.Cmd // var viewerModel tea.Model // viewerModel, viewerCmd = p.detailsViewer.Update(msg) // p.detailsViewer = viewerModel.(*detailsviewer.DetailsViewer) // cmds = append(cmds, viewerCmd) // } else { // // Route to table when details panel not active // p.taskTable, cmd = p.taskTable.Update(msg) // cmds = append(cmds, cmd) // // if p.tasks != nil && len(p.tasks) > 0 { // p.selectedTask = p.tasks[p.taskTable.Cursor()] // } else { // p.selectedTask = nil // } // } } return p, tea.Batch(cmds...) } func (p *TaskPage) View() tea.View { if len(p.tasks) == 0 { return tea.NewView(p.common.Styles.Base.Render("No tasks found")) } tableView := p.taskTable.View() return tea.NewView(tableView) } // // if !p.detailsPanelActive { // return tableView // } // // // Combine table and details panel vertically // return lipgloss.JoinVertical( // lipgloss.Left, // tableView, // p.detailsViewer.View(), // ) // } // // func (p *TaskPage) populateTaskTable(tasks taskwarrior.Tasks) { // if len(tasks) == 0 { // return // } // // // Build task tree for hierarchical display // taskTree := taskwarrior.BuildTaskTree(tasks) // // // Use flattened tree list for display order // orderedTasks := make(taskwarrior.Tasks, len(taskTree.FlatList)) // for i, node := range taskTree.FlatList { // orderedTasks[i] = node.Task // } // // selected := p.taskTable.Cursor() // // // Adjust cursor for tree ordering // if p.selectedTask != nil { // for i, task := range orderedTasks { // if task.Uuid == p.selectedTask.Uuid { // selected = i // break // } // } // } // if selected > len(orderedTasks)-1 { // selected = len(orderedTasks) - 1 // } // // // Calculate proper dimensions based on whether details panel is active // baseHeight := p.common.Height() - p.common.Styles.Base.GetVerticalFrameSize() // baseWidth := p.common.Width() - p.common.Styles.Base.GetHorizontalFrameSize() // // var tableHeight int // if p.detailsPanelActive { // // Allocate 60% for table, 40% for details panel // // Minimum 5 lines for details, minimum 10 lines for table // detailsHeight := max(min(baseHeight*2/5, baseHeight-10), 5) // tableHeight = baseHeight - detailsHeight - 1 // -1 for spacing // } else { // tableHeight = baseHeight // } // // p.taskTable = table.New( // p.common, // able.WithReport(p.activeReport), // table.WithTasks(orderedTasks), // table.WithTaskTree(taskTree), // table.WithFocused(true), // table.WithWidth(baseWidth), // table.WithHeight(tableHeight), // table.WithStyles(p.common.Styles.TableStyle), // ) // // if selected == 0 { // selected = p.taskTable.Cursor() // } // if selected < len(orderedTasks) { // p.taskTable.SetCursor(selected) // } else { // p.taskTable.SetCursor(len(p.tasks) - 1) // } // // // Refresh details content if panel is active // if p.detailsPanelActive && p.selectedTask != nil { // p.detailsViewer.SetTask(p.selectedTask) // } // } // func (p *TaskPage) getTasks() tea.Cmd { return func() tea.Msg { filters := []string{} if p.activeProject != "" { filters = append(filters, "project:"+p.activeProject) } tasks := p.common.TW.GetTasks(p.activeReport, filters...) return common.TaskMsg(tasks) } }