summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/task/task.go6
-rw-r--r--internal/ui/handlers.go49
-rw-r--r--internal/ui/keyhandlers.go46
-rw-r--r--internal/ui/table.go64
-rw-r--r--internal/ui/taskdetail.go19
-rwxr-xr-xtasksamuraibin6047846 -> 6081536 bytes
6 files changed, 158 insertions, 26 deletions
diff --git a/internal/task/task.go b/internal/task/task.go
index 86c93f4..2c7f8e8 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -26,6 +26,7 @@ type Task struct {
ID int `json:"id"`
UUID string `json:"uuid"`
Description string `json:"description"`
+ Project string `json:"project"`
Tags []string `json:"tags"`
Status string `json:"status"`
Start string `json:"start"`
@@ -298,6 +299,11 @@ func SetDescription(id int, desc string) error {
return modifyTask(id, "description:"+desc)
}
+// SetProject changes the project of the task with the given id.
+func SetProject(id int, project string) error {
+ return modifyTask(id, "project:"+project)
+}
+
// Annotate adds an annotation to the task with the given id.
func Annotate(id int, text string) error {
if id <= 0 {
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go
index 576519c..1bc65c0 100644
--- a/internal/ui/handlers.go
+++ b/internal/ui/handlers.go
@@ -227,6 +227,28 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return model, cmd
}
+// handleProjectMode handles project editing
+func (m *Model) handleProjectMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
+ onEnter := func(value string) error {
+ return task.SetProject(m.projID, value)
+ }
+
+ onExit := func() {
+ m.projEditing = false
+ m.reload()
+ }
+
+ model, cmd := m.handleTextInput(msg, &m.projInput, onEnter, onExit)
+ if msg.Type == tea.KeyEnter {
+ if m.showTaskDetail {
+ // In detail view, blink the project field
+ return model, m.startDetailBlink(fieldProject) // Project field index in detail view
+ }
+ return model, m.startBlink(m.projID, false)
+ }
+ return model, cmd
+}
+
// handlePriorityMode handles priority selection
func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
@@ -477,6 +499,9 @@ func (m *Model) handleEditingModes(msg tea.KeyMsg) (handled bool, model tea.Mode
case m.recurEditing:
model, cmd = m.handleRecurrenceMode(msg)
return true, model, cmd
+ case m.projEditing:
+ model, cmd = m.handleProjectMode(msg)
+ return true, model, cmd
case m.prioritySelecting:
model, cmd = m.handlePriorityMode(msg)
return true, model, cmd
@@ -595,7 +620,7 @@ func (m *Model) handleDetailFieldEdit() (tea.Model, tea.Cmd) {
id := m.currentTaskDetail.ID
// Map detail field index to editable fields
- // fieldPriority = 3, fieldTags = 4, fieldDue = 5, fieldStart = 6, fieldRecur = 8 or 9 (depending on if fields exist)
+ // fieldPriority = 3, fieldTags = 4, fieldDue = 5, fieldStart = 6, fieldProject = 7, fieldRecur = 9 or 10 (depending on if fields exist)
// Count fields up to current position to handle dynamic fields
fieldPos := 0
@@ -666,13 +691,29 @@ func (m *Model) handleDetailFieldEdit() (tea.Model, tea.Cmd) {
}
fieldPos++
- // Entry (7)
+ // Project (7)
+ if m.detailFieldIndex == fieldPos {
+ m.clearEditingModes()
+ m.projID = id
+ m.projEditing = true
+ if m.currentTaskDetail.Project != "" {
+ m.projInput.SetValue(m.currentTaskDetail.Project)
+ } else {
+ m.projInput.SetValue("")
+ }
+ m.projInput.Focus()
+ m.updateTableHeight()
+ return m, nil
+ }
+ fieldPos++
+
+ // Entry (8)
if m.detailFieldIndex == fieldPos {
return m, nil // Not editable
}
fieldPos++
- // Recurrence (8) - only if it exists
+ // Recurrence (9) - only if it exists
if m.currentTaskDetail.Recur != "" {
if m.detailFieldIndex == fieldPos {
m.clearEditingModes()
@@ -686,7 +727,7 @@ func (m *Model) handleDetailFieldEdit() (tea.Model, tea.Cmd) {
fieldPos++
}
- // Description (9)
+ // Description (10 or 11 depending on recurrence)
if m.detailFieldIndex == fieldPos {
// Launch external editor for description
m.detailDescEditing = true
diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go
index 419bc14..204d08f 100644
--- a/internal/ui/keyhandlers.go
+++ b/internal/ui/keyhandlers.go
@@ -85,6 +85,8 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m.handleAddTask()
case "t":
return m.handleEditTags()
+ case "J":
+ return m.handleEditProject()
case "c":
return m.handleRandomTheme()
case "C":
@@ -387,6 +389,28 @@ func (m *Model) handleEditTags() (tea.Model, tea.Cmd) {
return m, nil
}
+func (m *Model) handleEditProject() (tea.Model, tea.Cmd) {
+ id, err := m.getSelectedTaskID()
+ if err != nil {
+ return m, nil
+ }
+
+ m.clearEditingModes()
+ m.projID = id
+ m.projEditing = true
+
+ // Get current project value
+ task := m.getTaskAtCursor()
+ if task != nil {
+ m.projInput.SetValue(task.Project)
+ } else {
+ m.projInput.SetValue("")
+ }
+ m.projInput.Focus()
+ m.updateTableHeight()
+ return m, nil
+}
+
func (m *Model) handleRandomTheme() (tea.Model, tea.Cmd) {
m.theme = RandomTheme()
m.applyTheme()
@@ -539,7 +563,19 @@ func (m *Model) handleEnterOrEdit() (tea.Model, tea.Cmd) {
m.updateTableHeight()
return m, nil
- case 3: // Due date
+ case 3: // Project
+ m.clearEditingModes()
+ m.projID = id
+ m.projEditing = true
+ task := m.getTaskAtCursor()
+ if task != nil {
+ m.projInput.SetValue(task.Project)
+ }
+ m.projInput.Focus()
+ m.updateTableHeight()
+ return m, nil
+
+ case 4: // Due date
m.dueID = id
task := m.getTaskAtCursor()
if task != nil && task.Due != "" {
@@ -556,7 +592,7 @@ func (m *Model) handleEnterOrEdit() (tea.Model, tea.Cmd) {
m.updateTableHeight()
return m, nil
- case 4: // Recurrence
+ case 5: // Recurrence
m.clearEditingModes()
m.recurID = id
m.recurEditing = true
@@ -568,7 +604,7 @@ func (m *Model) handleEnterOrEdit() (tea.Model, tea.Cmd) {
m.updateTableHeight()
return m, nil
- case 5: // Tags
+ case 6: // Tags
m.clearEditingModes()
m.tagsID = id
m.tagsEditing = true
@@ -577,7 +613,7 @@ func (m *Model) handleEnterOrEdit() (tea.Model, tea.Cmd) {
m.updateTableHeight()
return m, nil
- case 6: // Annotations
+ case 7: // Annotations
m.clearEditingModes()
m.annotateID = id
m.annotating = true
@@ -596,7 +632,7 @@ func (m *Model) handleEnterOrEdit() (tea.Model, tea.Cmd) {
m.updateTableHeight()
return m, nil
- case 7: // Description
+ case 8: // Description
m.clearEditingModes()
m.descID = id
m.descEditing = true
diff --git a/internal/ui/table.go b/internal/ui/table.go
index a430dcf..9def986 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -63,6 +63,10 @@ type Model struct {
recurID int
recurInput textinput.Model
+ projEditing bool
+ projID int
+ projInput textinput.Model
+
filterEditing bool
filterInput textinput.Model
@@ -113,6 +117,7 @@ type Model struct {
tagsWidth int
descWidth int
annWidth int
+ projWidth int
total int
inProgress int
@@ -215,6 +220,7 @@ func (m *Model) clearEditingModes() {
m.tagsEditing = false
m.dueEditing = false
m.recurEditing = false
+ m.projEditing = false
m.filterEditing = false
m.addingTask = false
m.searching = false
@@ -270,6 +276,8 @@ func New(filters []string, browserCmd string) (Model, error) {
m.tagsInput.Prompt = "tags: "
m.recurInput = textinput.New()
m.recurInput.Prompt = "recur: "
+ m.projInput = textinput.New()
+ m.projInput.Prompt = "project: "
m.dueDate = time.Now()
m.searchInput = textinput.New()
m.searchInput.Prompt = "search: "
@@ -296,6 +304,7 @@ func (m *Model) newTable(rows []atable.Row) (atable.Model, atable.Styles) {
{Title: "Pri", Width: m.priWidth},
{Title: "ID", Width: m.idWidth},
{Title: "Age", Width: m.ageWidth},
+ {Title: "Project", Width: m.projWidth},
{Title: "Due", Width: m.dueWidth},
{Title: "Recur", Width: m.recurWidth},
{Title: "Tags", Width: m.tagsWidth},
@@ -346,16 +355,19 @@ func (m *Model) reload() error {
for i, tsk := range tasks {
rows = append(rows, m.taskToRowSearch(tsk, m.searchRegex, m.tblStyles, -1))
if m.searchRegex != nil {
+ if m.searchRegex.MatchString(tsk.Project) {
+ m.searchMatches = append(m.searchMatches, cellMatch{row: i, col: 3})
+ }
tags := strings.Join(tsk.Tags, " ")
if m.searchRegex.MatchString(tags) {
- m.searchMatches = append(m.searchMatches, cellMatch{row: i, col: 5})
+ m.searchMatches = append(m.searchMatches, cellMatch{row: i, col: 6})
}
if m.searchRegex.MatchString(tsk.Description) {
- m.searchMatches = append(m.searchMatches, cellMatch{row: i, col: 7})
+ m.searchMatches = append(m.searchMatches, cellMatch{row: i, col: 8})
}
for _, a := range tsk.Annotations {
if m.searchRegex.MatchString(a.Description) {
- m.searchMatches = append(m.searchMatches, cellMatch{row: i, col: 6})
+ m.searchMatches = append(m.searchMatches, cellMatch{row: i, col: 7})
break
}
}
@@ -616,6 +628,12 @@ func (m Model) View() string {
m.recurInput.View(),
)
}
+ if m.projEditing {
+ view = lipgloss.JoinVertical(lipgloss.Left,
+ view,
+ m.projInput.View(),
+ )
+ }
if m.filterEditing {
view = lipgloss.JoinVertical(lipgloss.Left,
view,
@@ -688,6 +706,7 @@ func (m Model) buildHelpContent() string {
m.formatHelpLine("r", "set random due date", keyStyle, descStyle),
m.formatHelpLine("R", "edit recurrence", keyStyle, descStyle),
m.formatHelpLine("t", "edit tags", keyStyle, descStyle),
+ m.formatHelpLine("J", "edit project", keyStyle, descStyle),
m.formatHelpLine("a, A", "add/replace annotations", keyStyle, descStyle),
m.formatHelpLine("o", "open URL from description", keyStyle, descStyle),
"")
@@ -880,6 +899,7 @@ func (m Model) taskToRow(t task.Task) atable.Row {
m.formatPriority(t.Priority, m.priWidth),
style.Render(strconv.Itoa(t.ID)),
style.Render(age),
+ style.Render(t.Project),
m.formatDue(t.Due, m.dueWidth),
style.Render(recur),
style.Render(tags),
@@ -1034,22 +1054,24 @@ func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Sty
priStr := m.formatPriority(t.Priority, m.priWidth)
idStr := getStyle(1).Render(strconv.Itoa(t.ID))
ageStr := getStyle(2).Render(age)
+ projStr := m.highlightCell(getStyle(3), re, t.Project)
dueStr := m.formatDue(t.Due, m.dueWidth)
- recurStr := m.highlightCell(getStyle(4), re, recur)
- tagStr := m.highlightCell(getStyle(5), re, tags)
+ recurStr := m.highlightCell(getStyle(5), re, recur)
+ tagStr := m.highlightCell(getStyle(6), re, tags)
annRaw := strings.Join(anns, "; ")
annCount := ""
if n := len(anns); n > 0 {
annCount = strconv.FormatInt(int64(n), 16)
}
- annStr := m.highlightCellMatch(getStyle(6), re, annRaw, annCount)
- descStr := m.highlightCell(getStyle(7), re, t.Description)
- urgStr := getStyle(8).Render(m.formatUrgency(urg, m.urgWidth))
+ annStr := m.highlightCellMatch(getStyle(7), re, annRaw, annCount)
+ descStr := m.highlightCell(getStyle(8), re, t.Description)
+ urgStr := getStyle(9).Render(m.formatUrgency(urg, m.urgWidth))
return atable.Row{
priStr,
idStr,
ageStr,
+ projStr,
dueStr,
recurStr,
tagStr,
@@ -1062,7 +1084,7 @@ func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Sty
func (m Model) expandedCellView() string {
row := m.tbl.Cursor()
col := m.tbl.ColumnCursor()
- if row < 0 || row >= len(m.tasks) || col < 0 || col > 8 {
+ if row < 0 || row >= len(m.tasks) || col < 0 || col > 9 {
return ""
}
t := m.tasks[row]
@@ -1078,20 +1100,22 @@ func (m Model) expandedCellView() string {
val = fmt.Sprintf("%dd", days)
}
case 3:
- val = ansi.Strip(m.formatDue(t.Due, m.dueWidth))
+ val = t.Project
case 4:
- val = t.Recur
+ val = ansi.Strip(m.formatDue(t.Due, m.dueWidth))
case 5:
- val = strings.Join(t.Tags, " ")
+ val = t.Recur
case 6:
+ val = strings.Join(t.Tags, " ")
+ case 7:
var anns []string
for _, a := range t.Annotations {
anns = append(anns, a.Description)
}
val = strings.Join(anns, "; ")
- case 7:
- val = t.Description
case 8:
+ val = t.Description
+ case 9:
val = fmt.Sprintf("%.1f", t.Urgency)
}
header := ""
@@ -1139,7 +1163,7 @@ func (m *Model) updateTableHeight() {
if m.cellExpanded {
h--
}
- if m.annotating || m.dueEditing || m.prioritySelecting || m.searching || m.descEditing || m.tagsEditing || m.recurEditing || m.filterEditing || m.addingTask {
+ if m.annotating || m.dueEditing || m.prioritySelecting || m.searching || m.descEditing || m.tagsEditing || m.recurEditing || m.projEditing || m.filterEditing || m.addingTask {
h--
}
if h < 1 {
@@ -1177,6 +1201,7 @@ func (m *Model) computeColumnWidths() {
maxRecur := 1
maxTags := 0
maxAnn := 1
+ maxProj := 1
for _, t := range m.tasks {
if l := len(strconv.Itoa(t.ID)); l > maxID {
maxID = l
@@ -1199,6 +1224,9 @@ func (m *Model) computeColumnWidths() {
if l := len(t.Recur); l > maxRecur {
maxRecur = l
}
+ if l := len(t.Project); l > maxProj {
+ maxProj = l
+ }
tags := strings.Join(t.Tags, " ")
if l := len(tags); l > maxTags {
maxTags = l
@@ -1217,13 +1245,14 @@ func (m *Model) computeColumnWidths() {
m.recurWidth = maxRecur
m.tagsWidth = maxTags
m.annWidth = maxAnn
+ m.projWidth = maxProj
total := m.tbl.Width()
if total == 0 {
total = 80
}
- base := m.idWidth + m.priWidth + m.ageWidth + m.dueWidth + m.recurWidth + m.tagsWidth + m.annWidth + m.urgWidth
- base += 8 // spaces between columns
+ base := m.idWidth + m.priWidth + m.ageWidth + m.dueWidth + m.recurWidth + m.tagsWidth + m.annWidth + m.urgWidth + m.projWidth
+ base += 9 // spaces between columns
m.descWidth = total - base
if m.descWidth < 1 {
m.descWidth = 1
@@ -1239,6 +1268,7 @@ func (m *Model) applyColumns() {
{Title: "Pri", Width: m.priWidth},
{Title: "ID", Width: m.idWidth},
{Title: "Age", Width: m.ageWidth},
+ {Title: "Project", Width: m.projWidth},
{Title: "Due", Width: m.dueWidth},
{Title: "Recur", Width: m.recurWidth},
{Title: "Tags", Width: m.tagsWidth},
diff --git a/internal/ui/taskdetail.go b/internal/ui/taskdetail.go
index 70c06a0..c7a5b89 100644
--- a/internal/ui/taskdetail.go
+++ b/internal/ui/taskdetail.go
@@ -17,6 +17,7 @@ const (
fieldTags
fieldDue
fieldStart
+ fieldProject
fieldEntry
fieldRecur
fieldDescription
@@ -119,6 +120,24 @@ func (m *Model) renderTaskDetail() string {
currentField++
lines = append(lines, m.renderTaskFieldWithIndex("Start", m.formatTaskDate(t.Start), labelStyle, valueStyle, currentField))
currentField++
+
+ // Project
+ if m.projEditing && m.projID == t.ID {
+ // Show project editing UI without prompt
+ originalPrompt := m.projInput.Prompt
+ m.projInput.Prompt = ""
+ projView := m.projInput.View()
+ m.projInput.Prompt = originalPrompt
+ lines = append(lines, m.renderEditingField("Project", projView, labelStyle, currentField))
+ } else {
+ projectValue := t.Project
+ if projectValue == "" {
+ projectValue = "-"
+ }
+ lines = append(lines, m.renderTaskFieldWithIndex("Project", projectValue, labelStyle, valueStyle, currentField))
+ }
+ currentField++
+
lines = append(lines, m.renderTaskFieldWithIndex("Entry", m.formatTaskDate(t.Entry), labelStyle, valueStyle, currentField))
currentField++
diff --git a/tasksamurai b/tasksamurai
index 9be8bc6..8faaf07 100755
--- a/tasksamurai
+++ b/tasksamurai
Binary files differ