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 }