standard_renderer.go 17 KB

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