Move signal handling from internal/tui to pkg/core

WatchSignals and CtrlCExitWindow are generic — any frontend can use them.
Removing them from the TUI package keeps tui focused on terminal I/O only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-09 21:03:46 +02:00
parent c5b16e739e
commit dffce36dd4
5 changed files with 47 additions and 46 deletions

View File

@ -1,40 +0,0 @@
package tui
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"time"
"ollie/pkg/core"
)
const ctrlCExitWindow = 750 * time.Millisecond
func startSignalWatcher(appCancel context.CancelCauseFunc, c core.Core, errStream io.Writer) {
ch := make(chan os.Signal, 16)
signals := []os.Signal{os.Interrupt}
if haveSIGTERM {
signals = append(signals, SIGTERM)
}
signal.Notify(ch, signals...)
go func() {
for sig := range ch {
switch sig {
case SIGTERM:
signal.Stop(ch)
close(ch)
fmt.Fprint(errStream, "\n(terminated)\n")
appCancel(context.Canceled)
continue
case os.Interrupt:
if c.Interrupt(core.ErrInterrupted) {
fmt.Fprint(errStream, "\n^C\n")
}
}
}
}()
}

View File

@ -80,7 +80,7 @@ func (t *TUI) Run(ctx context.Context) {
t.split = newSplitInput(tt, tt.Output(), t.core.Prompt(), nil)
appCtx, appCancel := context.WithCancelCause(ctx)
startSignalWatcher(appCancel, t.core, os.Stderr)
core.WatchSignals(appCancel, t.core, os.Stderr)
var lastCtrlC time.Time
firstRead := true
@ -101,7 +101,7 @@ func (t *TUI) Run(ctx context.Context) {
errs := err.Error()
if errs == "interrupted" || errs == "^C" {
now := time.Now()
if !lastCtrlC.IsZero() && now.Sub(lastCtrlC) <= ctrlCExitWindow {
if !lastCtrlC.IsZero() && now.Sub(lastCtrlC) <= core.CtrlCExitWindow {
break
}
lastCtrlC = now

41
pkg/core/signals.go Normal file
View File

@ -0,0 +1,41 @@
package core
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"time"
)
const CtrlCExitWindow = 750 * time.Millisecond
// WatchSignals installs OS signal handlers for the lifetime of the program.
// SIGTERM cancels the app context; SIGINT interrupts the current agent turn.
// It is safe to call from any frontend (TUI, HTTP, CLI one-shot, etc.).
func WatchSignals(appCancel context.CancelCauseFunc, c Core, errStream io.Writer) {
ch := make(chan os.Signal, 16)
signals := []os.Signal{os.Interrupt}
if haveSIGTERM {
signals = append(signals, sigTERM)
}
signal.Notify(ch, signals...)
go func() {
for sig := range ch {
switch sig {
case sigTERM:
signal.Stop(ch)
close(ch)
fmt.Fprint(errStream, "\n(terminated)\n")
appCancel(context.Canceled)
continue
case os.Interrupt:
if c.Interrupt(ErrInterrupted) {
fmt.Fprint(errStream, "\n^C\n")
}
}
}
}()
}

View File

@ -1,10 +1,10 @@
//go:build !windows
package tui
package core
import "syscall"
const (
haveSIGTERM = true
SIGTERM = syscall.SIGTERM
sigTERM = syscall.SIGTERM
)

View File

@ -1,9 +1,9 @@
//go:build windows
package tui
package core
import "os"
const haveSIGTERM = false
var SIGTERM = os.Signal(nil)
var sigTERM = os.Signal(nil)