github.com/elves/elvish@v0.15.0/pkg/cli/app.go (about)

     1  // Package cli implements a generic interactive line editor.
     2  package cli
     3  
     4  import (
     5  	"io"
     6  	"os"
     7  	"sync"
     8  	"syscall"
     9  
    10  	"github.com/elves/elvish/pkg/cli/term"
    11  	"github.com/elves/elvish/pkg/sys"
    12  )
    13  
    14  // App represents a CLI app.
    15  type App interface {
    16  	// MutateState mutates the state of the app.
    17  	MutateState(f func(*State))
    18  	// CopyState returns a copy of the a state.
    19  	CopyState() State
    20  	// CodeArea returns the codearea widget of the app.
    21  	CodeArea() CodeArea
    22  	// ReadCode requests the App to read code from the terminal by running an
    23  	// event loop. This function is not re-entrant.
    24  	ReadCode() (string, error)
    25  	// Redraw requests a redraw. It never blocks and can be called regardless of
    26  	// whether the App is active or not.
    27  	Redraw()
    28  	// RedrawFull requests a full redraw. It never blocks and can be called
    29  	// regardless of whether the App is active or not.
    30  	RedrawFull()
    31  	// CommitEOF causes the main loop to exit with EOF. If this method is called
    32  	// when an event is being handled, the main loop will exit after the handler
    33  	// returns.
    34  	CommitEOF()
    35  	// CommitCode causes the main loop to exit with the current code content. If
    36  	// this method is called when an event is being handled, the main loop will
    37  	// exit after the handler returns.
    38  	CommitCode()
    39  	// Notify adds a note and requests a redraw.
    40  	Notify(note string)
    41  }
    42  
    43  type app struct {
    44  	loop    *loop
    45  	reqRead chan struct{}
    46  
    47  	TTY               TTY
    48  	MaxHeight         func() int
    49  	RPromptPersistent func() bool
    50  	BeforeReadline    []func()
    51  	AfterReadline     []func(string)
    52  	Highlighter       Highlighter
    53  	Prompt            Prompt
    54  	RPrompt           Prompt
    55  
    56  	StateMutex sync.RWMutex
    57  	State      State
    58  
    59  	codeArea CodeArea
    60  }
    61  
    62  // State represents mutable state of an App.
    63  type State struct {
    64  	// Notes that have been added since the last redraw.
    65  	Notes []string
    66  	// An addon widget. When non-nil, it is shown under the codearea widget and
    67  	// terminal events are handled by it.
    68  	//
    69  	// The addon widget may implement the Focuser interface, in which case the
    70  	// Focus method is used to determine whether the cursor should be placed on
    71  	// the addon widget during each render. If the widget does not implement the
    72  	// Focuser interface, the cursor is always placed on the addon widget.
    73  	Addon Widget
    74  }
    75  
    76  // Focuser is an interface that addon widgets may implement.
    77  type Focuser interface {
    78  	Focus() bool
    79  }
    80  
    81  // NewApp creates a new App from the given specification.
    82  func NewApp(spec AppSpec) App {
    83  	lp := newLoop()
    84  	a := app{
    85  		loop:              lp,
    86  		TTY:               spec.TTY,
    87  		MaxHeight:         spec.MaxHeight,
    88  		RPromptPersistent: spec.RPromptPersistent,
    89  		BeforeReadline:    spec.BeforeReadline,
    90  		AfterReadline:     spec.AfterReadline,
    91  		Highlighter:       spec.Highlighter,
    92  		Prompt:            spec.Prompt,
    93  		RPrompt:           spec.RPrompt,
    94  		State:             spec.State,
    95  	}
    96  	if a.TTY == nil {
    97  		a.TTY = StdTTY
    98  	}
    99  	if a.MaxHeight == nil {
   100  		a.MaxHeight = func() int { return -1 }
   101  	}
   102  	if a.RPromptPersistent == nil {
   103  		a.RPromptPersistent = func() bool { return false }
   104  	}
   105  	if a.Highlighter == nil {
   106  		a.Highlighter = dummyHighlighter{}
   107  	}
   108  	if a.Prompt == nil {
   109  		a.Prompt = NewConstPrompt(nil)
   110  	}
   111  	if a.RPrompt == nil {
   112  		a.RPrompt = NewConstPrompt(nil)
   113  	}
   114  	lp.HandleCb(a.handle)
   115  	lp.RedrawCb(a.redraw)
   116  
   117  	a.codeArea = NewCodeArea(CodeAreaSpec{
   118  		OverlayHandler: spec.OverlayHandler,
   119  		Highlighter:    a.Highlighter.Get,
   120  		Prompt:         a.Prompt.Get,
   121  		RPrompt:        a.RPrompt.Get,
   122  		Abbreviations:  spec.Abbreviations,
   123  		QuotePaste:     spec.QuotePaste,
   124  		OnSubmit:       a.CommitCode,
   125  		State:          spec.CodeAreaState,
   126  
   127  		SmallWordAbbreviations: spec.SmallWordAbbreviations,
   128  	})
   129  
   130  	return &a
   131  }
   132  
   133  func (a *app) MutateState(f func(*State)) {
   134  	a.StateMutex.Lock()
   135  	defer a.StateMutex.Unlock()
   136  	f(&a.State)
   137  }
   138  
   139  func (a *app) CopyState() State {
   140  	a.StateMutex.RLock()
   141  	defer a.StateMutex.RUnlock()
   142  	return a.State
   143  }
   144  
   145  func (a *app) CodeArea() CodeArea {
   146  	return a.codeArea
   147  }
   148  
   149  func (a *app) resetAllStates() {
   150  	a.MutateState(func(s *State) { *s = State{} })
   151  	a.codeArea.MutateState(
   152  		func(s *CodeAreaState) { *s = CodeAreaState{} })
   153  }
   154  
   155  func (a *app) handle(e event) {
   156  	switch e := e.(type) {
   157  	case os.Signal:
   158  		switch e {
   159  		case syscall.SIGHUP:
   160  			a.loop.Return("", io.EOF)
   161  		case syscall.SIGINT:
   162  			a.resetAllStates()
   163  			a.triggerPrompts(true)
   164  		case sys.SIGWINCH:
   165  			a.RedrawFull()
   166  		}
   167  	case term.Event:
   168  		if listing := a.CopyState().Addon; listing != nil {
   169  			listing.Handle(e)
   170  		} else {
   171  			a.codeArea.Handle(e)
   172  		}
   173  		if !a.loop.HasReturned() {
   174  			a.triggerPrompts(false)
   175  			a.reqRead <- struct{}{}
   176  		}
   177  	}
   178  }
   179  
   180  func (a *app) triggerPrompts(force bool) {
   181  	a.Prompt.Trigger(force)
   182  	a.RPrompt.Trigger(force)
   183  }
   184  
   185  func (a *app) redraw(flag redrawFlag) {
   186  	// Get the dimensions available.
   187  	height, width := a.TTY.Size()
   188  	if maxHeight := a.MaxHeight(); maxHeight > 0 && maxHeight < height {
   189  		height = maxHeight
   190  	}
   191  
   192  	var notes []string
   193  	var addon Renderer
   194  	a.MutateState(func(s *State) {
   195  		notes, addon = s.Notes, s.Addon
   196  		s.Notes = nil
   197  	})
   198  
   199  	bufNotes := renderNotes(notes, width)
   200  	isFinalRedraw := flag&finalRedraw != 0
   201  	if isFinalRedraw {
   202  		hideRPrompt := !a.RPromptPersistent()
   203  		if hideRPrompt {
   204  			a.codeArea.MutateState(func(s *CodeAreaState) { s.HideRPrompt = true })
   205  		}
   206  		bufMain := renderApp(a.codeArea, nil /* addon */, width, height)
   207  		if hideRPrompt {
   208  			a.codeArea.MutateState(func(s *CodeAreaState) { s.HideRPrompt = false })
   209  		}
   210  		// Insert a newline after the buffer and position the cursor there.
   211  		bufMain.Extend(term.NewBuffer(width), true)
   212  
   213  		a.TTY.UpdateBuffer(bufNotes, bufMain, flag&fullRedraw != 0)
   214  		a.TTY.ResetBuffer()
   215  	} else {
   216  		bufMain := renderApp(a.codeArea, addon, width, height)
   217  		a.TTY.UpdateBuffer(bufNotes, bufMain, flag&fullRedraw != 0)
   218  	}
   219  }
   220  
   221  // Renders notes. This does not respect height so that overflow notes end up in
   222  // the scrollback buffer.
   223  func renderNotes(notes []string, width int) *term.Buffer {
   224  	if len(notes) == 0 {
   225  		return nil
   226  	}
   227  	bb := term.NewBufferBuilder(width)
   228  	for i, note := range notes {
   229  		if i > 0 {
   230  			bb.Newline()
   231  		}
   232  		bb.Write(note)
   233  	}
   234  	return bb.Buffer()
   235  }
   236  
   237  // Renders the codearea, and uses the rest of the height for the listing.
   238  func renderApp(codeArea, addon Renderer, width, height int) *term.Buffer {
   239  	buf := codeArea.Render(width, height)
   240  	if addon != nil && len(buf.Lines) < height {
   241  		bufListing := addon.Render(width, height-len(buf.Lines))
   242  		focus := true
   243  		if focuser, ok := addon.(Focuser); ok {
   244  			focus = focuser.Focus()
   245  		}
   246  		buf.Extend(bufListing, focus)
   247  	}
   248  	return buf
   249  }
   250  
   251  func (a *app) ReadCode() (string, error) {
   252  	for _, f := range a.BeforeReadline {
   253  		f()
   254  	}
   255  	defer func() {
   256  		content := a.codeArea.CopyState().Buffer.Content
   257  		for _, f := range a.AfterReadline {
   258  			f(content)
   259  		}
   260  		a.resetAllStates()
   261  	}()
   262  
   263  	restore, err := a.TTY.Setup()
   264  	if err != nil {
   265  		return "", err
   266  	}
   267  	defer restore()
   268  
   269  	var wg sync.WaitGroup
   270  	defer wg.Wait()
   271  
   272  	// Relay input events.
   273  	a.reqRead = make(chan struct{}, 1)
   274  	a.reqRead <- struct{}{}
   275  	defer close(a.reqRead)
   276  	defer a.TTY.StopInput()
   277  	wg.Add(1)
   278  	go func() {
   279  		defer wg.Done()
   280  		for range a.reqRead {
   281  			event, err := a.TTY.ReadEvent()
   282  			if err == nil {
   283  				a.loop.Input(event)
   284  			} else if err == term.ErrStopped {
   285  				return
   286  			} else if term.IsReadErrorRecoverable(err) {
   287  				a.loop.Input(term.NonfatalErrorEvent{Err: err})
   288  			} else {
   289  				a.loop.Input(term.FatalErrorEvent{Err: err})
   290  				return
   291  			}
   292  		}
   293  	}()
   294  
   295  	// Relay signals.
   296  	sigCh := a.TTY.NotifySignals()
   297  	defer a.TTY.StopSignals()
   298  	wg.Add(1)
   299  	go func() {
   300  		for sig := range sigCh {
   301  			a.loop.Input(sig)
   302  		}
   303  		wg.Done()
   304  	}()
   305  
   306  	// Relay late updates from prompt, rprompt and highlighter.
   307  	stopRelayLateUpdates := make(chan struct{})
   308  	defer close(stopRelayLateUpdates)
   309  	relayLateUpdates := func(ch <-chan struct{}) {
   310  		if ch == nil {
   311  			return
   312  		}
   313  		wg.Add(1)
   314  		go func() {
   315  			defer wg.Done()
   316  			for {
   317  				select {
   318  				case <-ch:
   319  					a.Redraw()
   320  				case <-stopRelayLateUpdates:
   321  					return
   322  				}
   323  			}
   324  		}()
   325  	}
   326  
   327  	relayLateUpdates(a.Prompt.LateUpdates())
   328  	relayLateUpdates(a.RPrompt.LateUpdates())
   329  	relayLateUpdates(a.Highlighter.LateUpdates())
   330  
   331  	// Trigger an initial prompt update.
   332  	a.triggerPrompts(true)
   333  
   334  	return a.loop.Run()
   335  }
   336  
   337  func (a *app) Redraw() {
   338  	a.loop.Redraw(false)
   339  }
   340  
   341  func (a *app) RedrawFull() {
   342  	a.loop.Redraw(true)
   343  }
   344  
   345  func (a *app) CommitEOF() {
   346  	a.loop.Return("", io.EOF)
   347  }
   348  
   349  func (a *app) CommitCode() {
   350  	code := a.codeArea.CopyState().Buffer.Content
   351  	a.loop.Return(code, nil)
   352  }
   353  
   354  func (a *app) Notify(note string) {
   355  	a.MutateState(func(s *State) { s.Notes = append(s.Notes, note) })
   356  	a.Redraw()
   357  }