standard_renderer.go 17 KB

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