// TODO: update table every second (to show correct relative time) package pages import ( "tasksquire/internal/common" "tasksquire/internal/components/table" "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 table.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: table.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) p.populateTaskTable(p.tasks) // case UpdateReportMsg: // p.activeReport = msg // cmds = append(cmds, p.getTasks()) // case UpdateContextMsg: // p.activeContext = msg // p.common.TW.SetContext(msg) // 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 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 } selected := p.taskTable.Cursor() // 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 // } // numCols := len(p.activeReport.Columns) taskRows := make([]table.Row, len(tasks)) taskStyles := make([]lipgloss.Style, len(tasks)) widths := make([]int, numCols) for i, task := range tasks { row := make(table.Row, numCols) for j, colKey := range p.activeReport.Columns { val := task.GetString(colKey) row[j] = val widths[j] = max(widths[j], lipgloss.Width(val)) } taskRows[i] = row taskStyles[i] = common.GetTaskTabelStyle(task, *p.common) } var columns []table.Column for j, w := range widths { title := p.activeReport.Labels[j] width := 0 if w > 0 { width = max(w, lipgloss.Width(title)) } columns = append(columns, table.Column{ Title: title, Width: width, }) } if len(columns) > 0 { usedWidth := 0 for i := 0; i < len(columns)-1; i++ { usedWidth += columns[i].Width + 1 // padding/border offset } remaining := p.taskTable.Width() - usedWidth - 1 lastIdx := len(columns) - 1 columns[lastIdx].Width = max(columns[lastIdx].Width, remaining) } p.taskTable = table.New( table.WithColumns(columns), table.WithRows(taskRows), table.WithRowStyles(taskStyles), table.WithFocused(true), table.WithWidth(baseWidth), table.WithHeight(tableHeight), table.WithStyles(table.Styles{ Header: p.common.Styles.TableStyle.Header , Cell: p.common.Styles.TableStyle.Cell, Selected: p.common.Styles.TableStyle.Selected, }), ) if selected == 0 { selected = p.taskTable.Cursor() } if selected < len(tasks) { 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) } }