Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SIGINT): better handle interrupts #1255

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions examples/suspend/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"fmt"
"os"

Expand All @@ -23,9 +24,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
case "q", "esc":
m.quitting = true
return m, tea.Quit
case "ctrl+c":
m.quitting = true
return m, tea.Interrupt
case "ctrl+z":
m.suspending = true
return m, tea.Suspend
Expand All @@ -39,12 +43,15 @@ func (m model) View() string {
return ""
}

return "\nPress ctrl-z to suspend, or ctrl+c to exit\n"
return "\nPress ctrl-z to suspend, ctrl+c to interrupt, q, or esc to exit\n"
}

func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Println("Error running program:", err)
if errors.Is(err, tea.ErrInterrupted) {
os.Exit(130)
}
os.Exit(1)
}
}
2 changes: 1 addition & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func WithoutRenderer() ProgramOption {
// This feature is provisional, and may be changed or removed in a future version
// of this package.
//
// Deprecated: this incurs a noticable performance hit. A future release will
// Deprecated: this incurs a noticeable performance hit. A future release will
// optimize ANSI automatically without the performance penalty.
func WithANSICompressor() ProgramOption {
return func(p *Program) {
Expand Down
46 changes: 37 additions & 9 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ import (
"golang.org/x/sync/errgroup"
)

// ErrProgramKilled is returned by [Program.Run] when the program got killed.
// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
var ErrProgramKilled = errors.New("program was killed")

// ErrInterrupted is returned by [Program.Run] when the program get a SIGINT
// signal, or when it receives a [InterruptMsg].
var ErrInterrupted = errors.New("program was interrupted")

// Msg contain data from the result of a IO operation. Msgs trigger the update
// function and, henceforth, the UI.
type Msg interface{}
Expand Down Expand Up @@ -186,8 +190,8 @@ func Quit() Msg {
return QuitMsg{}
}

// QuitMsg signals that the program should quit. You can send a QuitMsg with
// Quit.
// QuitMsg signals that the program should quit. You can send a [QuitMsg] with
// [Quit].
type QuitMsg struct{}

// Suspend is a special command that tells the Bubble Tea program to suspend.
Expand All @@ -199,13 +203,28 @@ func Suspend() Msg {
// This usually happens when ctrl+z is pressed on common programs, but since
// bubbletea puts the terminal in raw mode, we need to handle it in a
// per-program basis.
// You can send this message with Suspend.
//
// You can send this message with [Suspend()].
type SuspendMsg struct{}

// ResumeMsg can be listen to to do something once a program is resumed back
// from a suspend state.
type ResumeMsg struct{}

// InterruptMsg signals the program should suspend.
// This usually happens when ctrl+c is pressed on common programs, but since
// bubbletea puts the terminal in raw mode, we need to handle it in a
// per-program basis.
//
// You can send this message with [Interrupt()].
type InterruptMsg struct{}

// Interrupt is a special command that tells the Bubble Tea program to
// interrupt.
func Interrupt() Msg {
return InterruptMsg{}
}

// NewProgram creates a new Program.
func NewProgram(model Model, opts ...ProgramOption) *Program {
p := &Program{
Expand Down Expand Up @@ -263,9 +282,14 @@ func (p *Program) handleSignals() chan struct{} {
case <-p.ctx.Done():
return

case <-sig:
case s := <-sig:
if atomic.LoadUint32(&p.ignoreSignals) == 0 {
p.msgs <- QuitMsg{}
switch s {
case syscall.SIGINT:
p.msgs <- InterruptMsg{}
default:
p.msgs <- QuitMsg{}
}
return
}
}
Expand Down Expand Up @@ -362,6 +386,9 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
case QuitMsg:
return model, nil

case InterruptMsg:
return model, ErrInterrupted

case SuspendMsg:
if suspendSupported {
p.suspend()
Expand Down Expand Up @@ -593,10 +620,11 @@ func (p *Program) Run() (Model, error) {

// Run event loop, handle updates and draw.
model, err := p.eventLoop(model, cmds)
killed := p.ctx.Err() != nil
if killed {
killed := p.ctx.Err() != nil || err != nil
if killed && err == nil {
err = fmt.Errorf("%w: %s", ErrProgramKilled, p.ctx.Err())
} else {
}
if err == nil {
// Ensure we rendered the final state of the model.
p.renderer.write(model.View())
}
Expand Down
Loading