// TODO: update table every second (to show correct relative time) package pages import ( "strings" "tasksquire/common" "tasksquire/taskwarrior" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type ReportPage struct { common *common.Common activeReport *taskwarrior.Report activeContext *taskwarrior.Context activeProject string selectedTask *taskwarrior.Task tasks taskwarrior.Tasks taskTable table.Model tableStyle table.Styles keymap ReportKeys subpage tea.Model subpageActive bool } type ReportKeys struct { Quit key.Binding Up key.Binding Down key.Binding Select key.Binding ToggleFocus key.Binding } func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage { s := table.DefaultStyles() s.Header = s.Header. BorderStyle(lipgloss.NormalBorder()). BorderForeground(com.Styles.Active.GetForeground()). BorderBottom(true). Bold(true) s.Selected = s.Selected. Reverse(true). Bold(true) keys := ReportKeys{ Quit: key.NewBinding( key.WithKeys("q", "ctrl+c"), key.WithHelp("q, ctrl+c", "Quit"), ), Up: key.NewBinding( key.WithKeys("k", "up"), key.WithHelp("↑/k", "Up"), ), Down: key.NewBinding( key.WithKeys("j", "down"), key.WithHelp("↓/j", "Down"), ), Select: key.NewBinding( key.WithKeys("enter"), key.WithHelp("enter", "Select"), ), ToggleFocus: key.NewBinding( key.WithKeys("esc"), key.WithHelp("esc", "Toggle focus"), ), } return &ReportPage{ common: com, activeReport: report, activeContext: com.TW.GetActiveContext(), activeProject: "", tableStyle: s, keymap: keys, } } func (p *ReportPage) SetSize(width int, height int) { p.common.SetSize(width, height) p.taskTable.SetWidth(width - 2) p.taskTable.SetHeight(height - 4) } func (p *ReportPage) Init() tea.Cmd { return p.getTasks() } func (p *ReportPage) 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: p.subpageActive = false case 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 UpdatedTasksMsg: cmds = append(cmds, p.getTasks()) case tea.KeyMsg: 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) p.subpage.Init() p.subpageActive = true p.common.PushPage(p) return p.subpage, nil case key.Matches(msg, p.common.Keymap.SetContext): p.subpage = NewContextPickerPage(p.common) p.subpage.Init() p.subpageActive = true p.common.PushPage(p) return p.subpage, nil case key.Matches(msg, p.common.Keymap.Add): p.subpage = NewTaskEditorPage(p.common, taskwarrior.Task{}) p.subpage.Init() p.subpageActive = true p.common.PushPage(p) return p.subpage, nil case key.Matches(msg, p.common.Keymap.Edit): p.subpage = NewTaskEditorPage(p.common, *p.selectedTask) p.subpage.Init() p.subpageActive = true p.common.PushPage(p) return p.subpage, 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.SetProject): p.subpage = NewProjectPickerPage(p.common, p.activeProject) p.subpage.Init() p.subpageActive = true p.common.PushPage(p) return p.subpage, nil case key.Matches(msg, p.common.Keymap.Undo): p.common.TW.Undo() return p, p.getTasks() } } var cmd tea.Cmd 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 *ReportPage) View() string { // return p.common.Styles.Main.Render(p.taskTable.View()) + "\n" if p.tasks == nil || len(p.tasks) == 0 { return p.common.Styles.Main.Render("No tasks found") } return p.common.Styles.Main.Render(p.taskTable.View()) } func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) { var selected int nCols := len(p.activeReport.Columns) columns := make([]table.Column, 0) columnSizes := make([]int, nCols) fullRows := make([]table.Row, len(tasks)) rows := make([]table.Row, len(tasks)) descIndex := -1 for i, task := range tasks { if p.selectedTask != nil && task.Uuid == p.selectedTask.Uuid { selected = i } row := table.Row{} for i, col := range p.activeReport.Columns { if strings.Contains(col, "description") { descIndex = i } field := task.GetString(col) columnSizes[i] = max(columnSizes[i], len(field)) row = append(row, field) } fullRows[i] = row } for i, r := range fullRows { row := table.Row{} for j, size := range columnSizes { if size == 0 { continue } row = append(row, r[j]) } rows[i] = row } combinedSize := 0 for i, label := range p.activeReport.Labels { if columnSizes[i] == 0 { continue } width := max(columnSizes[i], len(label)) columns = append(columns, table.Column{Title: label, Width: width}) if i == descIndex { descIndex = len(columns) - 1 continue } combinedSize += width } if descIndex >= 0 { columns[descIndex].Width = p.taskTable.Width() - combinedSize - 14 } if selected == 0 { selected = p.taskTable.Cursor() } p.taskTable = table.New( table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), // table.WithHeight(7), // table.WithWidth(100), ) p.taskTable.SetStyles(p.tableStyle) if selected < len(p.tasks) { p.taskTable.SetCursor(selected) } else { p.taskTable.SetCursor(len(p.tasks) - 1) } } func (p *ReportPage) 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 TaskMsg(tasks) } } type TaskMsg taskwarrior.Tasks