diff --git a/internal/ui/app.go b/internal/ui/app.go index 131352d1..0d1b0268 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -452,12 +452,12 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Task just became blocked - ring bell and show notification m.notification = fmt.Sprintf("⚠ Task #%d needs input: %s", t.ID, t.Title) m.notifyUntil = time.Now().Add(10 * time.Second) - fmt.Print("\a") // Ring terminal bell + RingBell() // Ring terminal bell (writes to /dev/tty to bypass TUI) } else if t.Status == db.StatusDone && db.IsInProgress(prevStatus) { // Task completed - ring bell and show notification m.notification = fmt.Sprintf("✓ Task #%d complete: %s", t.ID, t.Title) m.notifyUntil = time.Now().Add(5 * time.Second) - fmt.Print("\a") // Ring terminal bell + RingBell() // Ring terminal bell (writes to /dev/tty to bypass TUI) } } m.prevStatuses[t.ID] = t.Status @@ -567,11 +567,11 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if event.Task.Status == db.StatusBlocked { m.notification = fmt.Sprintf("⚠ Task #%d needs input: %s", event.TaskID, event.Task.Title) m.notifyUntil = time.Now().Add(10 * time.Second) - fmt.Print("\a") // Ring terminal bell + RingBell() // Ring terminal bell (writes to /dev/tty to bypass TUI) } else if event.Task.Status == db.StatusDone && db.IsInProgress(prevStatus) { m.notification = fmt.Sprintf("✓ Task #%d complete: %s", event.TaskID, event.Task.Title) m.notifyUntil = time.Now().Add(5 * time.Second) - fmt.Print("\a") // Ring terminal bell + RingBell() // Ring terminal bell (writes to /dev/tty to bypass TUI) } else if db.IsInProgress(event.Task.Status) { m.notification = fmt.Sprintf("▶ Task #%d started: %s", event.TaskID, event.Task.Title) m.notifyUntil = time.Now().Add(3 * time.Second) diff --git a/internal/ui/bell.go b/internal/ui/bell.go new file mode 100644 index 00000000..0548c3e5 --- /dev/null +++ b/internal/ui/bell.go @@ -0,0 +1,23 @@ +package ui + +import ( + "os" +) + +// RingBell sends the BEL character (\a) to the terminal to trigger an audible bell. +// It writes directly to /dev/tty to bypass any stdout buffering that might occur +// when running inside a TUI framework like Bubble Tea. +func RingBell() { + // Open /dev/tty directly to write to the actual terminal + // This bypasses Bubble Tea's alternate screen buffer and stdout capture + tty, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0) + if err != nil { + // Fallback to stderr if /dev/tty is not available + // (stderr is less likely to be captured than stdout) + os.Stderr.WriteString("\a") + return + } + defer tty.Close() + + tty.WriteString("\a") +}