Skip to content

Commit

Permalink
add: program.ReleaseTerminal and RestoreTerminal to re-use input & te…
Browse files Browse the repository at this point in the history
…rminal

ReleaseTerminal makes BubbleTea release the input / terminal, so users
can spawn a sub-command. RestoreTerminal sets the input reader up again
and triggers a repaint.
  • Loading branch information
muesli committed Feb 25, 2022
1 parent 62259b7 commit 5cd97b7
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 51 deletions.
65 changes: 65 additions & 0 deletions examples/exec/main.go
@@ -0,0 +1,65 @@
package main

import (
"fmt"
"os"
"os/exec"

tea "github.com/charmbracelet/bubbletea"
)

var (
p *tea.Program
)

type model struct {
err error
}

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.KeyMsg:
switch msg.String() {
case "e":
if err := p.ReleaseTerminal(); err != nil {
m.err = err
return m, nil
}

c := exec.Command(os.Getenv("EDITOR")) //nolint:gosec
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
m.err = c.Run()

if err := p.RestoreTerminal(); err != nil {
m.err = err
}

return m, nil
case "ctrl+c", "q":
return m, tea.Quit
}
}
return m, nil
}

func (m model) View() string {
if m.err != nil {
return "Error: " + m.err.Error()
}
return "Press e to open Vim. Press q to quit."
}

func main() {
m := model{}
p = tea.NewProgram(m)
if err := p.Start(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
100 changes: 49 additions & 51 deletions tea.go
Expand Up @@ -11,7 +11,6 @@ package tea

import (
"context"
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -82,12 +81,17 @@ type Program struct {
// treated as bits. These options can be set via various ProgramOptions.
startupOptions startupOptions

ctx context.Context
mtx *sync.Mutex

msgs chan Msg
msgs chan Msg
errs chan error
readLoopDone chan struct{}

output io.Writer // where to send output. this will usually be os.Stdout.
input io.Reader // this will usually be os.Stdin.
cancelReader cancelReader

output io.Writer // where to send output. this will usually be os.Stdout.
input io.Reader // this will usually be os.Stdin.
renderer renderer
altScreenActive bool

Expand Down Expand Up @@ -263,14 +267,11 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {

// StartReturningModel initializes the program. Returns the final model.
func (p *Program) StartReturningModel() (Model, error) {
var (
cmds = make(chan Cmd)
errs = make(chan error)
)
cmds := make(chan Cmd)
p.errs = make(chan error)

// Channels for managing goroutine lifecycles.
var (
readLoopDone = make(chan struct{})
sigintLoopDone = make(chan struct{})
cmdLoopDone = make(chan struct{})
resizeLoopDone = make(chan struct{})
Expand All @@ -279,7 +280,7 @@ func (p *Program) StartReturningModel() (Model, error) {
waitForGoroutines = func(withReadLoop bool) {
if withReadLoop {
select {
case <-readLoopDone:
case <-p.readLoopDone:
case <-time.After(500 * time.Millisecond):
// The read loop hangs, which means the input
// cancelReader's cancel function has returned true even
Expand All @@ -293,7 +294,8 @@ func (p *Program) StartReturningModel() (Model, error) {
}
)

ctx, cancelContext := context.WithCancel(context.Background())
var cancelContext context.CancelFunc
p.ctx, cancelContext = context.WithCancel(context.Background())
defer cancelContext()

switch {
Expand Down Expand Up @@ -345,7 +347,7 @@ func (p *Program) StartReturningModel() (Model, error) {
}()

select {
case <-ctx.Done():
case <-p.ctx.Done():
case <-sig:
p.msgs <- quitMsg{}
}
Expand Down Expand Up @@ -390,7 +392,7 @@ func (p *Program) StartReturningModel() (Model, error) {
defer close(initSignalDone)
select {
case cmds <- initCmd:
case <-ctx.Done():
case <-p.ctx.Done():
}
}()
} else {
Expand All @@ -404,57 +406,32 @@ func (p *Program) StartReturningModel() (Model, error) {
// Render the initial view.
p.renderer.write(model.View())

cancelReader, err := newCancelReader(p.input)
if err != nil {
return model, err
}

defer cancelReader.Close() // nolint:errcheck

// Subscribe to user input.
if p.input != nil {
go func() {
defer close(readLoopDone)

for {
if ctx.Err() != nil {
return
}

msgs, err := readInputs(cancelReader)
if err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, errCanceled) {
errs <- err
}

return
}

for _, msg := range msgs {
p.msgs <- msg
}
}
}()
if err := p.initCancelReader(); err != nil {
return model, err
}
} else {
defer close(readLoopDone)
defer close(p.readLoopDone)
}
defer p.cancelReader.Close() // nolint:errcheck

if f, ok := p.output.(*os.File); ok && isatty.IsTerminal(f.Fd()) {
// Get the initial terminal size and send it to the program.
go func() {
w, h, err := term.GetSize(int(f.Fd()))
if err != nil {
errs <- err
p.errs <- err
}

select {
case <-ctx.Done():
case <-p.ctx.Done():
case p.msgs <- WindowSizeMsg{w, h}:
}
}()

// Listen for window resizes.
go listenForResize(ctx, f, p.msgs, errs, resizeLoopDone)
go listenForResize(p.ctx, f, p.msgs, p.errs, resizeLoopDone)
} else {
close(resizeLoopDone)
}
Expand All @@ -465,7 +442,7 @@ func (p *Program) StartReturningModel() (Model, error) {

for {
select {
case <-ctx.Done():
case <-p.ctx.Done():

return
case cmd := <-cmds:
Expand All @@ -481,7 +458,7 @@ func (p *Program) StartReturningModel() (Model, error) {
go func() {
select {
case p.msgs <- cmd():
case <-ctx.Done():
case <-p.ctx.Done():
}
}()
}
Expand All @@ -493,9 +470,9 @@ func (p *Program) StartReturningModel() (Model, error) {
select {
case <-p.killc:
return nil, nil
case err := <-errs:
case err := <-p.errs:
cancelContext()
waitForGoroutines(cancelReader.Cancel())
waitForGoroutines(p.cancelReader.Cancel())
p.shutdown(false)
return model, err

Expand All @@ -505,7 +482,7 @@ func (p *Program) StartReturningModel() (Model, error) {
switch msg := msg.(type) {
case quitMsg:
cancelContext()
waitForGoroutines(cancelReader.Cancel())
waitForGoroutines(p.cancelReader.Cancel())
p.shutdown(false)
return model, nil

Expand Down Expand Up @@ -690,3 +667,24 @@ func (p *Program) DisableMouseAllMotion() {
defer p.mtx.Unlock()
fmt.Fprintf(p.output, te.CSI+te.DisableMouseAllMotionSeq)
}

// ReleaseTerminal restores the original terminal state and cancels the input
// reader.
func (p *Program) ReleaseTerminal() error {
p.cancelInput()
return p.restoreTerminal()
}

// RestoreTerminal sets up the input reader & terminal state and triggers a
// repaint.
func (p *Program) RestoreTerminal() error {
if err := p.initTerminal(); err != nil {
return err
}
if err := p.initCancelReader(); err != nil {
return err
}

p.renderer.repaint()
return nil
}
45 changes: 45 additions & 0 deletions tty.go
@@ -1,5 +1,10 @@
package tea

import (
"errors"
"io"
)

func (p *Program) initTerminal() error {
err := p.initInput()
if err != nil {
Expand Down Expand Up @@ -29,3 +34,43 @@ func (p Program) restoreTerminal() error {

return p.restoreInput()
}

// initCancelReader (re)commences reading inputs.
func (p *Program) initCancelReader() error {
var err error
p.cancelReader, err = newCancelReader(p.input)
if err != nil {
return err
}

p.readLoopDone = make(chan struct{})
go func() {
defer close(p.readLoopDone)

for {
if p.ctx.Err() != nil {
return
}

msgs, err := readInputs(p.cancelReader)
if err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, errCanceled) {
p.errs <- err
}

return
}

for _, msg := range msgs {
p.msgs <- msg
}
}
}()

return nil
}

// cancelInput cancels the input reader.
func (p *Program) cancelInput() {
p.cancelReader.Cancel()
}

0 comments on commit 5cd97b7

Please sign in to comment.