fatty 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +2 -0
  3. data/.simplecov +23 -0
  4. data/.yardopts +4 -0
  5. data/CHANGELOG.md +34 -0
  6. data/CHANGELOG.org +38 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +31 -0
  9. data/README.org +166 -0
  10. data/Rakefile +15 -0
  11. data/TODO.org +163 -0
  12. data/examples/markdown/native-markdown.md +370 -0
  13. data/examples/markdown/ox-gfm-markdown.md +373 -0
  14. data/examples/markdown/ox-gfm-markdown.org +376 -0
  15. data/exe/fatty +275 -0
  16. data/fatty.gemspec +42 -0
  17. data/lib/fatty/accept_env.rb +32 -0
  18. data/lib/fatty/action.rb +103 -0
  19. data/lib/fatty/action_environment.rb +42 -0
  20. data/lib/fatty/actionable.rb +73 -0
  21. data/lib/fatty/alert.rb +93 -0
  22. data/lib/fatty/ansi/renderer.rb +168 -0
  23. data/lib/fatty/ansi.rb +352 -0
  24. data/lib/fatty/colors/color.rb +379 -0
  25. data/lib/fatty/colors/pairs.rb +73 -0
  26. data/lib/fatty/colors/palette.rb +73 -0
  27. data/lib/fatty/colors/rgb.txt +788 -0
  28. data/lib/fatty/colors.rb +5 -0
  29. data/lib/fatty/config.rb +86 -0
  30. data/lib/fatty/config_files/config.yml +50 -0
  31. data/lib/fatty/config_files/help.md +120 -0
  32. data/lib/fatty/config_files/help.org +124 -0
  33. data/lib/fatty/config_files/keybindings.yml +49 -0
  34. data/lib/fatty/config_files/keydefs.yml +23 -0
  35. data/lib/fatty/config_files/themes/mono.yml +76 -0
  36. data/lib/fatty/config_files/themes/nordic.yml +77 -0
  37. data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
  38. data/lib/fatty/config_files/themes/terminal.yml +90 -0
  39. data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
  40. data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
  41. data/lib/fatty/core_ext/string.rb +21 -0
  42. data/lib/fatty/core_ext.rb +3 -0
  43. data/lib/fatty/counter.rb +81 -0
  44. data/lib/fatty/curses/context.rb +279 -0
  45. data/lib/fatty/curses/curses_coder.rb +684 -0
  46. data/lib/fatty/curses/event_source.rb +230 -0
  47. data/lib/fatty/curses/key_decoder.rb +183 -0
  48. data/lib/fatty/curses/patch.rb +116 -0
  49. data/lib/fatty/curses/window_styling.rb +32 -0
  50. data/lib/fatty/curses.rb +16 -0
  51. data/lib/fatty/env.rb +100 -0
  52. data/lib/fatty/help.rb +41 -0
  53. data/lib/fatty/history/entry.rb +71 -0
  54. data/lib/fatty/history.rb +289 -0
  55. data/lib/fatty/input_buffer.rb +998 -0
  56. data/lib/fatty/input_field.rb +507 -0
  57. data/lib/fatty/key_event.rb +342 -0
  58. data/lib/fatty/key_map.rb +392 -0
  59. data/lib/fatty/keymaps/emacs.rb +189 -0
  60. data/lib/fatty/log_formats/json.rb +47 -0
  61. data/lib/fatty/log_formats/text.rb +67 -0
  62. data/lib/fatty/logger.rb +142 -0
  63. data/lib/fatty/markdown/ansi_renderer.rb +373 -0
  64. data/lib/fatty/markdown/render.rb +22 -0
  65. data/lib/fatty/markdown.rb +4 -0
  66. data/lib/fatty/menu_env.rb +22 -0
  67. data/lib/fatty/mouse_event.rb +32 -0
  68. data/lib/fatty/output_buffer.rb +78 -0
  69. data/lib/fatty/pager.rb +801 -0
  70. data/lib/fatty/prompt.rb +40 -0
  71. data/lib/fatty/renderer/curses.rb +697 -0
  72. data/lib/fatty/renderer/truecolor.rb +607 -0
  73. data/lib/fatty/renderer.rb +419 -0
  74. data/lib/fatty/screen.rb +96 -0
  75. data/lib/fatty/search.rb +43 -0
  76. data/lib/fatty/session/alert_session.rb +52 -0
  77. data/lib/fatty/session/input_session.rb +99 -0
  78. data/lib/fatty/session/isearch_session.rb +172 -0
  79. data/lib/fatty/session/keytest_session.rb +236 -0
  80. data/lib/fatty/session/modal_session.rb +61 -0
  81. data/lib/fatty/session/output_session.rb +105 -0
  82. data/lib/fatty/session/popup_session.rb +540 -0
  83. data/lib/fatty/session/prompt_session.rb +157 -0
  84. data/lib/fatty/session/search_session.rb +136 -0
  85. data/lib/fatty/session/shell_session.rb +566 -0
  86. data/lib/fatty/session.rb +173 -0
  87. data/lib/fatty/sessions.rb +14 -0
  88. data/lib/fatty/terminal/popup_owner.rb +26 -0
  89. data/lib/fatty/terminal/progress.rb +374 -0
  90. data/lib/fatty/terminal.rb +1067 -0
  91. data/lib/fatty/themes/loader.rb +136 -0
  92. data/lib/fatty/themes/manager.rb +71 -0
  93. data/lib/fatty/themes/registry.rb +64 -0
  94. data/lib/fatty/themes/resolver.rb +224 -0
  95. data/lib/fatty/themes/themes.rb +131 -0
  96. data/lib/fatty/themes.rb +6 -0
  97. data/lib/fatty/version.rb +5 -0
  98. data/lib/fatty/view/alert_view.rb +14 -0
  99. data/lib/fatty/view/cursor_view.rb +18 -0
  100. data/lib/fatty/view/input_view.rb +9 -0
  101. data/lib/fatty/view/output_view.rb +9 -0
  102. data/lib/fatty/view/status_view.rb +14 -0
  103. data/lib/fatty/view.rb +33 -0
  104. data/lib/fatty/viewport.rb +90 -0
  105. data/lib/fatty/views.rb +9 -0
  106. data/lib/fatty.rb +55 -0
  107. data/sig/fatty.rbs +4 -0
  108. metadata +250 -0
@@ -0,0 +1,607 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class Renderer
5
+ class Truecolor < Fatty::Renderer
6
+ include Fatty::Curses::WindowStyling
7
+
8
+ def initialize(...)
9
+ super
10
+ @ansi_renderer = Fatty::Ansi::Renderer.new
11
+ @pending_ansi_draws = []
12
+ end
13
+
14
+ def render_status(text, role: :status_info)
15
+ state = status_state(text, role)
16
+ return if state == @last_status_state
17
+
18
+ @last_status_state = state
19
+
20
+ lines = status_render_lines(
21
+ text,
22
+ width: screen.status_rect.cols,
23
+ max_rows: screen.status_rect.rows,
24
+ )
25
+
26
+ screen.status_rect.rows.times do |idx|
27
+ queue_ansi_line(
28
+ row: screen.status_rect.row + idx,
29
+ col: screen.status_rect.col,
30
+ width: screen.status_rect.cols,
31
+ text: lines[idx].to_s,
32
+ role: role,
33
+ )
34
+ end
35
+ end
36
+
37
+ def render_alert(alert)
38
+ state = alert_state(alert)
39
+ return if state == @last_alert_state
40
+
41
+ @last_alert_state = state
42
+ text = alert ? alert.format : ""
43
+ role = alert ? alert.role : :alert_info
44
+
45
+ queue_ansi_line(
46
+ row: screen.alert_rect.row,
47
+ col: screen.alert_rect.col,
48
+ width: screen.alert_rect.cols,
49
+ text: text,
50
+ role: role,
51
+ )
52
+ end
53
+
54
+ def render_output(output, viewport:, highlights: nil)
55
+ lines = viewport.slice(output.lines)
56
+ normalized = normalized_highlights(highlights)
57
+
58
+ curr = output_state(
59
+ viewport: viewport,
60
+ lines: lines,
61
+ highlights: normalized,
62
+ )
63
+ return if curr == @last_output_state
64
+
65
+ draw_output_lines(lines, viewport: viewport, highlights: normalized)
66
+ @last_output_state = curr
67
+ end
68
+
69
+ def render_input_field(field)
70
+ state = input_field_state(field)
71
+ return if state == @last_input_state
72
+
73
+ @last_input_state = state
74
+ queue_ansi_segments_line(
75
+ row: screen.input_rect.row,
76
+ col: screen.input_rect.col,
77
+ width: screen.input_rect.cols,
78
+ segments: field_segments(
79
+ field,
80
+ base_role: :input,
81
+ suggestion_role: :input_suggestion,
82
+ region_role: :region,
83
+ ),
84
+ fill_role: :input,
85
+ )
86
+ end
87
+
88
+ def render_pager_field(field, row:, role: :pager_status)
89
+ state = pager_field_state(field, row: row, role: role)
90
+ return if state == @last_pager_field_state
91
+
92
+ @last_pager_field_state = state
93
+
94
+ win = context.output_win
95
+ return unless win
96
+
97
+ row0, col0 = win.origin
98
+ return unless row0 && col0
99
+
100
+ cols = win.respond_to?(:maxx) ? win.maxx : @screen.cols
101
+
102
+ queue_ansi_segments_line(
103
+ row: row0 + row,
104
+ col: col0,
105
+ width: cols,
106
+ segments: field_segments(
107
+ field,
108
+ base_role: role,
109
+ suggestion_role: :input_suggestion,
110
+ region_role: :region,
111
+ ),
112
+ fill_role: role,
113
+ )
114
+ end
115
+
116
+ def render_popup(session:)
117
+ state = popup_state(session)
118
+ return if state == @last_popup_state
119
+
120
+ @last_popup_state = state
121
+
122
+ win = session.win
123
+ return unless win
124
+
125
+ row0, col0 = win.origin
126
+ return unless row0 && col0
127
+
128
+ width = win.maxx
129
+ height = win.maxy
130
+ inner_w = [width - 2, 0].max
131
+ inner_h = [height - 2, 0].max
132
+
133
+ # Draw the window with the border
134
+ render_popup_frame(session: session)
135
+
136
+ # Add any title
137
+ render_popup_title(session: session) if session.title
138
+
139
+ # Draw the message if any inside the window
140
+ layout = PopupLayout.new(row: 0, width: inner_w)
141
+ row = render_popup_message(session: session, layout: layout)
142
+
143
+ counts_present = !!session.counts
144
+ input_row = inner_h - 1
145
+ counts_row = counts_present ? input_row - 1 : nil
146
+
147
+ # Draw the displayed items inside the window with a gutter to have an
148
+ # indicator of what is selected, if any.
149
+ list_h = [inner_h - row - 1 - (counts_present ? 1 : 0), 0].max
150
+ layout = PopupLayout.new(row: row, width: inner_w, height: list_h)
151
+ render_popup_items(session: session, layout: layout)
152
+
153
+ # Draw the "counts" line to indicate the total number of items, items
154
+ # selected, and items displayed.
155
+ layout = PopupLayout.new(row: counts_row, width: inner_w)
156
+ render_popup_counts(session: session, layout: layout) if counts_present
157
+
158
+ # Draw the input field for the user to type narrowing selection
159
+ # queries.
160
+ layout = PopupLayout.new(row: input_row, width: inner_w)
161
+ render_popup_input_field(session: session, layout: layout)
162
+ end
163
+
164
+ def render_prompt_popup(session:)
165
+ state = prompt_popup_state(session)
166
+ return if state == @last_prompt_popup_state
167
+
168
+ @last_prompt_popup_state = state
169
+
170
+ win = session.win
171
+ return unless win
172
+
173
+ row0, col0 = win.origin
174
+ return unless row0 && col0
175
+
176
+ inner_w = [win.maxx - 2, 0].max
177
+ inner_h = [win.maxy - 2, 0].max
178
+ return if inner_w <= 0 || inner_h <= 0
179
+
180
+ render_popup_frame(session: session)
181
+ render_popup_title(session: session) if session.title
182
+
183
+ layout = PopupLayout.new(row: 0, width: inner_w)
184
+ row = render_popup_message(session: session, layout: layout)
185
+
186
+ layout = PopupLayout.new(row: row, width: inner_w)
187
+ render_popup_input_field(session: session, layout: layout)
188
+ rescue RuntimeError => e
189
+ raise unless e.message.include?("closed window") ||
190
+ e.message.include?("already closed window")
191
+
192
+ nil
193
+ end
194
+
195
+ def begin_frame
196
+ @pending_ansi_draws = []
197
+ end
198
+
199
+ def finish_frame
200
+ flush_ansi_draws unless @pending_ansi_draws.empty?
201
+ end
202
+
203
+ def clear_physical_screen!
204
+ $stdout.write("\e[2J\e[H")
205
+ $stdout.flush
206
+ invalidate!
207
+ self
208
+ end
209
+
210
+ def restore_cursor(field)
211
+ rect = screen.input_rect
212
+ x = field.cursor_x.to_i.clamp(0, [rect.cols - 1, 0].max)
213
+ queue_ansi_cursor(row: rect.row, col: rect.col + x)
214
+ end
215
+
216
+ # In truecolor mode, curses windows must still have themed backgrounds.
217
+ # ncurses may repaint or expose its backing store during getch/doupdate,
218
+ # so we must keep the backing store visually consistent with ANSI output.
219
+ def sync_backgrounds!
220
+ return self unless context.truecolor
221
+
222
+ sync_window_background(context.output_win, :output)
223
+ sync_window_background(context.input_win, :input)
224
+ sync_window_background(context.alert_win, :info)
225
+
226
+ if @screen.status_rect.rows.positive?
227
+ sync_window_background(context.status_win, :status)
228
+ end
229
+ self
230
+ end
231
+
232
+ private
233
+
234
+ def sync_window_background(win, role)
235
+ return unless win
236
+
237
+ attr = pair_attr(role, fallback: ::Curses::A_NORMAL)
238
+ win.bkgdset(attr) if win.respond_to?(:bkgdset)
239
+ win.erase
240
+ win.noutrefresh if win.respond_to?(:noutrefresh)
241
+ end
242
+
243
+ def render_popup_title(session:)
244
+ win = session.win
245
+ return unless win
246
+ return unless session.title
247
+
248
+ row, col = win.origin
249
+ return unless row && col
250
+
251
+ width = [win.maxx - 4, 0].max
252
+ text = " #{session.title} "[0, width]
253
+ return if text.empty?
254
+
255
+ queue_ansi_line(
256
+ row: row,
257
+ col: col + 2,
258
+ width: text.length,
259
+ text: text,
260
+ role: :popup_frame,
261
+ )
262
+ end
263
+
264
+ def render_popup_frame(session:)
265
+ win = session.win
266
+ return unless win
267
+
268
+ width = win.maxx
269
+ height = win.maxy
270
+ return if width < 2 || height < 2
271
+
272
+ row, col = win.origin
273
+ return unless row && col
274
+
275
+ b = popup_border
276
+ # Draw the top frame
277
+ queue_ansi_line(
278
+ row: row,
279
+ col: col,
280
+ width: width,
281
+ text: b[:tl] + (b[:h] * (width - 2)) + b[:tr],
282
+ role: :popup_frame,
283
+ )
284
+ (1...(height - 1)).each do |y|
285
+ # Draw the left frame
286
+ queue_ansi_line(
287
+ row: row + y,
288
+ col: col,
289
+ width: 1,
290
+ text: b[:v],
291
+ role: :popup_frame,
292
+ )
293
+ # Draw the right frame
294
+ queue_ansi_line(
295
+ row: row + y,
296
+ col: col + width - 1,
297
+ width: 1,
298
+ text: b[:v],
299
+ role: :popup_frame,
300
+ )
301
+ end
302
+ # Draw the bottom frame
303
+ queue_ansi_line(
304
+ row: row + height - 1,
305
+ col: col,
306
+ width: width,
307
+ text: b[:bl] + (b[:h] * (width - 2)) + b[:br],
308
+ role: :popup_frame,
309
+ )
310
+ end
311
+
312
+ def render_popup_message(session:, layout:)
313
+ win = session.win
314
+ return layout.row unless win
315
+
316
+ return layout.row unless session.message && !session.message.empty?
317
+
318
+ row = win.origin[0] + 1 + layout.row
319
+ col = win.origin[1] + 1
320
+ queue_ansi_line(
321
+ row: row,
322
+ col: col,
323
+ width: layout.width,
324
+ text: session.message.to_s,
325
+ role: :popup,
326
+ )
327
+ layout.row + 1
328
+ end
329
+
330
+ def render_popup_items(session:, layout:)
331
+ win = session.win
332
+ return unless win
333
+
334
+ items = session.displayed
335
+ sel = session.selected
336
+ start = session.scroll_start(list_h: layout.height)
337
+ row = layout.row
338
+
339
+ (0...layout.height).each do |i|
340
+ idx = start + i
341
+ selected = idx == sel
342
+ role = selected ? :popup_selection : :popup
343
+ line = ""
344
+ if idx < items.length
345
+ item = items[idx]
346
+ gutter = session.gutter_for(item: item, selected: selected)
347
+ avail = [layout.width - gutter.length, 0].max
348
+ line = (gutter + item.to_s[0, avail])[0, layout.width]
349
+ end
350
+ queue_ansi_popup_line(
351
+ win: win,
352
+ inner_row: row + i,
353
+ width: layout.width,
354
+ text: line,
355
+ role: role,
356
+ )
357
+ end
358
+ end
359
+
360
+ def render_popup_counts(session:, layout:)
361
+ win = session.win
362
+ return unless win
363
+
364
+ row = win.origin[0] + 1 + layout.row
365
+ col = win.origin[1] + 1
366
+ queue_ansi_line(
367
+ row: row,
368
+ col: col,
369
+ width: layout.width,
370
+ text: popup_counts_text(session),
371
+ role: :popup_counts,
372
+ )
373
+ end
374
+
375
+ def render_popup_input_field(session:, layout:)
376
+ win = session.win
377
+ return unless win
378
+
379
+ row = win.origin[0] + 1 + layout.row
380
+ col = win.origin[1] + 1
381
+ queue_ansi_segments_line(
382
+ row: row,
383
+ col: col,
384
+ width: layout.width,
385
+ segments: field_segments(
386
+ session.field,
387
+ base_role: :popup_input,
388
+ suggestion_role: :input_suggestion,
389
+ region_role: :region,
390
+ ),
391
+ fill_role: :popup_input,
392
+ )
393
+ cursor_x = session.field.cursor_x.to_i.clamp(0, [layout.width - 1, 0].max)
394
+ queue_ansi_cursor(row: row, col: col + cursor_x)
395
+ end
396
+
397
+ def queue_ansi_popup_title(win:, title:)
398
+ return unless title
399
+
400
+ row, col = win.origin
401
+ return unless row && col
402
+
403
+ width = win.maxx
404
+ text = " #{title} "[0, [width - 4, 0].max]
405
+ return if text.empty?
406
+
407
+ queue_ansi_line(
408
+ row: row,
409
+ col: col + 2,
410
+ width: text.length,
411
+ text: text,
412
+ role: :popup_frame,
413
+ )
414
+ end
415
+
416
+ def queue_ansi_cursor(row:, col:)
417
+ @pending_ansi_draws << {
418
+ type: :cursor,
419
+ row: row,
420
+ col: col,
421
+ }
422
+ end
423
+
424
+ def draw_output_lines(lines, viewport:, highlights: nil)
425
+ rect = screen.output_rect
426
+
427
+ rect.rows.times do |y|
428
+ line = lines[y].to_s
429
+ abs_line = viewport.top + y
430
+ ranges = highlight_ranges_for_line(highlights, abs_line)
431
+
432
+ queue_ansi_segments_line(
433
+ row: rect.row + y,
434
+ col: rect.col,
435
+ width: rect.cols,
436
+ segments: output_segments(line, ranges: ranges),
437
+ fill_role: :output,
438
+ )
439
+ end
440
+ end
441
+
442
+ def output_segments(line, ranges:)
443
+ plain = Fatty::Ansi.plain_text(line.to_s)
444
+ base_segments = []
445
+
446
+ Fatty::Ansi.segment(line.to_s).each do |text, style|
447
+ base_segments << {
448
+ text: text.to_s,
449
+ role: :output,
450
+ style: style,
451
+ }
452
+ end
453
+ apply_highlight_ranges_to_segments(base_segments, plain:, ranges:)
454
+ end
455
+
456
+ def apply_highlight_ranges_to_segments(segments, plain:, ranges:)
457
+ ranges = Array(ranges).sort_by(&:first)
458
+ return segments if ranges.empty?
459
+
460
+ out = []
461
+ pos = 0
462
+
463
+ segments.each do |seg|
464
+ text = seg[:text].to_s
465
+ seg_start = pos
466
+ seg_end = pos + text.length
467
+ cursor = 0
468
+
469
+ ranges.each do |from, to, highlight_role|
470
+ from = from.to_i
471
+ to = to.to_i
472
+ next if to <= seg_start || from >= seg_end
473
+
474
+ local_from = [from - seg_start, cursor].max
475
+ local_to = [to - seg_start, text.length].min
476
+
477
+ if local_from > cursor
478
+ out << seg.merge(text: text[cursor...local_from].to_s)
479
+ end
480
+
481
+ out << {
482
+ text: text[local_from...local_to].to_s,
483
+ role: highlight_role == :secondary ? :match_other : :match_current,
484
+ }
485
+
486
+ cursor = local_to
487
+ end
488
+
489
+ if cursor < text.length
490
+ out << seg.merge(text: text[cursor..].to_s)
491
+ end
492
+
493
+ pos = seg_end
494
+ end
495
+
496
+ out.reject { |seg| seg[:text].empty? }
497
+ end
498
+ end
499
+
500
+ # Restore cursor into the output window at a specific *output-win* row.
501
+ # `row:` is 0..(screen.output_rect.rows-1), NOT an absolute screen row.
502
+ def restore_output_cursor(field, row:)
503
+ cols = @screen.cols
504
+
505
+ x = field.cursor_x.to_i
506
+ x = x.clamp(0, [cols - 1, 0].max)
507
+
508
+ row0 = @screen.output_rect.row
509
+ col0 = @screen.output_rect.col
510
+ cols = @screen.output_rect.cols
511
+
512
+ @pending_ansi_draws << {
513
+ type: :cursor,
514
+ row: row0 + row,
515
+ col: col0 + x.clamp(0, [cols - 1, 0].max),
516
+ }
517
+ end
518
+
519
+ def queue_ansi_popup_line(win:, inner_row:, inner_col: 0, width:, text:, role:)
520
+ row, col = win.origin
521
+ return unless row && col
522
+
523
+ queue_ansi_line(
524
+ row: row + 1 + inner_row,
525
+ col: col + 1 + inner_col,
526
+ width: width,
527
+ text: text.to_s,
528
+ role: role,
529
+ )
530
+ end
531
+
532
+ def queue_ansi_segments_line(row:, col:, width:, segments:, fill_role: :output)
533
+ @pending_ansi_draws << {
534
+ type: :segments_line,
535
+ row: row,
536
+ col: col,
537
+ width: width,
538
+ segments: segments,
539
+ fill_role: fill_role,
540
+ }
541
+ nil
542
+ end
543
+
544
+ def queue_ansi_line(row:, col:, width:, text:, role: nil)
545
+ spec = palette[role] || {}
546
+ @pending_ansi_draws << {
547
+ row: row,
548
+ col: col,
549
+ width: width,
550
+ text: text,
551
+ role: role,
552
+ spec: spec,
553
+ }
554
+ end
555
+
556
+ def queue_ansi_rect(row:, col:, width:, height:, role:)
557
+ return if height <= 0 || width <= 0
558
+
559
+ height.times do |i|
560
+ queue_ansi_line(
561
+ row: row + i,
562
+ col: col,
563
+ width: width,
564
+ text: " " * width,
565
+ role: role,
566
+ )
567
+ end
568
+ nil
569
+ end
570
+
571
+ def flush_ansi_draws
572
+ Fatty.debug("flush_ansi_draws pending_count=#{@pending_ansi_draws.length}", tag: :render)
573
+
574
+ # Hide the cursor
575
+ @ansi_renderer.write_ansi("\e[?25l")
576
+
577
+ @pending_ansi_draws.each do |draw|
578
+ case draw[:type]
579
+ when :cursor
580
+ @ansi_renderer.write_ansi("\e[#{draw[:row] + 1};#{draw[:col] + 1}H")
581
+ when :segments_line
582
+ @ansi_renderer.render_segments_line(
583
+ row: draw[:row],
584
+ col: draw[:col],
585
+ width: draw[:width],
586
+ segments: draw[:segments],
587
+ palette: palette,
588
+ fill_role: draw[:fill_role] || :output,
589
+ )
590
+ else
591
+ @ansi_renderer.render_line(
592
+ row: draw[:row],
593
+ col: draw[:col],
594
+ width: draw[:width],
595
+ text: draw[:text],
596
+ role: draw[:role],
597
+ palette: palette,
598
+ )
599
+ end
600
+ end
601
+ @pending_ansi_draws.clear
602
+ ensure
603
+ # Unhide the cursor
604
+ @ansi_renderer.write_ansi("\e[?25h")
605
+ end
606
+ end
607
+ end