standard_renderer.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. package tea
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/charmbracelet/x/ansi"
  10. "github.com/muesli/ansi/compressor"
  11. )
  12. const (
  13. // defaultFramerate specifies the maximum interval at which we should
  14. // update the view.
  15. defaultFPS = 60
  16. maxFPS = 120
  17. )
  18. // standardRenderer is a framerate-based terminal renderer, updating the view
  19. // at a given framerate to avoid overloading the terminal emulator.
  20. //
  21. // In cases where very high performance is needed the renderer can be told
  22. // to exclude ranges of lines, allowing them to be written to directly.
  23. type standardRenderer struct {
  24. mtx *sync.Mutex
  25. out io.Writer
  26. buf bytes.Buffer
  27. queuedMessageLines []string
  28. framerate time.Duration
  29. ticker *time.Ticker
  30. done chan struct{}
  31. lastRender string
  32. linesRendered int
  33. useANSICompressor bool
  34. once sync.Once
  35. // cursor visibility state
  36. cursorHidden bool
  37. // essentially whether or not we're using the full size of the terminal
  38. altScreenActive bool
  39. // whether or not we're currently using bracketed paste
  40. bpActive bool
  41. // renderer dimensions; usually the size of the window
  42. width int
  43. height int
  44. // lines explicitly set not to render
  45. ignoreLines map[int]struct{}
  46. }
  47. // newRenderer creates a new renderer. Normally you'll want to initialize it
  48. // with os.Stdout as the first argument.
  49. func newRenderer(out io.Writer, useANSICompressor bool, fps int) renderer {
  50. if fps < 1 {
  51. fps = defaultFPS
  52. } else if fps > maxFPS {
  53. fps = maxFPS
  54. }
  55. r := &standardRenderer{
  56. out: out,
  57. mtx: &sync.Mutex{},
  58. done: make(chan struct{}),
  59. framerate: time.Second / time.Duration(fps),
  60. useANSICompressor: useANSICompressor,
  61. queuedMessageLines: []string{},
  62. }
  63. if r.useANSICompressor {
  64. r.out = &compressor.Writer{Forward: out}
  65. }
  66. return r
  67. }
  68. // start starts the renderer.
  69. func (r *standardRenderer) start() {
  70. if r.ticker == nil {
  71. r.ticker = time.NewTicker(r.framerate)
  72. } else {
  73. // If the ticker already exists, it has been stopped and we need to
  74. // reset it.
  75. r.ticker.Reset(r.framerate)
  76. }
  77. // Since the renderer can be restarted after a stop, we need to reset
  78. // the done channel and its corresponding sync.Once.
  79. r.once = sync.Once{}
  80. go r.listen()
  81. }
  82. // stop permanently halts the renderer, rendering the final frame.
  83. func (r *standardRenderer) stop() {
  84. // Stop the renderer before acquiring the mutex to avoid a deadlock.
  85. r.once.Do(func() {
  86. r.done <- struct{}{}
  87. })
  88. // flush locks the mutex
  89. r.flush()
  90. r.mtx.Lock()
  91. defer r.mtx.Unlock()
  92. r.execute(ansi.EraseEntireLine)
  93. if r.useANSICompressor {
  94. if w, ok := r.out.(io.WriteCloser); ok {
  95. _ = w.Close()
  96. }
  97. }
  98. }
  99. // execute writes a sequence to the terminal.
  100. func (r *standardRenderer) execute(seq string) {
  101. _, _ = io.WriteString(r.out, seq)
  102. }
  103. // kill halts the renderer. The final frame will not be rendered.
  104. func (r *standardRenderer) kill() {
  105. // Stop the renderer before acquiring the mutex to avoid a deadlock.
  106. r.once.Do(func() {
  107. r.done <- struct{}{}
  108. })
  109. r.mtx.Lock()
  110. defer r.mtx.Unlock()
  111. r.execute(ansi.EraseEntireLine)
  112. }
  113. // listen waits for ticks on the ticker, or a signal to stop the renderer.
  114. func (r *standardRenderer) listen() {
  115. for {
  116. select {
  117. case <-r.done:
  118. r.ticker.Stop()
  119. return
  120. case <-r.ticker.C:
  121. r.flush()
  122. }
  123. }
  124. }
  125. // flush renders the buffer.
  126. func (r *standardRenderer) flush() {
  127. r.mtx.Lock()
  128. defer r.mtx.Unlock()
  129. if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
  130. // Nothing to do
  131. return
  132. }
  133. // Output buffer
  134. buf := &bytes.Buffer{}
  135. newLines := strings.Split(r.buf.String(), "\n")
  136. // If we know the output's height, we can use it to determine how many
  137. // lines we can render. We drop lines from the top of the render buffer if
  138. // necessary, as we can't navigate the cursor into the terminal's scrollback
  139. // buffer.
  140. if r.height > 0 && len(newLines) > r.height {
  141. newLines = newLines[len(newLines)-r.height:]
  142. }
  143. numLinesThisFlush := len(newLines)
  144. oldLines := strings.Split(r.lastRender, "\n")
  145. skipLines := make(map[int]struct{})
  146. flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
  147. // Clear any lines we painted in the last render.
  148. if r.linesRendered > 0 {
  149. for i := r.linesRendered - 1; i > 0; i-- {
  150. // if we are clearing queued messages, we want to clear all lines, since
  151. // printing messages allows for native terminal word-wrap, we
  152. // don't have control over the queued lines
  153. if flushQueuedMessages {
  154. buf.WriteString(ansi.EraseEntireLine)
  155. } else if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) {
  156. // If the number of lines we want to render hasn't increased and
  157. // new line is the same as the old line we can skip rendering for
  158. // this line as a performance optimization.
  159. skipLines[i] = struct{}{}
  160. } else if _, exists := r.ignoreLines[i]; !exists {
  161. buf.WriteString(ansi.EraseEntireLine)
  162. }
  163. buf.WriteString(ansi.CursorUp1)
  164. }
  165. if _, exists := r.ignoreLines[0]; !exists {
  166. // We need to return to the start of the line here to properly
  167. // erase it. Going back the entire width of the terminal will
  168. // usually be farther than we need to go, but terminal emulators
  169. // will stop the cursor at the start of the line as a rule.
  170. //
  171. // We use this sequence in particular because it's part of the ANSI
  172. // standard (whereas others are proprietary to, say, VT100/VT52).
  173. // If cursor previous line (ESC[ + <n> + F) were better supported
  174. // we could use that above to eliminate this step.
  175. buf.WriteString(ansi.CursorLeft(r.width))
  176. buf.WriteString(ansi.EraseEntireLine)
  177. }
  178. }
  179. // Merge the set of lines we're skipping as a rendering optimization with
  180. // the set of lines we've explicitly asked the renderer to ignore.
  181. for k, v := range r.ignoreLines {
  182. skipLines[k] = v
  183. }
  184. if flushQueuedMessages {
  185. // Dump the lines we've queued up for printing
  186. for _, line := range r.queuedMessageLines {
  187. _, _ = buf.WriteString(line)
  188. _, _ = buf.WriteString("\r\n")
  189. }
  190. // clear the queued message lines
  191. r.queuedMessageLines = []string{}
  192. }
  193. // Paint new lines
  194. for i := 0; i < len(newLines); i++ {
  195. if _, skip := skipLines[i]; skip {
  196. // Unless this is the last line, move the cursor down.
  197. if i < len(newLines)-1 {
  198. buf.WriteString(ansi.CursorDown1)
  199. }
  200. } else {
  201. if i == 0 && r.lastRender == "" {
  202. // On first render, reset the cursor to the start of the line
  203. // before writing anything.
  204. buf.WriteByte('\r')
  205. }
  206. line := newLines[i]
  207. // Truncate lines wider than the width of the window to avoid
  208. // wrapping, which will mess up rendering. If we don't have the
  209. // width of the window this will be ignored.
  210. //
  211. // Note that on Windows we only get the width of the window on
  212. // program initialization, so after a resize this won't perform
  213. // correctly (signal SIGWINCH is not supported on Windows).
  214. if r.width > 0 {
  215. line = ansi.Truncate(line, r.width, "")
  216. }
  217. _, _ = buf.WriteString(line)
  218. if i < len(newLines)-1 {
  219. _, _ = buf.WriteString("\r\n")
  220. }
  221. }
  222. }
  223. r.linesRendered = numLinesThisFlush
  224. // Make sure the cursor is at the start of the last line to keep rendering
  225. // behavior consistent.
  226. if r.altScreenActive {
  227. // This case fixes a bug in macOS terminal. In other terminals the
  228. // other case seems to do the job regardless of whether or not we're
  229. // using the full terminal window.
  230. buf.WriteString(ansi.MoveCursor(r.linesRendered, 0))
  231. } else {
  232. buf.WriteString(ansi.CursorLeft(r.width))
  233. }
  234. _, _ = r.out.Write(buf.Bytes())
  235. r.lastRender = r.buf.String()
  236. r.buf.Reset()
  237. }
  238. // write writes to the internal buffer. The buffer will be outputted via the
  239. // ticker which calls flush().
  240. func (r *standardRenderer) write(s string) {
  241. r.mtx.Lock()
  242. defer r.mtx.Unlock()
  243. r.buf.Reset()
  244. // If an empty string was passed we should clear existing output and
  245. // rendering nothing. Rather than introduce additional state to manage
  246. // this, we render a single space as a simple (albeit less correct)
  247. // solution.
  248. if s == "" {
  249. s = " "
  250. }
  251. _, _ = r.buf.WriteString(s)
  252. }
  253. func (r *standardRenderer) repaint() {
  254. r.lastRender = ""
  255. }
  256. func (r *standardRenderer) clearScreen() {
  257. r.mtx.Lock()
  258. defer r.mtx.Unlock()
  259. r.execute(ansi.EraseEntireDisplay)
  260. r.execute(ansi.MoveCursorOrigin)
  261. r.repaint()
  262. }
  263. func (r *standardRenderer) altScreen() bool {
  264. r.mtx.Lock()
  265. defer r.mtx.Unlock()
  266. return r.altScreenActive
  267. }
  268. func (r *standardRenderer) enterAltScreen() {
  269. r.mtx.Lock()
  270. defer r.mtx.Unlock()
  271. if r.altScreenActive {
  272. return
  273. }
  274. r.altScreenActive = true
  275. r.execute(ansi.EnableAltScreenBuffer)
  276. // Ensure that the terminal is cleared, even when it doesn't support
  277. // alt screen (or alt screen support is disabled, like GNU screen by
  278. // default).
  279. //
  280. // Note: we can't use r.clearScreen() here because the mutex is already
  281. // locked.
  282. r.execute(ansi.EraseEntireDisplay)
  283. r.execute(ansi.MoveCursorOrigin)
  284. // cmd.exe and other terminals keep separate cursor states for the AltScreen
  285. // and the main buffer. We have to explicitly reset the cursor visibility
  286. // whenever we enter AltScreen.
  287. if r.cursorHidden {
  288. r.execute(ansi.HideCursor)
  289. } else {
  290. r.execute(ansi.ShowCursor)
  291. }
  292. r.repaint()
  293. }
  294. func (r *standardRenderer) exitAltScreen() {
  295. r.mtx.Lock()
  296. defer r.mtx.Unlock()
  297. if !r.altScreenActive {
  298. return
  299. }
  300. r.altScreenActive = false
  301. r.execute(ansi.DisableAltScreenBuffer)
  302. // cmd.exe and other terminals keep separate cursor states for the AltScreen
  303. // and the main buffer. We have to explicitly reset the cursor visibility
  304. // whenever we exit AltScreen.
  305. if r.cursorHidden {
  306. r.execute(ansi.HideCursor)
  307. } else {
  308. r.execute(ansi.ShowCursor)
  309. }
  310. r.repaint()
  311. }
  312. func (r *standardRenderer) showCursor() {
  313. r.mtx.Lock()
  314. defer r.mtx.Unlock()
  315. r.cursorHidden = false
  316. r.execute(ansi.ShowCursor)
  317. }
  318. func (r *standardRenderer) hideCursor() {
  319. r.mtx.Lock()
  320. defer r.mtx.Unlock()
  321. r.cursorHidden = true
  322. r.execute(ansi.HideCursor)
  323. }
  324. func (r *standardRenderer) enableMouseCellMotion() {
  325. r.mtx.Lock()
  326. defer r.mtx.Unlock()
  327. r.execute(ansi.EnableMouseCellMotion)
  328. }
  329. func (r *standardRenderer) disableMouseCellMotion() {
  330. r.mtx.Lock()
  331. defer r.mtx.Unlock()
  332. r.execute(ansi.DisableMouseCellMotion)
  333. }
  334. func (r *standardRenderer) enableMouseAllMotion() {
  335. r.mtx.Lock()
  336. defer r.mtx.Unlock()
  337. r.execute(ansi.EnableMouseAllMotion)
  338. }
  339. func (r *standardRenderer) disableMouseAllMotion() {
  340. r.mtx.Lock()
  341. defer r.mtx.Unlock()
  342. r.execute(ansi.DisableMouseAllMotion)
  343. }
  344. func (r *standardRenderer) enableMouseSGRMode() {
  345. r.mtx.Lock()
  346. defer r.mtx.Unlock()
  347. r.execute(ansi.EnableMouseSgrExt)
  348. }
  349. func (r *standardRenderer) disableMouseSGRMode() {
  350. r.mtx.Lock()
  351. defer r.mtx.Unlock()
  352. r.execute(ansi.DisableMouseSgrExt)
  353. }
  354. func (r *standardRenderer) enableBracketedPaste() {
  355. r.mtx.Lock()
  356. defer r.mtx.Unlock()
  357. r.execute(ansi.EnableBracketedPaste)
  358. r.bpActive = true
  359. }
  360. func (r *standardRenderer) disableBracketedPaste() {
  361. r.mtx.Lock()
  362. defer r.mtx.Unlock()
  363. r.execute(ansi.DisableBracketedPaste)
  364. r.bpActive = false
  365. }
  366. func (r *standardRenderer) bracketedPasteActive() bool {
  367. r.mtx.Lock()
  368. defer r.mtx.Unlock()
  369. return r.bpActive
  370. }
  371. // setWindowTitle sets the terminal window title.
  372. func (r *standardRenderer) setWindowTitle(title string) {
  373. r.execute(ansi.SetWindowTitle(title))
  374. }
  375. // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
  376. // renderer.
  377. func (r *standardRenderer) setIgnoredLines(from int, to int) {
  378. // Lock if we're going to be clearing some lines since we don't want
  379. // anything jacking our cursor.
  380. if r.linesRendered > 0 {
  381. r.mtx.Lock()
  382. defer r.mtx.Unlock()
  383. }
  384. if r.ignoreLines == nil {
  385. r.ignoreLines = make(map[int]struct{})
  386. }
  387. for i := from; i < to; i++ {
  388. r.ignoreLines[i] = struct{}{}
  389. }
  390. // Erase ignored lines
  391. if r.linesRendered > 0 {
  392. buf := &bytes.Buffer{}
  393. for i := r.linesRendered - 1; i >= 0; i-- {
  394. if _, exists := r.ignoreLines[i]; exists {
  395. buf.WriteString(ansi.EraseEntireLine)
  396. }
  397. buf.WriteString(ansi.CursorUp1)
  398. }
  399. buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) // put cursor back
  400. _, _ = r.out.Write(buf.Bytes())
  401. }
  402. }
  403. // clearIgnoredLines returns control of any ignored lines to the standard
  404. // Bubble Tea renderer. That is, any lines previously set to be ignored can be
  405. // rendered to again.
  406. func (r *standardRenderer) clearIgnoredLines() {
  407. r.ignoreLines = nil
  408. }
  409. // insertTop effectively scrolls up. It inserts lines at the top of a given
  410. // area designated to be a scrollable region, pushing everything else down.
  411. // This is roughly how ncurses does it.
  412. //
  413. // To call this function use command ScrollUp().
  414. //
  415. // For this to work renderer.ignoreLines must be set to ignore the scrollable
  416. // region since we are bypassing the normal Bubble Tea renderer here.
  417. //
  418. // Because this method relies on the terminal dimensions, it's only valid for
  419. // full-window applications (generally those that use the alternate screen
  420. // buffer).
  421. //
  422. // This method bypasses the normal rendering buffer and is philosophically
  423. // different than the normal way we approach rendering in Bubble Tea. It's for
  424. // use in high-performance rendering, such as a pager that could potentially
  425. // be rendering very complicated ansi. In cases where the content is simpler
  426. // standard Bubble Tea rendering should suffice.
  427. func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) {
  428. r.mtx.Lock()
  429. defer r.mtx.Unlock()
  430. buf := &bytes.Buffer{}
  431. buf.WriteString(ansi.SetScrollingRegion(topBoundary, bottomBoundary))
  432. buf.WriteString(ansi.MoveCursor(topBoundary, 0))
  433. buf.WriteString(ansi.InsertLine(len(lines)))
  434. _, _ = buf.WriteString(strings.Join(lines, "\r\n"))
  435. buf.WriteString(ansi.SetScrollingRegion(0, r.height))
  436. // Move cursor back to where the main rendering routine expects it to be
  437. buf.WriteString(ansi.MoveCursor(r.linesRendered, 0))
  438. _, _ = r.out.Write(buf.Bytes())
  439. }
  440. // insertBottom effectively scrolls down. It inserts lines at the bottom of
  441. // a given area designated to be a scrollable region, pushing everything else
  442. // up. This is roughly how ncurses does it.
  443. //
  444. // To call this function use the command ScrollDown().
  445. //
  446. // See note in insertTop() for caveats, how this function only makes sense for
  447. // full-window applications, and how it differs from the normal way we do
  448. // rendering in Bubble Tea.
  449. func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) {
  450. r.mtx.Lock()
  451. defer r.mtx.Unlock()
  452. buf := &bytes.Buffer{}
  453. buf.WriteString(ansi.SetScrollingRegion(topBoundary, bottomBoundary))
  454. buf.WriteString(ansi.MoveCursor(bottomBoundary, 0))
  455. _, _ = buf.WriteString("\r\n" + strings.Join(lines, "\r\n"))
  456. buf.WriteString(ansi.SetScrollingRegion(0, r.height))
  457. // Move cursor back to where the main rendering routine expects it to be
  458. buf.WriteString(ansi.MoveCursor(r.linesRendered, 0))
  459. _, _ = r.out.Write(buf.Bytes())
  460. }
  461. // handleMessages handles internal messages for the renderer.
  462. func (r *standardRenderer) handleMessages(msg Msg) {
  463. switch msg := msg.(type) {
  464. case repaintMsg:
  465. // Force a repaint by clearing the render cache as we slide into a
  466. // render.
  467. r.mtx.Lock()
  468. r.repaint()
  469. r.mtx.Unlock()
  470. case WindowSizeMsg:
  471. r.mtx.Lock()
  472. r.width = msg.Width
  473. r.height = msg.Height
  474. r.repaint()
  475. r.mtx.Unlock()
  476. case clearScrollAreaMsg:
  477. r.clearIgnoredLines()
  478. // Force a repaint on the area where the scrollable stuff was in this
  479. // update cycle
  480. r.mtx.Lock()
  481. r.repaint()
  482. r.mtx.Unlock()
  483. case syncScrollAreaMsg:
  484. // Re-render scrolling area
  485. r.clearIgnoredLines()
  486. r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary)
  487. r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
  488. // Force non-scrolling stuff to repaint in this update cycle
  489. r.mtx.Lock()
  490. r.repaint()
  491. r.mtx.Unlock()
  492. case scrollUpMsg:
  493. r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
  494. case scrollDownMsg:
  495. r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary)
  496. case printLineMessage:
  497. if !r.altScreenActive {
  498. lines := strings.Split(msg.messageBody, "\n")
  499. r.mtx.Lock()
  500. r.queuedMessageLines = append(r.queuedMessageLines, lines...)
  501. r.repaint()
  502. r.mtx.Unlock()
  503. }
  504. }
  505. }
  506. // HIGH-PERFORMANCE RENDERING STUFF
  507. type syncScrollAreaMsg struct {
  508. lines []string
  509. topBoundary int
  510. bottomBoundary int
  511. }
  512. // SyncScrollArea performs a paint of the entire region designated to be the
  513. // scrollable area. This is required to initialize the scrollable region and
  514. // should also be called on resize (WindowSizeMsg).
  515. //
  516. // For high-performance, scroll-based rendering only.
  517. func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd {
  518. return func() Msg {
  519. return syncScrollAreaMsg{
  520. lines: lines,
  521. topBoundary: topBoundary,
  522. bottomBoundary: bottomBoundary,
  523. }
  524. }
  525. }
  526. type clearScrollAreaMsg struct{}
  527. // ClearScrollArea deallocates the scrollable region and returns the control of
  528. // those lines to the main rendering routine.
  529. //
  530. // For high-performance, scroll-based rendering only.
  531. func ClearScrollArea() Msg {
  532. return clearScrollAreaMsg{}
  533. }
  534. type scrollUpMsg struct {
  535. lines []string
  536. topBoundary int
  537. bottomBoundary int
  538. }
  539. // ScrollUp adds lines to the top of the scrollable region, pushing existing
  540. // lines below down. Lines that are pushed out the scrollable region disappear
  541. // from view.
  542. //
  543. // For high-performance, scroll-based rendering only.
  544. func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd {
  545. return func() Msg {
  546. return scrollUpMsg{
  547. lines: newLines,
  548. topBoundary: topBoundary,
  549. bottomBoundary: bottomBoundary,
  550. }
  551. }
  552. }
  553. type scrollDownMsg struct {
  554. lines []string
  555. topBoundary int
  556. bottomBoundary int
  557. }
  558. // ScrollDown adds lines to the bottom of the scrollable region, pushing
  559. // existing lines above up. Lines that are pushed out of the scrollable region
  560. // disappear from view.
  561. //
  562. // For high-performance, scroll-based rendering only.
  563. func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
  564. return func() Msg {
  565. return scrollDownMsg{
  566. lines: newLines,
  567. topBoundary: topBoundary,
  568. bottomBoundary: bottomBoundary,
  569. }
  570. }
  571. }
  572. type printLineMessage struct {
  573. messageBody string
  574. }
  575. // Println prints above the Program. This output is unmanaged by the program and
  576. // will persist across renders by the Program.
  577. //
  578. // Unlike fmt.Println (but similar to log.Println) the message will be print on
  579. // its own line.
  580. //
  581. // If the altscreen is active no output will be printed.
  582. func Println(args ...interface{}) Cmd {
  583. return func() Msg {
  584. return printLineMessage{
  585. messageBody: fmt.Sprint(args...),
  586. }
  587. }
  588. }
  589. // Printf prints above the Program. It takes a format template followed by
  590. // values similar to fmt.Printf. This output is unmanaged by the program and
  591. // will persist across renders by the Program.
  592. //
  593. // Unlike fmt.Printf (but similar to log.Printf) the message will be print on
  594. // its own line.
  595. //
  596. // If the altscreen is active no output will be printed.
  597. func Printf(template string, args ...interface{}) Cmd {
  598. return func() Msg {
  599. return printLineMessage{
  600. messageBody: fmt.Sprintf(template, args...),
  601. }
  602. }
  603. }