Skip to content

Commit

Permalink
feat: allow to suspend bubbletea programs (#1054)
Browse files Browse the repository at this point in the history
* feat: allow to suspend bubbletea programs

closes #497
closes #1053

* fix: typo

* fix: move things around, add example

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: block until CONT

* fix: ResumeMsg

* docs: improve example

* docs: one more example

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: examples tests

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
  • Loading branch information
caarlos0 committed Jul 11, 2024
1 parent 0208ac5 commit ea13ffb
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 12 deletions.
17 changes: 14 additions & 3 deletions examples/altscreen-toggle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ var (
)

type model struct {
altscreen bool
quitting bool
altscreen bool
quitting bool
suspending bool
}

func (m model) Init() tea.Cmd {
Expand All @@ -24,11 +25,17 @@ func (m model) Init() tea.Cmd {

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.ResumeMsg:
m.suspending = false
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
m.quitting = true
return m, tea.Quit
case "ctrl+z":
m.suspending = true
return m, tea.Suspend
case " ":
var cmd tea.Cmd
if m.altscreen {
Expand All @@ -44,6 +51,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

func (m model) View() string {
if m.suspending {
return ""
}

if m.quitting {
return "Bye!\n"
}
Expand All @@ -61,7 +72,7 @@ func (m model) View() string {
}

return fmt.Sprintf("\n\n You're in %s\n\n\n", keywordStyle.Render(mode)) +
helpStyle.Render(" space: switch modes • q: exit\n")
helpStyle.Render(" space: switch modes • ctrl-z: suspend • q: exit\n")
}

func main() {
Expand Down
2 changes: 1 addition & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.1.3 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20240521172236-71f88323a7ca // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20240710172756-1a5969e3146f // indirect
github.com/charmbracelet/x/input v0.1.0 // indirect
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4u
github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU=
github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw=
github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240521172236-71f88323a7ca h1:Cw9p8EJdhDGIWICF34TIxTcQrAdzBdgkvaLA4AmqDVk=
github.com/charmbracelet/x/exp/golden v0.0.0-20240521172236-71f88323a7ca/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/golden v0.0.0-20240710172756-1a5969e3146f h1:baeZl5dM2HFldOVxnFTWkljEe26O/o4i0bMFnIu1Q2M=
github.com/charmbracelet/x/exp/golden v0.0.0-20240710172756-1a5969e3146f/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/teatest v0.0.0-20240521184646-23081fb03b28 h1:sOWKNRjt8uOEVgPiJVIJCse1+mUDM2F/vYY6W0Go640=
github.com/charmbracelet/x/exp/teatest v0.0.0-20240521184646-23081fb03b28/go.mod h1:l1w+LTJZCCozeGzMEWGxRw6Mo2DfcZUvupz8HGubdes=
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
Expand Down
12 changes: 9 additions & 3 deletions examples/simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ func (m model) Init() tea.Cmd {
// message and send back an updated model accordingly. You can also return
// a command, which is a function that performs I/O and returns a message.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m, tea.Quit
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "ctrl+z":
return m, tea.Suspend
}

case tickMsg:
m--
if m <= 0 {
Expand All @@ -59,7 +65,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View returns a string based on data in the model. That string which will be
// rendered to the terminal.
func (m model) View() string {
return fmt.Sprintf("Hi. This program will exit in %d seconds. To quit sooner press any key.\n", m)
return fmt.Sprintf("Hi. This program will exit in %d seconds.\n\nTo quit sooner press ctrl-c, or press ctrl-z to suspend...\n", m)
}

// Messages are events that we respond to in our Update function. This
Expand Down
8 changes: 5 additions & 3 deletions examples/simple/testdata/TestApp.golden
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[?25l[?2004hHi. This program will exit in 10 seconds. To quit sooner press any key
Hi. This program will exit in 9 seconds. To quit sooner press any key.
[?2004l[?25h[?1002l[?1003l[?1006l
[?25l[?2004hHi. This program will exit in 10 seconds.

To quit sooner press ctrl-c, or press ctrl-z to suspend...
Hi. This program will exit in 9 seconds.
[?2004l[?25h[?1002l[?1003l[?1006l
Expand Down
50 changes: 50 additions & 0 deletions examples/suspend/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"fmt"
"os"

tea "github.com/charmbracelet/bubbletea"
)

type model struct {
quitting bool
suspending bool
}

func (m model) Init() tea.Cmd {
return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.ResumeMsg:
m.suspending = false
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
m.quitting = true
return m, tea.Quit
case "ctrl+z":
m.suspending = true
return m, tea.Suspend
}
}
return m, nil
}

func (m model) View() string {
if m.suspending || m.quitting {
return ""
}

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

func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
21 changes: 21 additions & 0 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,22 @@ func Quit() Msg {
// Quit.
type QuitMsg struct{}

// Suspend is a special command that tells the Bubble Tea program to suspend.
func Suspend() Msg {
return SuspendMsg{}
}

// SuspendMsg signals the program should suspend.
// 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.
type SuspendMsg struct{}

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

// NewProgram creates a new Program.
func NewProgram(model Model, opts ...ProgramOption) *Program {
p := &Program{
Expand Down Expand Up @@ -327,6 +343,11 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
case QuitMsg:
return model, nil

case SuspendMsg:
if suspendSupported {
p.suspend()
}

case clearScreenMsg:
p.renderer.clearScreen()

Expand Down
12 changes: 12 additions & 0 deletions tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import (
"github.com/muesli/cancelreader"
)

func (p *Program) suspend() {
if err := p.ReleaseTerminal(); err != nil {
// If we can't release input, abort.
return
}

suspendProcess()

_ = p.RestoreTerminal()
go p.Send(ResumeMsg{})
}

func (p *Program) initTerminal() error {
if err := p.initInput(); err != nil {
return err
Expand Down
13 changes: 13 additions & 0 deletions tty_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package tea
import (
"fmt"
"os"
"os/signal"
"syscall"

"github.com/charmbracelet/x/term"
)
Expand Down Expand Up @@ -34,3 +36,14 @@ func openInputTTY() (*os.File, error) {
}
return f, nil
}

const suspendSupported = true

// Send SIGTSTP to the entire process group.
func suspendProcess() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGCONT)
_ = syscall.Kill(0, syscall.SIGTSTP)
// blocks until a CONT happens...
<-c
}
4 changes: 4 additions & 0 deletions tty_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ func openInputTTY() (*os.File, error) {
}
return f, nil
}

const suspendSupported = false

func suspendProcess() {}

0 comments on commit ea13ffb

Please sign in to comment.