summaryrefslogtreecommitdiff
path: root/internal/ui/table.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-27 23:07:14 +0300
committerPaul Buetow <paul@buetow.org>2025-06-27 23:07:14 +0300
commit3654835ece8616c4aa65dc68a4ac7918c0d38d3c (patch)
tree3e17deab4f7732dffe1556b97ff12b2bb4b462e5 /internal/ui/table.go
parentcfc9e9a45cbf517a833c1fbffda6ed5068d08454 (diff)
Fix multiple bugs and improve error handling
- Fix file handle leak in SetDebugLog by properly closing previous debug files - Capture and display stderr from all taskwarrior commands for better error messages - Handle browser launch errors with status bar notifications - Add validation for task IDs to prevent negative/zero IDs - Add mutual exclusion for editing modes to prevent UI state conflicts - Add bounds checking for array access in expandedCellView - Cache compiled regular expressions for search performance - Add CLAUDE.md file with project documentation for AI assistance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal/ui/table.go')
-rw-r--r--internal/ui/table.go84
1 files changed, 73 insertions, 11 deletions
diff --git a/internal/ui/table.go b/internal/ui/table.go
index d75f350..de1776a 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -22,7 +22,10 @@ import (
var priorityOptions = []string{"H", "M", "L", ""}
-var urlRegex = regexp.MustCompile(`https?://\S+`)
+var (
+ urlRegex = regexp.MustCompile(`https?://\S+`)
+ searchRegexCache = make(map[string]*regexp.Regexp)
+)
func init() {
rand.Seed(time.Now().UnixNano())
@@ -113,6 +116,8 @@ type Model struct {
theme Theme
defaultTheme Theme
disco bool
+
+ statusMsg string
}
// editDoneMsg is emitted when the external editor process finishes.
@@ -136,6 +141,19 @@ func blinkCmd() tea.Cmd {
return tea.Tick(blinkInterval, func(time.Time) tea.Msg { return blinkMsg{} })
}
+// clearEditingModes ensures only one editing mode is active at a time
+func (m *Model) clearEditingModes() {
+ m.annotating = false
+ m.descEditing = false
+ m.tagsEditing = false
+ m.dueEditing = false
+ m.recurEditing = false
+ m.filterEditing = false
+ m.addingTask = false
+ m.searching = false
+ m.prioritySelecting = false
+}
+
func (m *Model) startBlink(id int, markDone bool) tea.Cmd {
m.blinkID = id
m.blinkMarkDone = markDone
@@ -314,6 +332,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, blinkCmd()
}
return m, nil
+ case struct{ clearStatus bool }:
+ m.statusMsg = ""
+ return m, nil
case tea.KeyMsg:
// Only allow navigation while a task row is blinking. This
// prevents accidental modifications to other tasks but still
@@ -545,9 +566,26 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.searching {
switch msg.Type {
case tea.KeyEnter:
- re, err := regexp.Compile(m.searchInput.Value())
- if err == nil {
- m.searchRegex = re
+ pattern := m.searchInput.Value()
+ if pattern != "" {
+ // Check cache first
+ if cached, ok := searchRegexCache[pattern]; ok {
+ m.searchRegex = cached
+ } else {
+ // Compile and cache if not found
+ re, err := regexp.Compile(pattern)
+ if err == nil {
+ m.searchRegex = re
+ // Limit cache size to prevent memory leak
+ if len(searchRegexCache) > 100 {
+ // Clear cache when it gets too large
+ searchRegexCache = make(map[string]*regexp.Regexp)
+ }
+ searchRegexCache[pattern] = re
+ } else {
+ m.searchRegex = nil
+ }
+ }
} else {
m.searchRegex = nil
}
@@ -638,13 +676,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "o":
if row := m.tbl.SelectedRow(); row != nil {
desc := m.tasks[m.tbl.Cursor()].Description
- re := regexp.MustCompile(`https?://\S+`)
- url := re.FindString(desc)
+ url := urlRegex.FindString(desc)
if url != "" {
- _ = exec.Command(m.browserCmd, url).Run()
- idStr := ansi.Strip(row[1])
- if id, err := strconv.Atoi(idStr); err == nil {
- return m, m.startBlink(id, false)
+ if err := exec.Command(m.browserCmd, url).Run(); err != nil {
+ // Show error in status bar
+ m.statusMsg = fmt.Sprintf("Error opening browser: %v", err)
+ // Clear status message after delay
+ cmd := tea.Tick(3*time.Second, func(time.Time) tea.Msg {
+ return struct{ clearStatus bool }{true}
+ })
+ return m, cmd
+ } else {
+ idStr := ansi.Strip(row[1])
+ if id, err := strconv.Atoi(idStr); err == nil {
+ return m, m.startBlink(id, false)
+ }
}
}
}
@@ -668,6 +714,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.tbl.SelectedRow(); row != nil {
idStr := ansi.Strip(row[1])
if id, err := strconv.Atoi(idStr); err == nil {
+ m.clearEditingModes()
m.dueID = id
m.dueEditing = true
m.dueDate = time.Now()
@@ -691,6 +738,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.tbl.SelectedRow(); row != nil {
idStr := ansi.Strip(row[1])
if id, err := strconv.Atoi(idStr); err == nil {
+ m.clearEditingModes()
m.recurID = id
m.recurEditing = true
m.recurInput.SetValue(m.tasks[m.tbl.Cursor()].Recur)
@@ -703,6 +751,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.tbl.SelectedRow(); row != nil {
idStr := ansi.Strip(row[1])
if id, err := strconv.Atoi(idStr); err == nil {
+ m.clearEditingModes()
m.priorityID = id
m.prioritySelecting = true
m.priorityIndex = 0
@@ -714,6 +763,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.tbl.SelectedRow(); row != nil {
idStr := ansi.Strip(row[1])
if id, err := strconv.Atoi(idStr); err == nil {
+ m.clearEditingModes()
m.annotateID = id
m.annotating = true
m.replaceAnnotations = false
@@ -727,6 +777,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.tbl.SelectedRow(); row != nil {
idStr := ansi.Strip(row[1])
if id, err := strconv.Atoi(idStr); err == nil {
+ m.clearEditingModes()
m.annotateID = id
m.annotating = true
m.replaceAnnotations = true
@@ -737,12 +788,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
case "f":
+ m.clearEditingModes()
m.filterEditing = true
m.filterInput.SetValue(strings.Join(m.filters, " "))
m.filterInput.Focus()
m.updateTableHeight()
return m, nil
case "+":
+ m.clearEditingModes()
m.addingTask = true
m.addInput.SetValue("")
m.addInput.Focus()
@@ -752,6 +805,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.tbl.SelectedRow(); row != nil {
idStr := ansi.Strip(row[1])
if id, err := strconv.Atoi(idStr); err == nil {
+ m.clearEditingModes()
m.tagsID = id
m.tagsEditing = true
m.tagsInput.SetValue("")
@@ -776,6 +830,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.reload()
return m, nil
case "/", "?":
+ m.clearEditingModes()
m.searching = true
m.searchInput.SetValue("")
m.searchInput.Focus()
@@ -810,6 +865,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
col := m.tbl.ColumnCursor()
switch col {
case 0:
+ m.clearEditingModes()
m.priorityID = id
m.prioritySelecting = true
switch m.tasks[m.tbl.Cursor()].Priority {
@@ -831,10 +887,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} else {
m.dueDate = time.Now()
}
+ m.clearEditingModes()
m.dueEditing = true
m.updateTableHeight()
return m, nil
case 4:
+ m.clearEditingModes()
m.recurID = id
m.recurEditing = true
m.recurInput.SetValue(m.tasks[m.tbl.Cursor()].Recur)
@@ -861,6 +919,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.updateTableHeight()
return m, nil
case 7:
+ m.clearEditingModes()
m.descID = id
m.descEditing = true
m.descInput.SetValue(m.tasks[m.tbl.Cursor()].Description)
@@ -996,6 +1055,9 @@ func (m Model) View() string {
func (m Model) statusLine() string {
status := fmt.Sprintf("Total:%d InProgress:%d Due:%d | press H for help", m.total, m.inProgress, m.due)
+ if m.statusMsg != "" {
+ status = m.statusMsg
+ }
return lipgloss.NewStyle().
Foreground(lipgloss.Color(m.theme.StatusFG)).
Background(lipgloss.Color(m.theme.StatusBG)).
@@ -1221,7 +1283,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 {
+ if row < 0 || row >= len(m.tasks) || col < 0 || col > 8 {
return ""
}
t := m.tasks[row]