standard_renderer.go 20 KB

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