fatty 0.99.0 → 0.99.3

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.org +11 -29
  3. data/README.md +2207 -18
  4. data/README.org +2066 -104
  5. data/exe/fatty +236 -93
  6. data/help/help.md +233 -0
  7. data/lib/fatty/action.rb +3 -7
  8. data/lib/fatty/actionable.rb +2 -0
  9. data/lib/fatty/alert.rb +65 -39
  10. data/lib/fatty/ansi/renderer.rb +26 -6
  11. data/lib/fatty/ansi.rb +1 -0
  12. data/lib/fatty/api/alert.rb +15 -0
  13. data/lib/fatty/api/choose.rb +145 -0
  14. data/lib/fatty/api/environment.rb +12 -0
  15. data/lib/fatty/api/keytest.rb +11 -0
  16. data/lib/fatty/api/menu.rb +104 -0
  17. data/lib/fatty/api/output.rb +55 -0
  18. data/lib/fatty/api/progress.rb +24 -0
  19. data/lib/fatty/api/prompt.rb +77 -0
  20. data/lib/fatty/api/status.rb +55 -0
  21. data/lib/fatty/api.rb +38 -0
  22. data/lib/fatty/callback_environment.rb +74 -0
  23. data/lib/fatty/colors/color.rb +366 -232
  24. data/lib/fatty/colors/pairs.rb +2 -0
  25. data/lib/fatty/command.rb +78 -0
  26. data/lib/fatty/config.rb +227 -39
  27. data/lib/fatty/config_files/config.yml +4 -12
  28. data/lib/fatty/config_files/keydefs.yml +13 -23
  29. data/lib/fatty/config_files/themes/capuchin_monk.yml +140 -0
  30. data/lib/fatty/config_files/themes/catppuccin_latte.yml +140 -0
  31. data/lib/fatty/config_files/themes/catppuccin_mocha.yml +140 -0
  32. data/lib/fatty/config_files/themes/cyberpunk.yml +141 -0
  33. data/lib/fatty/config_files/themes/dracula.yml +140 -0
  34. data/lib/fatty/config_files/themes/everforest_dark.yml +140 -0
  35. data/lib/fatty/config_files/themes/gruvbox_dark.yml +140 -0
  36. data/lib/fatty/config_files/themes/gruvbox_light.yml +140 -0
  37. data/lib/fatty/config_files/themes/mono.yml +119 -47
  38. data/lib/fatty/config_files/themes/monokai.yml +140 -0
  39. data/lib/fatty/config_files/themes/nordic.yml +1 -0
  40. data/lib/fatty/config_files/themes/onedark.yml +140 -0
  41. data/lib/fatty/config_files/themes/solarized_dark.yml +1 -0
  42. data/lib/fatty/config_files/themes/solarized_light.yml +140 -0
  43. data/lib/fatty/config_files/themes/terminal.yml +67 -0
  44. data/lib/fatty/config_files/themes/tokyo_night.yml +140 -0
  45. data/lib/fatty/config_files/themes/wordperfect.yml +80 -9
  46. data/lib/fatty/core_ext/hash.rb +19 -0
  47. data/lib/fatty/core_ext/string.rb +19 -12
  48. data/lib/fatty/core_ext.rb +1 -0
  49. data/lib/fatty/curses/context.rb +86 -84
  50. data/lib/fatty/curses/event_source.rb +128 -40
  51. data/lib/fatty/curses/key_decoder.rb +77 -11
  52. data/lib/fatty/env.rb +21 -20
  53. data/lib/fatty/help.rb +1 -29
  54. data/lib/fatty/history/entry.rb +2 -0
  55. data/lib/fatty/history.rb +17 -14
  56. data/lib/fatty/input_buffer.rb +71 -47
  57. data/lib/fatty/input_field.rb +216 -149
  58. data/lib/fatty/key_event.rb +67 -66
  59. data/lib/fatty/key_map.rb +54 -16
  60. data/lib/fatty/keymaps/emacs.rb +40 -6
  61. data/lib/fatty/log_formats/json.rb +2 -0
  62. data/lib/fatty/log_formats/text.rb +2 -0
  63. data/lib/fatty/logger.rb +7 -2
  64. data/lib/fatty/markdown/ansi_renderer.rb +75 -6
  65. data/lib/fatty/markdown/render.rb +2 -3
  66. data/lib/fatty/mouse_event.rb +142 -2
  67. data/lib/fatty/output_buffer.rb +2 -0
  68. data/lib/fatty/pager.rb +158 -47
  69. data/lib/fatty/progress.rb +417 -0
  70. data/lib/fatty/renderer/curses.rb +340 -184
  71. data/lib/fatty/renderer/truecolor.rb +266 -163
  72. data/lib/fatty/renderer.rb +115 -97
  73. data/lib/fatty/screen.rb +2 -0
  74. data/lib/fatty/session/alert_session.rb +39 -21
  75. data/lib/fatty/session/isearch_session.rb +92 -38
  76. data/lib/fatty/session/keytest_session.rb +105 -85
  77. data/lib/fatty/session/modal_session.rb +38 -1
  78. data/lib/fatty/session/output_session.rb +472 -21
  79. data/lib/fatty/session/popup_session.rb +251 -134
  80. data/lib/fatty/session/prompt_session.rb +68 -26
  81. data/lib/fatty/session/search_session.rb +79 -31
  82. data/lib/fatty/session/shell_session.rb +383 -368
  83. data/lib/fatty/session/status_session.rb +167 -0
  84. data/lib/fatty/session.rb +95 -95
  85. data/lib/fatty/terminal/popup_owner.rb +14 -11
  86. data/lib/fatty/terminal.rb +515 -813
  87. data/lib/fatty/themes/loader.rb +4 -2
  88. data/lib/fatty/themes/manager.rb +4 -0
  89. data/lib/fatty/themes/registry.rb +2 -0
  90. data/lib/fatty/themes/resolver.rb +175 -162
  91. data/lib/fatty/version.rb +1 -1
  92. data/lib/fatty/viewport.rb +8 -1
  93. data/lib/fatty.rb +35 -7
  94. metadata +42 -40
  95. data/.envrc +0 -2
  96. data/.simplecov +0 -23
  97. data/.yardopts +0 -4
  98. data/CHANGELOG.md +0 -34
  99. data/Rakefile +0 -15
  100. data/TODO.org +0 -163
  101. data/examples/markdown/native-markdown.md +0 -370
  102. data/examples/markdown/ox-gfm-markdown.md +0 -373
  103. data/examples/markdown/ox-gfm-markdown.org +0 -376
  104. data/fatty.gemspec +0 -42
  105. data/lib/fatty/accept_env.rb +0 -32
  106. data/lib/fatty/config_files/help.md +0 -120
  107. data/lib/fatty/config_files/help.org +0 -124
  108. data/lib/fatty/config_files/themes/wordperfect_light.yml +0 -77
  109. data/lib/fatty/menu_env.rb +0 -22
  110. data/lib/fatty/session/input_session.rb +0 -99
  111. data/lib/fatty/sessions.rb +0 -14
  112. data/lib/fatty/terminal/progress.rb +0 -374
  113. data/lib/fatty/view/alert_view.rb +0 -14
  114. data/lib/fatty/view/cursor_view.rb +0 -18
  115. data/lib/fatty/view/input_view.rb +0 -9
  116. data/lib/fatty/view/output_view.rb +0 -9
  117. data/lib/fatty/view/status_view.rb +0 -14
  118. data/lib/fatty/view.rb +0 -33
  119. data/lib/fatty/views.rb +0 -9
  120. data/sig/fatty.rbs +0 -4
@@ -9,38 +9,26 @@ module Fatty
9
9
  super
10
10
  @ansi_renderer = Fatty::Ansi::Renderer.new
11
11
  @pending_ansi_draws = []
12
+ @cursor_visible = true
12
13
  end
13
14
 
14
- def render_status(text, role: :status_info)
15
- state = status_state(text, role)
15
+ def render_status(status_session)
16
+ state = status_state(status_session)
16
17
  return if state == @last_status_state
17
18
 
18
19
  @last_status_state = state
19
20
 
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
21
+ draw_status_lines(status_session)
35
22
  end
36
23
 
37
- def render_alert(alert)
38
- state = alert_state(alert)
24
+ def render_alert(alert_session)
25
+ state = alert_state(alert_session)
39
26
  return if state == @last_alert_state
40
27
 
28
+ alert = alert_session.current
41
29
  @last_alert_state = state
42
30
  text = alert ? alert.format : ""
43
- role = alert ? alert.role : :alert_info
31
+ role = alert ? alert_role(alert.role) : :alert_info
44
32
 
45
33
  queue_ansi_line(
46
34
  row: screen.alert_rect.row,
@@ -51,18 +39,24 @@ module Fatty
51
39
  )
52
40
  end
53
41
 
54
- def render_output(output, viewport:, highlights: nil)
55
- lines = viewport.slice(output.lines)
56
- normalized = normalized_highlights(highlights)
42
+ def render_output(output_session, viewport: output_session.viewport)
43
+ lines = viewport.slice(output_session.visible_lines)
44
+ normalized = normalized_highlights(output_session.highlights)
45
+ line_number_width =
46
+ if output_session.line_numbers?
47
+ output_session.output.lines.length.to_s.length
48
+ end
49
+
50
+ curr = output_state(output_session, viewport: viewport)
51
+ return if curr == @last_output_state
57
52
 
58
- curr = output_state(
53
+ draw_output_lines(
54
+ lines,
59
55
  viewport: viewport,
60
- lines: lines,
61
56
  highlights: normalized,
57
+ line_number_width: line_number_width,
62
58
  )
63
- return if curr == @last_output_state
64
-
65
- draw_output_lines(lines, viewport: viewport, highlights: normalized)
59
+ @last_pager_field_state = nil
66
60
  @last_output_state = curr
67
61
  end
68
62
 
@@ -85,12 +79,22 @@ module Fatty
85
79
  )
86
80
  end
87
81
 
82
+ def clear_input_field
83
+ @last_input_state = nil
84
+ queue_ansi_line(
85
+ row: screen.input_rect.row,
86
+ col: screen.input_rect.col,
87
+ width: screen.input_rect.cols,
88
+ text: "",
89
+ role: :input,
90
+ )
91
+ end
92
+
88
93
  def render_pager_field(field, row:, role: :pager_status)
89
94
  state = pager_field_state(field, row: row, role: role)
90
95
  return if state == @last_pager_field_state
91
96
 
92
97
  @last_pager_field_state = state
93
-
94
98
  win = context.output_win
95
99
  return unless win
96
100
 
@@ -98,7 +102,6 @@ module Fatty
98
102
  return unless row0 && col0
99
103
 
100
104
  cols = win.respond_to?(:maxx) ? win.maxx : @screen.cols
101
-
102
105
  queue_ansi_segments_line(
103
106
  row: row0 + row,
104
107
  col: col0,
@@ -113,12 +116,14 @@ module Fatty
113
116
  )
114
117
  end
115
118
 
119
+ # Modal overlays, render_popup and render_prompt_popu are deliberately
120
+ # not state-cached.
121
+ #
122
+ # They must be restaged on every frame because they are drawn on top of the
123
+ # focused session. Even when the overlay's own state has not changed, the
124
+ # underlying session may have redrawn the area beneath it. Skipping the overlay
125
+ # render would let the underlying frame erase or visually punch through it.
116
126
  def render_popup(session:)
117
- state = popup_state(session)
118
- return if state == @last_popup_state
119
-
120
- @last_popup_state = state
121
-
122
127
  win = session.win
123
128
  return unless win
124
129
 
@@ -140,33 +145,43 @@ module Fatty
140
145
  layout = PopupLayout.new(row: 0, width: inner_w)
141
146
  row = render_popup_message(session: session, layout: layout)
142
147
 
143
- counts_present = !!session.counts
144
- input_row = inner_h - 1
145
- counts_row = counts_present ? input_row - 1 : nil
146
-
147
148
  # Draw the displayed items inside the window with a gutter to have an
148
149
  # indicator of what is selected, if any.
149
- list_h = [inner_h - row - 1 - (counts_present ? 1 : 0), 0].max
150
+ filter_present = session.filter_present?
151
+ counts_present = session.counts_present?
152
+ input_row = filter_present ? inner_h - 1 : nil
153
+ counts_row =
154
+ if counts_present
155
+ filter_present ? input_row - 1 : inner_h - 1
156
+ end
157
+
158
+ list_end =
159
+ if counts_present
160
+ counts_row
161
+ elsif filter_present
162
+ input_row
163
+ else
164
+ inner_h
165
+ end
166
+ list_h = [list_end - row, 0].max
167
+
150
168
  layout = PopupLayout.new(row: row, width: inner_w, height: list_h)
151
169
  render_popup_items(session: session, layout: layout)
152
170
 
153
171
  # Draw the "counts" line to indicate the total number of items, items
154
172
  # selected, and items displayed.
155
173
  layout = PopupLayout.new(row: counts_row, width: inner_w)
156
- render_popup_counts(session: session, layout: layout) if counts_present
174
+ render_popup_counts(session: session, layout: layout) if session.counts_present?
157
175
 
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)
176
+ if filter_present
177
+ # Draw the input field for the user to type narrowing selection
178
+ # queries.
179
+ layout = PopupLayout.new(row: input_row, width: inner_w)
180
+ render_popup_input_field(session: session, layout: layout)
181
+ end
162
182
  end
163
183
 
164
184
  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
185
  win = session.win
171
186
  return unless win
172
187
 
@@ -184,6 +199,7 @@ module Fatty
184
199
  row = render_popup_message(session: session, layout: layout)
185
200
 
186
201
  layout = PopupLayout.new(row: row, width: inner_w)
202
+ show_cursor
187
203
  render_popup_input_field(session: session, layout: layout)
188
204
  rescue RuntimeError => e
189
205
  raise unless e.message.include?("closed window") ||
@@ -208,11 +224,27 @@ module Fatty
208
224
  end
209
225
 
210
226
  def restore_cursor(field)
227
+ return self unless @cursor_visible
228
+
211
229
  rect = screen.input_rect
212
230
  x = field.cursor_x.to_i.clamp(0, [rect.cols - 1, 0].max)
213
231
  queue_ansi_cursor(row: rect.row, col: rect.col + x)
214
232
  end
215
233
 
234
+ def show_cursor
235
+ @cursor_visible = true
236
+ ::Curses.curs_set(1)
237
+ @ansi_renderer.write_ansi(@ansi_renderer.hide_cursor)
238
+ self
239
+ end
240
+
241
+ def hide_cursor
242
+ @cursor_visible = false
243
+ ::Curses.curs_set(0)
244
+ @ansi_renderer.write_ansi(@ansi_renderer.hide_cursor)
245
+ self
246
+ end
247
+
216
248
  # In truecolor mode, curses windows must still have themed backgrounds.
217
249
  # ncurses may repaint or expose its backing store during getch/doupdate,
218
250
  # so we must keep the backing store visually consistent with ANSI output.
@@ -229,8 +261,28 @@ module Fatty
229
261
  self
230
262
  end
231
263
 
264
+ # Restore cursor into the output window at a specific *output-win* row.
265
+ # `row:` is 0..(screen.output_rect.rows-1), NOT an absolute screen row.
266
+ def restore_output_cursor(field, row:)
267
+ return self unless @cursor_visible
268
+
269
+ cols = @screen.cols
270
+ x = field.cursor_x.to_i
271
+ x = x.clamp(0, [cols - 1, 0].max)
272
+ row0 = @screen.output_rect.row
273
+ col0 = @screen.output_rect.col
274
+ cols = @screen.output_rect.cols
275
+ @pending_ansi_draws << {
276
+ type: :cursor,
277
+ row: row0 + row,
278
+ col: col0 + x.clamp(0, [cols - 1, 0].max),
279
+ }
280
+ end
281
+
232
282
  private
233
283
 
284
+ # simplecov:disable
285
+
234
286
  def sync_window_background(win, role)
235
287
  return unless win
236
288
 
@@ -332,24 +384,32 @@ module Fatty
332
384
  return unless win
333
385
 
334
386
  items = session.displayed
335
- sel = session.selected
387
+ selected = session.current
336
388
  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
389
+
390
+ (0...layout.height).each do |offset|
391
+ item_index = start + offset
392
+ item_selected = item_index == selected
393
+ role = item_selected ? :popup_selection : :popup
394
+
395
+ line = popup_item_text(
396
+ session,
397
+ items: items,
398
+ item_index: item_index,
399
+ selected: item_selected,
400
+ width: layout.width,
401
+ )
402
+
403
+ queue_ansi_popup_fill_row(
404
+ win: win,
405
+ inner_row: layout.row + offset,
406
+ width: layout.width,
407
+ role: role,
408
+ )
409
+
350
410
  queue_ansi_popup_line(
351
411
  win: win,
352
- inner_row: row + i,
412
+ inner_row: layout.row + offset,
353
413
  width: layout.width,
354
414
  text: line,
355
415
  role: role,
@@ -357,6 +417,21 @@ module Fatty
357
417
  end
358
418
  end
359
419
 
420
+ def popup_item_text(session, items:, item_index:, selected:, width:)
421
+ line = ""
422
+
423
+ if item_index < items.length
424
+ item = items[item_index]
425
+ gutter = session.gutter_for(item: item, selected: selected)
426
+ text = Fatty::Ansi.strip(item.to_s).tr("\r\n", " ")
427
+ available = [width - Fatty::Ansi.visible_length(gutter), 0].max
428
+ text = Fatty::Ansi.truncate_visible(text, available)
429
+ line = Fatty::Ansi.truncate_visible(gutter + text, width)
430
+ end
431
+
432
+ line
433
+ end
434
+
360
435
  def render_popup_counts(session:, layout:)
361
436
  win = session.win
362
437
  return unless win
@@ -378,6 +453,14 @@ module Fatty
378
453
 
379
454
  row = win.origin[0] + 1 + layout.row
380
455
  col = win.origin[1] + 1
456
+
457
+ queue_ansi_popup_fill_row(
458
+ win: win,
459
+ inner_row: layout.row,
460
+ width: layout.width,
461
+ role: :popup_input,
462
+ )
463
+
381
464
  queue_ansi_segments_line(
382
465
  row: row,
383
466
  col: col,
@@ -390,6 +473,7 @@ module Fatty
390
473
  ),
391
474
  fill_role: :popup_input,
392
475
  )
476
+
393
477
  cursor_x = session.field.cursor_x.to_i.clamp(0, [layout.width - 1, 0].max)
394
478
  queue_ansi_cursor(row: row, col: col + cursor_x)
395
479
  end
@@ -421,24 +505,50 @@ module Fatty
421
505
  }
422
506
  end
423
507
 
424
- def draw_output_lines(lines, viewport:, highlights: nil)
508
+ def draw_output_lines(lines, viewport:, highlights: nil, line_number_width: nil)
425
509
  rect = screen.output_rect
426
510
 
427
511
  rect.rows.times do |y|
428
- line = lines[y].to_s
429
- abs_line = viewport.top + y
512
+ visible_line = lines[y]
513
+ text = visible_line&.text.to_s
514
+ abs_line = visible_line ? visible_line.number - 1 : nil
430
515
  ranges = highlight_ranges_for_line(highlights, abs_line)
516
+ segments = []
517
+
518
+ if visible_line && line_number_width
519
+ number_text = visible_line.number.to_s.rjust(line_number_width, "0")
520
+ segments << {
521
+ text: "#{number_text}: ",
522
+ role: :line_number,
523
+ }
524
+ end
431
525
 
526
+ segments.concat(output_segments(text, ranges: ranges))
432
527
  queue_ansi_segments_line(
433
528
  row: rect.row + y,
434
529
  col: rect.col,
435
530
  width: rect.cols,
436
- segments: output_segments(line, ranges: ranges),
531
+ segments: segments,
437
532
  fill_role: :output,
438
533
  )
439
534
  end
440
535
  end
441
536
 
537
+ def draw_status_lines(status_session)
538
+ rect = screen.status_rect
539
+ lines = status_segment_lines(status_session)
540
+
541
+ rect.rows.times do |y|
542
+ queue_ansi_segments_line(
543
+ row: rect.row + y,
544
+ col: rect.col,
545
+ width: rect.cols,
546
+ segments: lines[y] || [],
547
+ fill_role: status_session.role,
548
+ )
549
+ end
550
+ end
551
+
442
552
  def output_segments(line, ranges:)
443
553
  plain = Fatty::Ansi.plain_text(line.to_s)
444
554
  base_segments = []
@@ -495,113 +605,106 @@ module Fatty
495
605
 
496
606
  out.reject { |seg| seg[:text].empty? }
497
607
  end
498
- end
499
608
 
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
609
+ def queue_ansi_popup_line(win:, inner_row:, inner_col: 0, width:, text:, role:)
610
+ row, col = win.origin
611
+ return unless row && col
558
612
 
559
- height.times do |i|
560
613
  queue_ansi_line(
561
- row: row + i,
614
+ row: row + 1 + inner_row,
615
+ col: col + 1 + inner_col,
616
+ width: width,
617
+ text: text.to_s,
618
+ role: role,
619
+ )
620
+ end
621
+
622
+ def queue_ansi_segments_line(row:, col:, width:, segments:, fill_role: :output)
623
+ @pending_ansi_draws << {
624
+ type: :segments_line,
625
+ row: row,
562
626
  col: col,
563
627
  width: width,
628
+ segments: segments,
629
+ fill_role: fill_role,
630
+ }
631
+ nil
632
+ end
633
+
634
+ def queue_ansi_popup_fill_row(win:, inner_row:, inner_col: 0, width:, role:)
635
+ queue_ansi_popup_line(
636
+ win: win,
637
+ inner_row: inner_row,
638
+ inner_col: inner_col,
639
+ width: width,
564
640
  text: " " * width,
565
641
  role: role,
566
642
  )
567
643
  end
568
- nil
569
- end
570
644
 
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,
645
+ def queue_ansi_line(row:, col:, width:, text:, role: nil)
646
+ spec = palette[role] || {}
647
+ @pending_ansi_draws << {
648
+ row: row,
649
+ col: col,
650
+ width: width,
651
+ text: text,
652
+ role: role,
653
+ spec: spec,
654
+ }
655
+ end
656
+
657
+ def queue_ansi_rect(row:, col:, width:, height:, role:)
658
+ return if height <= 0 || width <= 0
659
+
660
+ height.times do |i|
661
+ queue_ansi_line(
662
+ row: row + i,
663
+ col: col,
664
+ width: width,
665
+ text: " " * width,
666
+ role: role,
598
667
  )
599
668
  end
669
+ nil
670
+ end
671
+
672
+ def flush_ansi_draws
673
+ Fatty.debug("flush_ansi_draws pending_count=#{@pending_ansi_draws.length}", tag: :render)
674
+
675
+ # Hide the cursor
676
+ @ansi_renderer.write_ansi("\e[?25l")
677
+
678
+ @pending_ansi_draws.each do |draw|
679
+ case draw[:type]
680
+ when :cursor
681
+ if @cursor_visible
682
+ @ansi_renderer.write_ansi("\e[#{draw[:row] + 1};#{draw[:col] + 1}H")
683
+ end
684
+ when :segments_line
685
+ @ansi_renderer.render_segments_line(
686
+ row: draw[:row],
687
+ col: draw[:col],
688
+ width: draw[:width],
689
+ segments: draw[:segments],
690
+ palette: palette,
691
+ fill_role: draw[:fill_role] || :output,
692
+ )
693
+ else
694
+ @ansi_renderer.render_line(
695
+ row: draw[:row],
696
+ col: draw[:col],
697
+ width: draw[:width],
698
+ text: draw[:text],
699
+ role: draw[:role],
700
+ palette: palette,
701
+ )
702
+ end
703
+ end
704
+ @pending_ansi_draws.clear
705
+ ensure
706
+ @ansi_renderer.write_ansi(@cursor_visible ? "\e[?25h" : "\e[?25l")
600
707
  end
601
- @pending_ansi_draws.clear
602
- ensure
603
- # Unhide the cursor
604
- @ansi_renderer.write_ansi("\e[?25h")
605
708
  end
606
709
  end
607
710
  end