// Package cli implements a generic interactive line editor.
package cli import ( ) // App represents a CLI app. type App interface { // ReadCode requests the App to read code from the terminal by running an // event loop. This function is not re-entrant. ReadCode() (string, error) // MutateState mutates the state of the app. MutateState(f func(*State)) // CopyState returns a copy of the a state. CopyState() State // CodeArea returns the codearea widget of the app. CodeArea() tk.CodeArea // SetAddon sets the current addon to the given widget. If there is an // existing addon, it is closed first. If the existing addon implements // interface{ Close(bool) }, the Close method is called with the accept // argument. To close the current addon without setting a new one, call // SetAddon(nil, accept). SetAddon(w tk.Widget, accept bool) // CommitEOF causes the main loop to exit with EOF. If this method is called // when an event is being handled, the main loop will exit after the handler // returns. CommitEOF() // CommitCode causes the main loop to exit with the current code content. If // this method is called when an event is being handled, the main loop will // exit after the handler returns. CommitCode() // Redraw requests a redraw. It never blocks and can be called regardless of // whether the App is active or not. Redraw() // RedrawFull requests a full redraw. It never blocks and can be called // regardless of whether the App is active or not. RedrawFull() // Notify adds a note and requests a redraw. Notify(note string) } type app struct { loop *loop reqRead chan struct{} TTY TTY MaxHeight func() int RPromptPersistent func() bool BeforeReadline []func() AfterReadline []func(string) Highlighter Highlighter Prompt Prompt RPrompt Prompt GlobalBindings tk.Bindings StateMutex sync.RWMutex State State codeArea tk.CodeArea } // State represents mutable state of an App. type State struct { // Notes that have been added since the last redraw. Notes []string // An addon widget. When non-nil, it is shown under the codearea widget and // terminal events are handled by it. // // The cursor is placed on the addon by default. If the addon widget // implements interface{ Focus() bool }, the Focus method is called to // determine that instead. Addon tk.Widget } // NewApp creates a new App from the given specification. func ( AppSpec) App { := newLoop() := app{ loop: , TTY: .TTY, MaxHeight: .MaxHeight, RPromptPersistent: .RPromptPersistent, BeforeReadline: .BeforeReadline, AfterReadline: .AfterReadline, Highlighter: .Highlighter, Prompt: .Prompt, RPrompt: .RPrompt, GlobalBindings: .GlobalBindings, State: .State, } if .TTY == nil { .TTY = NewTTY(os.Stdin, os.Stderr) } if .MaxHeight == nil { .MaxHeight = func() int { return -1 } } if .RPromptPersistent == nil { .RPromptPersistent = func() bool { return false } } if .Highlighter == nil { .Highlighter = dummyHighlighter{} } if .Prompt == nil { .Prompt = NewConstPrompt(nil) } if .RPrompt == nil { .RPrompt = NewConstPrompt(nil) } if .GlobalBindings == nil { .GlobalBindings = tk.DummyBindings{} } .HandleCb(.handle) .RedrawCb(.redraw) .codeArea = tk.NewCodeArea(tk.CodeAreaSpec{ Bindings: .CodeAreaBindings, Highlighter: .Highlighter.Get, Prompt: .Prompt.Get, RPrompt: .RPrompt.Get, Abbreviations: .Abbreviations, QuotePaste: .QuotePaste, OnSubmit: .CommitCode, State: .CodeAreaState, SmallWordAbbreviations: .SmallWordAbbreviations, }) return & } func ( *app) ( func(*State)) { .StateMutex.Lock() defer .StateMutex.Unlock() (&.State) } func ( *app) () State { .StateMutex.RLock() defer .StateMutex.RUnlock() return .State } func ( *app) () tk.CodeArea { return .codeArea } type closer interface { Close(bool) } func ( *app) ( tk.Widget, bool) { .StateMutex.Lock() defer .StateMutex.Unlock() if , := .State.Addon.(closer); { .Close() } .State.Addon = } func ( *app) () { .MutateState(func( *State) { * = State{} }) .codeArea.MutateState( func( *tk.CodeAreaState) { * = tk.CodeAreaState{} }) } func ( *app) ( event) { switch e := .(type) { case os.Signal: switch { case syscall.SIGHUP: .loop.Return("", io.EOF) case syscall.SIGINT: .resetAllStates() .triggerPrompts(true) case sys.SIGWINCH: .RedrawFull() } case term.Event: var tk.Widget if := .CopyState().Addon; != nil { = } else { = .codeArea } := .Handle() if ! { .GlobalBindings.Handle(, ) } if !.loop.HasReturned() { .triggerPrompts(false) .reqRead <- struct{}{} } } } func ( *app) ( bool) { .Prompt.Trigger() .RPrompt.Trigger() } func ( *app) ( redrawFlag) { // Get the dimensions available. , := .TTY.Size() if := .MaxHeight(); > 0 && < { = } var []string var tk.Renderer .MutateState(func( *State) { , = .Notes, .Addon .Notes = nil }) := renderNotes(, ) := &finalRedraw != 0 if { := !.RPromptPersistent() if { .codeArea.MutateState(func( *tk.CodeAreaState) { .HideRPrompt = true }) } := renderApp(.codeArea, nil /* addon */, , ) if { .codeArea.MutateState(func( *tk.CodeAreaState) { .HideRPrompt = false }) } // Insert a newline after the buffer and position the cursor there. .Extend(term.NewBuffer(), true) .TTY.UpdateBuffer(, , &fullRedraw != 0) .TTY.ResetBuffer() } else { := renderApp(.codeArea, , , ) .TTY.UpdateBuffer(, , &fullRedraw != 0) } } // Renders notes. This does not respect height so that overflow notes end up in // the scrollback buffer. func ( []string, int) *term.Buffer { if len() == 0 { return nil } := term.NewBufferBuilder() for , := range { if > 0 { .Newline() } .Write() } return .Buffer() } type focuser interface { Focus() bool } // Renders the codearea, and uses the rest of the height for the listing. func (, tk.Renderer, , int) *term.Buffer { := .Render(, ) if != nil && len(.Lines) < { := .Render(, -len(.Lines)) := true if , := .(focuser); { = .Focus() } .Extend(, ) } return } func ( *app) () (string, error) { for , := range .BeforeReadline { () } defer func() { := .codeArea.CopyState().Buffer.Content for , := range .AfterReadline { () } .resetAllStates() }() , := .TTY.Setup() if != nil { return "", } defer () var sync.WaitGroup defer .Wait() // Relay input events. .reqRead = make(chan struct{}, 1) .reqRead <- struct{}{} defer close(.reqRead) defer .TTY.CloseReader() .Add(1) go func() { defer .Done() for range .reqRead { , := .TTY.ReadEvent() if == nil { .loop.Input() } else if == term.ErrStopped { return } else if term.IsReadErrorRecoverable() { .loop.Input(term.NonfatalErrorEvent{Err: }) } else { .loop.Input(term.FatalErrorEvent{Err: }) return } } }() // Relay signals. := .TTY.NotifySignals() defer .TTY.StopSignals() .Add(1) go func() { for := range { .loop.Input() } .Done() }() // Relay late updates from prompt, rprompt and highlighter. := make(chan struct{}) defer close() := func( <-chan struct{}) { if == nil { return } .Add(1) go func() { defer .Done() for { select { case <-: .Redraw() case <-: return } } }() } (.Prompt.LateUpdates()) (.RPrompt.LateUpdates()) (.Highlighter.LateUpdates()) // Trigger an initial prompt update. .triggerPrompts(true) return .loop.Run() } func ( *app) () { .loop.Redraw(false) } func ( *app) () { .loop.Redraw(true) } func ( *app) () { .loop.Return("", io.EOF) } func ( *app) () { := .codeArea.CopyState().Buffer.Content .loop.Return(, nil) } func ( *app) ( string) { .MutateState(func( *State) { .Notes = append(.Notes, ) }) .Redraw() }