Add things
This commit is contained in:
@@ -159,6 +159,40 @@ func (p *ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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()
|
||||
@@ -264,17 +298,28 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
|
||||
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 tasks {
|
||||
for i, task := range orderedTasks {
|
||||
if task.Uuid == p.selectedTask.Uuid {
|
||||
selected = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if selected > len(tasks)-1 {
|
||||
selected = len(tasks) - 1
|
||||
if selected > len(orderedTasks)-1 {
|
||||
selected = len(orderedTasks) - 1
|
||||
}
|
||||
|
||||
// Calculate proper dimensions based on whether details panel is active
|
||||
@@ -294,7 +339,8 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
|
||||
p.taskTable = table.New(
|
||||
p.common,
|
||||
table.WithReport(p.activeReport),
|
||||
table.WithTasks(tasks),
|
||||
table.WithTasks(orderedTasks),
|
||||
table.WithTaskTree(taskTree),
|
||||
table.WithFocused(true),
|
||||
table.WithWidth(baseWidth),
|
||||
table.WithHeight(tableHeight),
|
||||
@@ -304,7 +350,7 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
|
||||
if selected == 0 {
|
||||
selected = p.taskTable.Cursor()
|
||||
}
|
||||
if selected < len(tasks) {
|
||||
if selected < len(orderedTasks) {
|
||||
p.taskTable.SetCursor(selected)
|
||||
} else {
|
||||
p.taskTable.SetCursor(len(p.tasks) - 1)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"tasksquire/common"
|
||||
"tasksquire/components/autocomplete"
|
||||
"tasksquire/components/picker"
|
||||
"tasksquire/components/timestampeditor"
|
||||
"tasksquire/taskwarrior"
|
||||
"tasksquire/timewarrior"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
@@ -21,6 +23,7 @@ type TimeEditorPage struct {
|
||||
|
||||
// Fields
|
||||
projectPicker *picker.Picker
|
||||
taskPicker *picker.Picker
|
||||
startEditor *timestampeditor.TimestampEditor
|
||||
endEditor *timestampeditor.TimestampEditor
|
||||
tagsInput *autocomplete.Autocomplete
|
||||
@@ -28,6 +31,7 @@ type TimeEditorPage struct {
|
||||
|
||||
// State
|
||||
selectedProject string
|
||||
selectedTask *taskwarrior.Task
|
||||
currentField int
|
||||
totalFields int
|
||||
uuid string // Preserved UUID tag
|
||||
@@ -38,18 +42,91 @@ type timeEditorProjectSelectedMsg struct {
|
||||
project string
|
||||
}
|
||||
|
||||
type timeEditorTaskSelectedMsg struct {
|
||||
task *taskwarrior.Task
|
||||
}
|
||||
|
||||
// createTaskPickerForProject creates a picker showing tasks with +track tag for the given project
|
||||
func createTaskPickerForProject(com *common.Common, project string, defaultTask string) *picker.Picker {
|
||||
// Build filters for tasks with +track tag
|
||||
filters := []string{"+track", "status:pending"}
|
||||
if project != "" {
|
||||
filters = append(filters, "project:"+project)
|
||||
}
|
||||
|
||||
taskItemProvider := func() []list.Item {
|
||||
tasks := com.TW.GetTasks(nil, filters...)
|
||||
// Add "(none)" as first option, then all tasks
|
||||
items := make([]list.Item, 0, len(tasks)+1)
|
||||
items = append(items, picker.NewItem("(none)"))
|
||||
for i := range tasks {
|
||||
items = append(items, picker.NewItem(tasks[i].Description))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
taskOnSelect := func(item list.Item) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Handle "(none)" selection
|
||||
if item.FilterValue() == "(none)" {
|
||||
return timeEditorTaskSelectedMsg{task: nil}
|
||||
}
|
||||
// Find the task by description
|
||||
tasks := com.TW.GetTasks(nil, filters...)
|
||||
for _, task := range tasks {
|
||||
if task.Description == item.FilterValue() {
|
||||
return timeEditorTaskSelectedMsg{task: task}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
title := "Task"
|
||||
if project != "" {
|
||||
title = fmt.Sprintf("Task (%s)", project)
|
||||
}
|
||||
|
||||
opts := []picker.PickerOption{
|
||||
picker.WithFilterByDefault(false), // Start in list mode, not filter mode
|
||||
}
|
||||
|
||||
// Pre-select task if provided, otherwise default to "(none)"
|
||||
if defaultTask != "" {
|
||||
opts = append(opts, picker.WithDefaultValue(defaultTask))
|
||||
} else {
|
||||
opts = append(opts, picker.WithDefaultValue("(none)"))
|
||||
}
|
||||
|
||||
taskPicker := picker.New(
|
||||
com,
|
||||
title,
|
||||
taskItemProvider,
|
||||
taskOnSelect,
|
||||
opts...,
|
||||
)
|
||||
taskPicker.SetSize(50, 10)
|
||||
|
||||
return taskPicker
|
||||
}
|
||||
|
||||
func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *TimeEditorPage {
|
||||
// Extract special tags (uuid, project, track) and display tags
|
||||
uuid, selectedProject, track, displayTags := extractSpecialTags(interval.Tags)
|
||||
|
||||
// Track selected task for pre-selection
|
||||
var selectedTask *taskwarrior.Task
|
||||
var defaultTaskDescription string
|
||||
|
||||
// If UUID exists, fetch the task and add its title to display tags
|
||||
if uuid != "" {
|
||||
tasks := com.TW.GetTasks(nil, "uuid:"+uuid)
|
||||
if len(tasks) > 0 {
|
||||
taskTitle := tasks[0].Description
|
||||
selectedTask = tasks[0]
|
||||
defaultTaskDescription = selectedTask.Description
|
||||
// Add to display tags if not already present
|
||||
// Note: formatTags() will handle quoting for display, so we store the raw title
|
||||
displayTags = ensureTagPresent(displayTags, taskTitle)
|
||||
displayTags = ensureTagPresent(displayTags, defaultTaskDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +170,12 @@ func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *Time
|
||||
)
|
||||
projectPicker.SetSize(50, 10) // Compact size for inline use
|
||||
|
||||
// Create task picker (only if project is selected)
|
||||
var taskPicker *picker.Picker
|
||||
if selectedProject != "" {
|
||||
taskPicker = createTaskPickerForProject(com, selectedProject, defaultTaskDescription)
|
||||
}
|
||||
|
||||
// Create start timestamp editor
|
||||
startEditor := timestampeditor.New(com).
|
||||
Title("Start").
|
||||
@@ -118,13 +201,15 @@ func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *Time
|
||||
common: com,
|
||||
interval: interval,
|
||||
projectPicker: projectPicker,
|
||||
taskPicker: taskPicker,
|
||||
startEditor: startEditor,
|
||||
endEditor: endEditor,
|
||||
tagsInput: tagsInput,
|
||||
adjust: true, // Enable :adjust by default
|
||||
selectedProject: selectedProject,
|
||||
selectedTask: selectedTask,
|
||||
currentField: 0,
|
||||
totalFields: 5, // Now 5 fields: project, tags, start, end, adjust
|
||||
totalFields: 6, // 6 fields: project, task, tags, start, end, adjust
|
||||
uuid: uuid,
|
||||
track: track,
|
||||
}
|
||||
@@ -143,15 +228,18 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case timeEditorProjectSelectedMsg:
|
||||
// Update selected project
|
||||
p.selectedProject = msg.project
|
||||
// Blur current field (project picker)
|
||||
// Project selection happens on Enter - advance to task picker
|
||||
// (Auto-selection of project already happened in Update() switch)
|
||||
p.blurCurrentField()
|
||||
// Advance to tags field
|
||||
p.currentField = 1
|
||||
// Refresh tag autocomplete with filtered combinations
|
||||
cmds = append(cmds, p.updateTagSuggestions())
|
||||
// Focus tags input
|
||||
cmds = append(cmds, p.focusCurrentField())
|
||||
return p, tea.Batch(cmds...)
|
||||
|
||||
case timeEditorTaskSelectedMsg:
|
||||
// Task selection happens on Enter - advance to tags field
|
||||
// (Auto-selection of task already happened in Update() switch)
|
||||
p.blurCurrentField()
|
||||
p.currentField = 2
|
||||
cmds = append(cmds, p.focusCurrentField())
|
||||
return p, tea.Batch(cmds...)
|
||||
|
||||
@@ -173,6 +261,11 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
if p.currentField == 1 {
|
||||
// Task picker - let it handle Enter (will trigger taskSelectedMsg)
|
||||
break
|
||||
}
|
||||
|
||||
if p.currentField == 2 {
|
||||
// Tags field
|
||||
if p.tagsInput.HasSuggestions() {
|
||||
// Let autocomplete handle suggestion selection
|
||||
@@ -180,12 +273,12 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
// Tags confirmed without suggestions - advance to start timestamp
|
||||
p.blurCurrentField()
|
||||
p.currentField = 2
|
||||
p.currentField = 3
|
||||
cmds = append(cmds, p.focusCurrentField())
|
||||
return p, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// For all other fields (2-4: start, end, adjust), save and exit
|
||||
// For all other fields (3-5: start, end, adjust), save and exit
|
||||
p.saveInterval()
|
||||
model, err := p.common.PopPage()
|
||||
if err != nil {
|
||||
@@ -215,30 +308,116 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch p.currentField {
|
||||
case 0: // Project picker
|
||||
// Track the previous project selection
|
||||
previousProject := p.selectedProject
|
||||
|
||||
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)
|
||||
|
||||
// Check if the highlighted project changed (auto-selection)
|
||||
currentProject := p.projectPicker.GetValue()
|
||||
if currentProject != previousProject && currentProject != "" {
|
||||
// Update the selected project and refresh task picker
|
||||
p.selectedProject = currentProject
|
||||
// Clear task selection when project changes
|
||||
p.selectedTask = nil
|
||||
p.uuid = ""
|
||||
// Create/update task picker for the new project
|
||||
p.taskPicker = createTaskPickerForProject(p.common, currentProject, "")
|
||||
// Refresh tag autocomplete with filtered combinations
|
||||
cmds = append(cmds, p.updateTagSuggestions())
|
||||
}
|
||||
case 1: // Task picker
|
||||
if p.taskPicker != nil {
|
||||
// Track the previous task selection
|
||||
var previousTaskDesc string
|
||||
if p.selectedTask != nil {
|
||||
previousTaskDesc = p.selectedTask.Description
|
||||
}
|
||||
|
||||
var model tea.Model
|
||||
model, cmd = p.taskPicker.Update(msg)
|
||||
if pk, ok := model.(*picker.Picker); ok {
|
||||
p.taskPicker = pk
|
||||
}
|
||||
|
||||
// Check if the highlighted task changed (auto-selection)
|
||||
currentTaskDesc := p.taskPicker.GetValue()
|
||||
if currentTaskDesc != previousTaskDesc && currentTaskDesc != "" {
|
||||
// Handle "(none)" selection - clear task state
|
||||
if currentTaskDesc == "(none)" {
|
||||
p.selectedTask = nil
|
||||
p.uuid = ""
|
||||
p.track = ""
|
||||
// Don't clear tags - user might still want manual tags
|
||||
// Refresh tag suggestions
|
||||
cmds = append(cmds, p.updateTagSuggestions())
|
||||
} else {
|
||||
// Find and update the selected task
|
||||
filters := []string{"+track", "status:pending"}
|
||||
if p.selectedProject != "" {
|
||||
filters = append(filters, "project:"+p.selectedProject)
|
||||
}
|
||||
tasks := p.common.TW.GetTasks(nil, filters...)
|
||||
for _, task := range tasks {
|
||||
if task.Description == currentTaskDesc {
|
||||
// Update selected task
|
||||
p.selectedTask = task
|
||||
p.uuid = task.Uuid
|
||||
|
||||
// Build tags from task
|
||||
tags := []string{}
|
||||
|
||||
// Add task description
|
||||
if task.Description != "" {
|
||||
tags = append(tags, task.Description)
|
||||
}
|
||||
|
||||
// Add task tags (excluding "track" tag since it's preserved separately)
|
||||
for _, tag := range task.Tags {
|
||||
if tag != "track" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Store track tag if present
|
||||
if task.HasTag("track") {
|
||||
p.track = "track"
|
||||
}
|
||||
|
||||
// Update tags input
|
||||
p.tagsInput.SetValue(formatTags(tags))
|
||||
|
||||
// Refresh tag suggestions
|
||||
cmds = append(cmds, p.updateTagSuggestions())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case 2: // Tags
|
||||
var model tea.Model
|
||||
model, cmd = p.tagsInput.Update(msg)
|
||||
if ac, ok := model.(*autocomplete.Autocomplete); ok {
|
||||
p.tagsInput = ac
|
||||
}
|
||||
case 2: // Start (was 1)
|
||||
case 3: // Start
|
||||
var model tea.Model
|
||||
model, cmd = p.startEditor.Update(msg)
|
||||
if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
|
||||
p.startEditor = editor
|
||||
}
|
||||
case 3: // End (was 2)
|
||||
case 4: // End
|
||||
var model tea.Model
|
||||
model, cmd = p.endEditor.Update(msg)
|
||||
if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
|
||||
p.endEditor = editor
|
||||
}
|
||||
case 4: // Adjust (was 3)
|
||||
case 5: // Adjust
|
||||
// Handle adjust toggle with space/enter
|
||||
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||
if msg.String() == " " || msg.String() == "enter" {
|
||||
@@ -256,13 +435,18 @@ func (p *TimeEditorPage) focusCurrentField() tea.Cmd {
|
||||
case 0:
|
||||
return p.projectPicker.Init() // Picker doesn't have explicit Focus()
|
||||
case 1:
|
||||
if p.taskPicker != nil {
|
||||
return p.taskPicker.Init()
|
||||
}
|
||||
return nil
|
||||
case 2:
|
||||
p.tagsInput.Focus()
|
||||
return p.tagsInput.Init()
|
||||
case 2:
|
||||
return p.startEditor.Focus()
|
||||
case 3:
|
||||
return p.endEditor.Focus()
|
||||
return p.startEditor.Focus()
|
||||
case 4:
|
||||
return p.endEditor.Focus()
|
||||
case 5:
|
||||
// Adjust checkbox doesn't need focus action
|
||||
return nil
|
||||
}
|
||||
@@ -272,14 +456,16 @@ func (p *TimeEditorPage) focusCurrentField() tea.Cmd {
|
||||
func (p *TimeEditorPage) blurCurrentField() {
|
||||
switch p.currentField {
|
||||
case 0:
|
||||
// Picker doesn't have explicit Blur(), state handled by Update
|
||||
// Project picker doesn't have explicit Blur(), state handled by Update
|
||||
case 1:
|
||||
p.tagsInput.Blur()
|
||||
// Task picker doesn't have explicit Blur(), state handled by Update
|
||||
case 2:
|
||||
p.startEditor.Blur()
|
||||
p.tagsInput.Blur()
|
||||
case 3:
|
||||
p.endEditor.Blur()
|
||||
p.startEditor.Blur()
|
||||
case 4:
|
||||
p.endEditor.Blur()
|
||||
case 5:
|
||||
// Adjust checkbox doesn't need blur action
|
||||
}
|
||||
}
|
||||
@@ -304,10 +490,33 @@ func (p *TimeEditorPage) View() string {
|
||||
sections = append(sections, "")
|
||||
sections = append(sections, "")
|
||||
|
||||
// Tags input (now field 1, was first)
|
||||
// Task picker (field 1)
|
||||
if p.currentField == 1 {
|
||||
if p.taskPicker != nil {
|
||||
sections = append(sections, p.taskPicker.View())
|
||||
} else {
|
||||
// No project selected yet
|
||||
blurredLabelStyle := p.common.Styles.Form.Blurred.Title
|
||||
sections = append(sections, blurredLabelStyle.Render("Task"))
|
||||
sections = append(sections, lipgloss.NewStyle().Faint(true).Render("(select a project first)"))
|
||||
}
|
||||
} else {
|
||||
blurredLabelStyle := p.common.Styles.Form.Blurred.Title
|
||||
sections = append(sections, blurredLabelStyle.Render("Task"))
|
||||
if p.selectedTask != nil {
|
||||
sections = append(sections, lipgloss.NewStyle().Faint(true).Render(p.selectedTask.Description))
|
||||
} else {
|
||||
sections = append(sections, lipgloss.NewStyle().Faint(true).Render("(none)"))
|
||||
}
|
||||
}
|
||||
|
||||
sections = append(sections, "")
|
||||
sections = append(sections, "")
|
||||
|
||||
// Tags input (field 2)
|
||||
tagsLabelStyle := p.common.Styles.Form.Focused.Title
|
||||
tagsLabel := tagsLabelStyle.Render("Tags")
|
||||
if p.currentField == 1 { // Changed from 0
|
||||
if p.currentField == 2 {
|
||||
sections = append(sections, tagsLabel)
|
||||
sections = append(sections, p.tagsInput.View())
|
||||
descStyle := p.common.Styles.Form.Focused.Description
|
||||
@@ -321,15 +530,15 @@ func (p *TimeEditorPage) View() string {
|
||||
sections = append(sections, "")
|
||||
sections = append(sections, "")
|
||||
|
||||
// Start editor
|
||||
// Start editor (field 3)
|
||||
sections = append(sections, p.startEditor.View())
|
||||
sections = append(sections, "")
|
||||
|
||||
// End editor
|
||||
// End editor (field 4)
|
||||
sections = append(sections, p.endEditor.View())
|
||||
sections = append(sections, "")
|
||||
|
||||
// Adjust checkbox (now field 4, was 3)
|
||||
// Adjust checkbox (field 5)
|
||||
adjustLabelStyle := p.common.Styles.Form.Focused.Title
|
||||
adjustLabel := adjustLabelStyle.Render("Adjust overlaps")
|
||||
|
||||
@@ -340,7 +549,7 @@ func (p *TimeEditorPage) View() string {
|
||||
checkbox = "[ ]"
|
||||
}
|
||||
|
||||
if p.currentField == 4 { // Changed from 3
|
||||
if p.currentField == 5 {
|
||||
focusedStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
sections = append(sections, adjustLabel)
|
||||
sections = append(sections, focusedStyle.Render(checkbox+" Auto-adjust overlapping intervals"))
|
||||
@@ -357,7 +566,7 @@ func (p *TimeEditorPage) View() string {
|
||||
|
||||
// Help text
|
||||
helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
sections = append(sections, helpStyle.Render("tab/shift+tab: navigate • enter: save • esc: cancel"))
|
||||
sections = append(sections, helpStyle.Render("tab/shift+tab: navigate • enter: select/save • esc: cancel"))
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||
}
|
||||
@@ -528,7 +737,7 @@ func (p *TimeEditorPage) updateTagSuggestions() tea.Cmd {
|
||||
p.tagsInput.SetValue(currentValue)
|
||||
|
||||
// If tags field is focused, refocus it
|
||||
if p.currentField == 1 {
|
||||
if p.currentField == 2 {
|
||||
p.tagsInput.Focus()
|
||||
return p.tagsInput.Init()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user