tea.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. // Package tea provides a framework for building rich terminal user interfaces
  2. // based on the paradigms of The Elm Architecture. It's well-suited for simple
  3. // and complex terminal applications, either inline, full-window, or a mix of
  4. // both. It's been battle-tested in several large projects and is
  5. // production-ready.
  6. //
  7. // A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials
  8. //
  9. // Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples
  10. package tea
  11. import (
  12. "context"
  13. "errors"
  14. "fmt"
  15. "io"
  16. "os"
  17. "os/signal"
  18. "runtime/debug"
  19. "sync"
  20. "sync/atomic"
  21. "syscall"
  22. "github.com/charmbracelet/x/term"
  23. "github.com/muesli/cancelreader"
  24. "golang.org/x/sync/errgroup"
  25. )
  26. // ErrProgramKilled is returned by [Program.Run] when the program got killed.
  27. var ErrProgramKilled = errors.New("program was killed")
  28. // Msg contain data from the result of a IO operation. Msgs trigger the update
  29. // function and, henceforth, the UI.
  30. type Msg interface{}
  31. // Model contains the program's state as well as its core functions.
  32. type Model interface {
  33. // Init is the first function that will be called. It returns an optional
  34. // initial command. To not perform an initial command return nil.
  35. Init() Cmd
  36. // Update is called when a message is received. Use it to inspect messages
  37. // and, in response, update the model and/or send a command.
  38. Update(Msg) (Model, Cmd)
  39. // View renders the program's UI, which is just a string. The view is
  40. // rendered after every Update.
  41. View() string
  42. }
  43. // Cmd is an IO operation that returns a message when it's complete. If it's
  44. // nil it's considered a no-op. Use it for things like HTTP requests, timers,
  45. // saving and loading from disk, and so on.
  46. //
  47. // Note that there's almost never a reason to use a command to send a message
  48. // to another part of your program. That can almost always be done in the
  49. // update function.
  50. type Cmd func() Msg
  51. type inputType int
  52. const (
  53. defaultInput inputType = iota
  54. ttyInput
  55. customInput
  56. )
  57. // String implements the stringer interface for [inputType]. It is inteded to
  58. // be used in testing.
  59. func (i inputType) String() string {
  60. return [...]string{
  61. "default input",
  62. "tty input",
  63. "custom input",
  64. }[i]
  65. }
  66. // Options to customize the program during its initialization. These are
  67. // generally set with ProgramOptions.
  68. //
  69. // The options here are treated as bits.
  70. type startupOptions int16
  71. func (s startupOptions) has(option startupOptions) bool {
  72. return s&option != 0
  73. }
  74. const (
  75. withAltScreen startupOptions = 1 << iota
  76. withMouseCellMotion
  77. withMouseAllMotion
  78. withANSICompressor
  79. withoutSignalHandler
  80. // Catching panics is incredibly useful for restoring the terminal to a
  81. // usable state after a panic occurs. When this is set, Bubble Tea will
  82. // recover from panics, print the stack trace, and disable raw mode. This
  83. // feature is on by default.
  84. withoutCatchPanics
  85. withoutBracketedPaste
  86. withReportFocus
  87. )
  88. // channelHandlers manages the series of channels returned by various processes.
  89. // It allows us to wait for those processes to terminate before exiting the
  90. // program.
  91. type channelHandlers []chan struct{}
  92. // Adds a channel to the list of handlers. We wait for all handlers to terminate
  93. // gracefully on shutdown.
  94. func (h *channelHandlers) add(ch chan struct{}) {
  95. *h = append(*h, ch)
  96. }
  97. // shutdown waits for all handlers to terminate.
  98. func (h channelHandlers) shutdown() {
  99. var wg sync.WaitGroup
  100. for _, ch := range h {
  101. wg.Add(1)
  102. go func(ch chan struct{}) {
  103. <-ch
  104. wg.Done()
  105. }(ch)
  106. }
  107. wg.Wait()
  108. }
  109. // Program is a terminal user interface.
  110. type Program struct {
  111. initialModel Model
  112. // handlers is a list of channels that need to be waited on before the
  113. // program can exit.
  114. handlers channelHandlers
  115. // Configuration options that will set as the program is initializing,
  116. // treated as bits. These options can be set via various ProgramOptions.
  117. startupOptions startupOptions
  118. // startupTitle is the title that will be set on the terminal when the
  119. // program starts.
  120. startupTitle string
  121. inputType inputType
  122. ctx context.Context
  123. cancel context.CancelFunc
  124. msgs chan Msg
  125. errs chan error
  126. finished chan struct{}
  127. // where to send output, this will usually be os.Stdout.
  128. output io.Writer
  129. // ttyOutput is null if output is not a TTY.
  130. ttyOutput term.File
  131. previousOutputState *term.State
  132. renderer renderer
  133. // the environment variables for the program, defaults to os.Environ().
  134. environ []string
  135. // where to read inputs from, this will usually be os.Stdin.
  136. input io.Reader
  137. // ttyInput is null if input is not a TTY.
  138. ttyInput term.File
  139. previousTtyInputState *term.State
  140. cancelReader cancelreader.CancelReader
  141. readLoopDone chan struct{}
  142. // was the altscreen active before releasing the terminal?
  143. altScreenWasActive bool
  144. ignoreSignals uint32
  145. bpWasActive bool // was the bracketed paste mode active before releasing the terminal?
  146. reportFocus bool // was focus reporting active before releasing the terminal?
  147. filter func(Model, Msg) Msg
  148. // fps is the frames per second we should set on the renderer, if
  149. // applicable,
  150. fps int
  151. }
  152. // Quit is a special command that tells the Bubble Tea program to exit.
  153. func Quit() Msg {
  154. return QuitMsg{}
  155. }
  156. // QuitMsg signals that the program should quit. You can send a QuitMsg with
  157. // Quit.
  158. type QuitMsg struct{}
  159. // Suspend is a special command that tells the Bubble Tea program to suspend.
  160. func Suspend() Msg {
  161. return SuspendMsg{}
  162. }
  163. // SuspendMsg signals the program should suspend.
  164. // This usually happens when ctrl+z is pressed on common programs, but since
  165. // bubbletea puts the terminal in raw mode, we need to handle it in a
  166. // per-program basis.
  167. // You can send this message with Suspend.
  168. type SuspendMsg struct{}
  169. // ResumeMsg can be listen to to do something once a program is resumed back
  170. // from a suspend state.
  171. type ResumeMsg struct{}
  172. // NewProgram creates a new Program.
  173. func NewProgram(model Model, opts ...ProgramOption) *Program {
  174. p := &Program{
  175. initialModel: model,
  176. msgs: make(chan Msg),
  177. }
  178. // Apply all options to the program.
  179. for _, opt := range opts {
  180. opt(p)
  181. }
  182. // A context can be provided with a ProgramOption, but if none was provided
  183. // we'll use the default background context.
  184. if p.ctx == nil {
  185. p.ctx = context.Background()
  186. }
  187. // Initialize context and teardown channel.
  188. p.ctx, p.cancel = context.WithCancel(p.ctx)
  189. // if no output was set, set it to stdout
  190. if p.output == nil {
  191. p.output = os.Stdout
  192. }
  193. // if no environment was set, set it to os.Environ()
  194. if p.environ == nil {
  195. p.environ = os.Environ()
  196. }
  197. return p
  198. }
  199. func (p *Program) handleSignals() chan struct{} {
  200. ch := make(chan struct{})
  201. // Listen for SIGINT and SIGTERM.
  202. //
  203. // In most cases ^C will not send an interrupt because the terminal will be
  204. // in raw mode and ^C will be captured as a keystroke and sent along to
  205. // Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be
  206. // caught here.
  207. //
  208. // SIGTERM is sent by unix utilities (like kill) to terminate a process.
  209. go func() {
  210. sig := make(chan os.Signal, 1)
  211. signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
  212. defer func() {
  213. signal.Stop(sig)
  214. close(ch)
  215. }()
  216. for {
  217. select {
  218. case <-p.ctx.Done():
  219. return
  220. case <-sig:
  221. if atomic.LoadUint32(&p.ignoreSignals) == 0 {
  222. p.msgs <- QuitMsg{}
  223. return
  224. }
  225. }
  226. }
  227. }()
  228. return ch
  229. }
  230. // handleResize handles terminal resize events.
  231. func (p *Program) handleResize() chan struct{} {
  232. ch := make(chan struct{})
  233. if p.ttyOutput != nil {
  234. // Get the initial terminal size and send it to the program.
  235. go p.checkResize()
  236. // Listen for window resizes.
  237. go p.listenForResize(ch)
  238. } else {
  239. close(ch)
  240. }
  241. return ch
  242. }
  243. // handleCommands runs commands in a goroutine and sends the result to the
  244. // program's message channel.
  245. func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
  246. ch := make(chan struct{})
  247. go func() {
  248. defer close(ch)
  249. for {
  250. select {
  251. case <-p.ctx.Done():
  252. return
  253. case cmd := <-cmds:
  254. if cmd == nil {
  255. continue
  256. }
  257. // Don't wait on these goroutines, otherwise the shutdown
  258. // latency would get too large as a Cmd can run for some time
  259. // (e.g. tick commands that sleep for half a second). It's not
  260. // possible to cancel them so we'll have to leak the goroutine
  261. // until Cmd returns.
  262. go func() {
  263. // Recover from panics.
  264. if !p.startupOptions.has(withoutCatchPanics) {
  265. defer p.recoverFromPanic()
  266. }
  267. msg := cmd() // this can be long.
  268. p.Send(msg)
  269. }()
  270. }
  271. }
  272. }()
  273. return ch
  274. }
  275. func (p *Program) disableMouse() {
  276. p.renderer.disableMouseCellMotion()
  277. p.renderer.disableMouseAllMotion()
  278. p.renderer.disableMouseSGRMode()
  279. }
  280. // eventLoop is the central message loop. It receives and handles the default
  281. // Bubble Tea messages, update the model and triggers redraws.
  282. func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
  283. for {
  284. select {
  285. case <-p.ctx.Done():
  286. return model, nil
  287. case err := <-p.errs:
  288. return model, err
  289. case msg := <-p.msgs:
  290. // Filter messages.
  291. if p.filter != nil {
  292. msg = p.filter(model, msg)
  293. }
  294. if msg == nil {
  295. continue
  296. }
  297. // Handle special internal messages.
  298. switch msg := msg.(type) {
  299. case QuitMsg:
  300. return model, nil
  301. case SuspendMsg:
  302. if suspendSupported {
  303. p.suspend()
  304. }
  305. case clearScreenMsg:
  306. p.renderer.clearScreen()
  307. case enterAltScreenMsg:
  308. p.renderer.enterAltScreen()
  309. case exitAltScreenMsg:
  310. p.renderer.exitAltScreen()
  311. case enableMouseCellMotionMsg, enableMouseAllMotionMsg:
  312. switch msg.(type) {
  313. case enableMouseCellMotionMsg:
  314. p.renderer.enableMouseCellMotion()
  315. case enableMouseAllMotionMsg:
  316. p.renderer.enableMouseAllMotion()
  317. }
  318. // mouse mode (1006) is a no-op if the terminal doesn't support it.
  319. p.renderer.enableMouseSGRMode()
  320. case disableMouseMsg:
  321. p.disableMouse()
  322. case showCursorMsg:
  323. p.renderer.showCursor()
  324. case hideCursorMsg:
  325. p.renderer.hideCursor()
  326. case enableBracketedPasteMsg:
  327. p.renderer.enableBracketedPaste()
  328. case disableBracketedPasteMsg:
  329. p.renderer.disableBracketedPaste()
  330. case enableReportFocusMsg:
  331. p.renderer.enableReportFocus()
  332. case disableReportFocusMsg:
  333. p.renderer.disableReportFocus()
  334. case execMsg:
  335. // NB: this blocks.
  336. p.exec(msg.cmd, msg.fn)
  337. case BatchMsg:
  338. for _, cmd := range msg {
  339. cmds <- cmd
  340. }
  341. continue
  342. case sequenceMsg:
  343. go func() {
  344. // Execute commands one at a time, in order.
  345. for _, cmd := range msg {
  346. if cmd == nil {
  347. continue
  348. }
  349. msg := cmd()
  350. if batchMsg, ok := msg.(BatchMsg); ok {
  351. g, _ := errgroup.WithContext(p.ctx)
  352. for _, cmd := range batchMsg {
  353. cmd := cmd
  354. g.Go(func() error {
  355. p.Send(cmd())
  356. return nil
  357. })
  358. }
  359. //nolint:errcheck
  360. g.Wait() // wait for all commands from batch msg to finish
  361. continue
  362. }
  363. p.Send(msg)
  364. }
  365. }()
  366. case setWindowTitleMsg:
  367. p.SetWindowTitle(string(msg))
  368. case windowSizeMsg:
  369. go p.checkResize()
  370. }
  371. // Process internal messages for the renderer.
  372. if r, ok := p.renderer.(*standardRenderer); ok {
  373. r.handleMessages(msg)
  374. }
  375. var cmd Cmd
  376. model, cmd = model.Update(msg) // run update
  377. cmds <- cmd // process command (if any)
  378. p.renderer.write(model.View()) // send view to renderer
  379. }
  380. }
  381. }
  382. // Run initializes the program and runs its event loops, blocking until it gets
  383. // terminated by either [Program.Quit], [Program.Kill], or its signal handler.
  384. // Returns the final model.
  385. func (p *Program) Run() (Model, error) {
  386. p.handlers = channelHandlers{}
  387. cmds := make(chan Cmd)
  388. p.errs = make(chan error)
  389. p.finished = make(chan struct{}, 1)
  390. defer p.cancel()
  391. switch p.inputType {
  392. case defaultInput:
  393. p.input = os.Stdin
  394. // The user has not set a custom input, so we need to check whether or
  395. // not standard input is a terminal. If it's not, we open a new TTY for
  396. // input. This will allow things to "just work" in cases where data was
  397. // piped in or redirected to the application.
  398. //
  399. // To disable input entirely pass nil to the [WithInput] program option.
  400. f, isFile := p.input.(term.File)
  401. if !isFile {
  402. break
  403. }
  404. if term.IsTerminal(f.Fd()) {
  405. break
  406. }
  407. f, err := openInputTTY()
  408. if err != nil {
  409. return p.initialModel, err
  410. }
  411. defer f.Close() //nolint:errcheck
  412. p.input = f
  413. case ttyInput:
  414. // Open a new TTY, by request
  415. f, err := openInputTTY()
  416. if err != nil {
  417. return p.initialModel, err
  418. }
  419. defer f.Close() //nolint:errcheck
  420. p.input = f
  421. case customInput:
  422. // (There is nothing extra to do.)
  423. }
  424. // Handle signals.
  425. if !p.startupOptions.has(withoutSignalHandler) {
  426. p.handlers.add(p.handleSignals())
  427. }
  428. // Recover from panics.
  429. if !p.startupOptions.has(withoutCatchPanics) {
  430. defer p.recoverFromPanic()
  431. }
  432. // If no renderer is set use the standard one.
  433. if p.renderer == nil {
  434. p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps)
  435. }
  436. // Check if output is a TTY before entering raw mode, hiding the cursor and
  437. // so on.
  438. if err := p.initTerminal(); err != nil {
  439. return p.initialModel, err
  440. }
  441. // Honor program startup options.
  442. if p.startupTitle != "" {
  443. p.renderer.setWindowTitle(p.startupTitle)
  444. }
  445. if p.startupOptions&withAltScreen != 0 {
  446. p.renderer.enterAltScreen()
  447. }
  448. if p.startupOptions&withoutBracketedPaste == 0 {
  449. p.renderer.enableBracketedPaste()
  450. }
  451. if p.startupOptions&withMouseCellMotion != 0 {
  452. p.renderer.enableMouseCellMotion()
  453. p.renderer.enableMouseSGRMode()
  454. } else if p.startupOptions&withMouseAllMotion != 0 {
  455. p.renderer.enableMouseAllMotion()
  456. p.renderer.enableMouseSGRMode()
  457. }
  458. if p.startupOptions&withReportFocus != 0 {
  459. p.renderer.enableReportFocus()
  460. }
  461. // Start the renderer.
  462. p.renderer.start()
  463. // Initialize the program.
  464. model := p.initialModel
  465. if initCmd := model.Init(); initCmd != nil {
  466. ch := make(chan struct{})
  467. p.handlers.add(ch)
  468. go func() {
  469. defer close(ch)
  470. select {
  471. case cmds <- initCmd:
  472. case <-p.ctx.Done():
  473. }
  474. }()
  475. }
  476. // Render the initial view.
  477. p.renderer.write(model.View())
  478. // Subscribe to user input.
  479. if p.input != nil {
  480. if err := p.initCancelReader(); err != nil {
  481. return model, err
  482. }
  483. }
  484. // Handle resize events.
  485. p.handlers.add(p.handleResize())
  486. // Process commands.
  487. p.handlers.add(p.handleCommands(cmds))
  488. // Run event loop, handle updates and draw.
  489. model, err := p.eventLoop(model, cmds)
  490. killed := p.ctx.Err() != nil
  491. if killed {
  492. err = fmt.Errorf("%w: %s", ErrProgramKilled, p.ctx.Err())
  493. } else {
  494. // Ensure we rendered the final state of the model.
  495. p.renderer.write(model.View())
  496. }
  497. // Restore terminal state.
  498. p.shutdown(killed)
  499. return model, err
  500. }
  501. // StartReturningModel initializes the program and runs its event loops,
  502. // blocking until it gets terminated by either [Program.Quit], [Program.Kill],
  503. // or its signal handler. Returns the final model.
  504. //
  505. // Deprecated: please use [Program.Run] instead.
  506. func (p *Program) StartReturningModel() (Model, error) {
  507. return p.Run()
  508. }
  509. // Start initializes the program and runs its event loops, blocking until it
  510. // gets terminated by either [Program.Quit], [Program.Kill], or its signal
  511. // handler.
  512. //
  513. // Deprecated: please use [Program.Run] instead.
  514. func (p *Program) Start() error {
  515. _, err := p.Run()
  516. return err
  517. }
  518. // Send sends a message to the main update function, effectively allowing
  519. // messages to be injected from outside the program for interoperability
  520. // purposes.
  521. //
  522. // If the program hasn't started yet this will be a blocking operation.
  523. // If the program has already been terminated this will be a no-op, so it's safe
  524. // to send messages after the program has exited.
  525. func (p *Program) Send(msg Msg) {
  526. select {
  527. case <-p.ctx.Done():
  528. case p.msgs <- msg:
  529. }
  530. }
  531. // Quit is a convenience function for quitting Bubble Tea programs. Use it
  532. // when you need to shut down a Bubble Tea program from the outside.
  533. //
  534. // If you wish to quit from within a Bubble Tea program use the Quit command.
  535. //
  536. // If the program is not running this will be a no-op, so it's safe to call
  537. // if the program is unstarted or has already exited.
  538. func (p *Program) Quit() {
  539. p.Send(Quit())
  540. }
  541. // Kill stops the program immediately and restores the former terminal state.
  542. // The final render that you would normally see when quitting will be skipped.
  543. // [program.Run] returns a [ErrProgramKilled] error.
  544. func (p *Program) Kill() {
  545. p.shutdown(true)
  546. }
  547. // Wait waits/blocks until the underlying Program finished shutting down.
  548. func (p *Program) Wait() {
  549. <-p.finished
  550. }
  551. // shutdown performs operations to free up resources and restore the terminal
  552. // to its original state.
  553. func (p *Program) shutdown(kill bool) {
  554. p.cancel()
  555. // Wait for all handlers to finish.
  556. p.handlers.shutdown()
  557. // Check if the cancel reader has been setup before waiting and closing.
  558. if p.cancelReader != nil {
  559. // Wait for input loop to finish.
  560. if p.cancelReader.Cancel() {
  561. if !kill {
  562. p.waitForReadLoop()
  563. }
  564. }
  565. _ = p.cancelReader.Close()
  566. }
  567. if p.renderer != nil {
  568. if kill {
  569. p.renderer.kill()
  570. } else {
  571. p.renderer.stop()
  572. }
  573. }
  574. _ = p.restoreTerminalState()
  575. if !kill {
  576. p.finished <- struct{}{}
  577. }
  578. }
  579. // recoverFromPanic recovers from a panic, prints the stack trace, and restores
  580. // the terminal to a usable state.
  581. func (p *Program) recoverFromPanic() {
  582. if r := recover(); r != nil {
  583. p.shutdown(true)
  584. fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
  585. debug.PrintStack()
  586. }
  587. }
  588. // ReleaseTerminal restores the original terminal state and cancels the input
  589. // reader. You can return control to the Program with RestoreTerminal.
  590. func (p *Program) ReleaseTerminal() error {
  591. atomic.StoreUint32(&p.ignoreSignals, 1)
  592. if p.cancelReader != nil {
  593. p.cancelReader.Cancel()
  594. }
  595. p.waitForReadLoop()
  596. if p.renderer != nil {
  597. p.renderer.stop()
  598. p.altScreenWasActive = p.renderer.altScreen()
  599. p.bpWasActive = p.renderer.bracketedPasteActive()
  600. p.reportFocus = p.renderer.reportFocus()
  601. }
  602. return p.restoreTerminalState()
  603. }
  604. // RestoreTerminal reinitializes the Program's input reader, restores the
  605. // terminal to the former state when the program was running, and repaints.
  606. // Use it to reinitialize a Program after running ReleaseTerminal.
  607. func (p *Program) RestoreTerminal() error {
  608. atomic.StoreUint32(&p.ignoreSignals, 0)
  609. if err := p.initTerminal(); err != nil {
  610. return err
  611. }
  612. if err := p.initCancelReader(); err != nil {
  613. return err
  614. }
  615. if p.altScreenWasActive {
  616. p.renderer.enterAltScreen()
  617. } else {
  618. // entering alt screen already causes a repaint.
  619. go p.Send(repaintMsg{})
  620. }
  621. if p.renderer != nil {
  622. p.renderer.start()
  623. }
  624. if p.bpWasActive {
  625. p.renderer.enableBracketedPaste()
  626. }
  627. if p.reportFocus {
  628. p.renderer.enableReportFocus()
  629. }
  630. // If the output is a terminal, it may have been resized while another
  631. // process was at the foreground, in which case we may not have received
  632. // SIGWINCH. Detect any size change now and propagate the new size as
  633. // needed.
  634. go p.checkResize()
  635. return nil
  636. }
  637. // Println prints above the Program. This output is unmanaged by the program
  638. // and will persist across renders by the Program.
  639. //
  640. // If the altscreen is active no output will be printed.
  641. func (p *Program) Println(args ...interface{}) {
  642. p.msgs <- printLineMessage{
  643. messageBody: fmt.Sprint(args...),
  644. }
  645. }
  646. // Printf prints above the Program. It takes a format template followed by
  647. // values similar to fmt.Printf. This output is unmanaged by the program and
  648. // will persist across renders by the Program.
  649. //
  650. // Unlike fmt.Printf (but similar to log.Printf) the message will be print on
  651. // its own line.
  652. //
  653. // If the altscreen is active no output will be printed.
  654. func (p *Program) Printf(template string, args ...interface{}) {
  655. p.msgs <- printLineMessage{
  656. messageBody: fmt.Sprintf(template, args...),
  657. }
  658. }