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
@@ -11,50 +11,44 @@ module Fatty
11
11
  super
12
12
  end
13
13
 
14
- def render_status(text, role: :status_info)
15
- state = status_state(text, role)
14
+ def render_status(status_session)
15
+ state = status_state(status_session)
16
16
  return if state == @last_status_state
17
17
 
18
18
  @last_status_state = state
19
-
20
- win = context.status_win
21
- return unless win
22
-
23
- rows = screen.status_rect.rows
24
- cols = screen.status_rect.cols
25
- base_attr = pair_attr(role, fallback: ::Curses::A_REVERSE)
26
-
27
- win.bkgdset(base_attr) if win.respond_to?(:bkgdset)
28
- win.erase
29
- win.attrset(base_attr)
30
-
31
- status_render_lines(text, width: cols, max_rows: rows).each_with_index do |line, row|
32
- win.setpos(row, 0)
33
- rendered = Fatty::Ansi.truncate_visible(line, cols)
34
- padding = [cols - Fatty::Ansi.visible_length(rendered), 0].max
35
- win.addstr(rendered + (" " * padding))
36
- end
37
- stage_window(win)
19
+ draw_status_lines(status_session)
38
20
  end
39
21
 
40
- def render_output(output, viewport:, highlights: nil)
41
- lines = viewport.slice(output.lines)
42
- normalized = normalized_highlights(highlights)
43
-
44
- curr = output_state(
45
- viewport: viewport,
46
- lines: lines,
47
- highlights: normalized,
48
- )
22
+ def render_output(output_session, viewport: output_session.viewport)
23
+ lines = viewport.slice(output_session.visible_lines)
24
+ normalized = normalized_highlights(output_session.highlights)
25
+ line_number_width =
26
+ if output_session.line_numbers?
27
+ output_session.output.lines.length.to_s.length
28
+ end
49
29
 
30
+ curr = output_state(output_session, viewport: viewport)
50
31
  prev = @last_output_state
51
32
 
52
- if prev && can_incrementally_scroll_output?(prev, curr)
53
- scroll_output_window_delta!(prev: prev, curr: curr)
33
+ if prev &&
34
+ !output_session.line_numbers? &&
35
+ !ansi_lines?(lines) &&
36
+ output_session.incrementally_scrollable_from?(prev, curr)
37
+ scroll_output_window_delta!(
38
+ delta: output_session.scroll_delta_from(prev, curr),
39
+ lines: lines,
40
+ viewport: viewport,
41
+ highlights: normalized,
42
+ )
54
43
  else
55
- draw_output_lines(lines, viewport: viewport, highlights: normalized)
44
+ draw_output_lines(
45
+ lines,
46
+ viewport: viewport,
47
+ highlights: normalized,
48
+ line_number_width: line_number_width,
49
+ )
56
50
  end
57
-
51
+ @last_pager_field_state = nil
58
52
  @last_output_state = curr
59
53
  end
60
54
 
@@ -91,8 +85,21 @@ module Fatty
91
85
  stage_window(win)
92
86
  end
93
87
 
94
- def render_alert(alert)
95
- state = alert_state(alert)
88
+ def clear_input_field
89
+ @last_input_state = nil
90
+
91
+ win = context.input_win
92
+ return unless win
93
+
94
+ attr = pair_attr(:input, fallback: ::Curses::A_NORMAL)
95
+ win.bkgdset(attr) if win.respond_to?(:bkgdset)
96
+ win.erase
97
+ win.attrset(attr)
98
+ stage_window(win)
99
+ end
100
+
101
+ def render_alert(alert_session)
102
+ state = alert_state(alert_session)
96
103
  return if state == @last_alert_state
97
104
 
98
105
  @last_alert_state = state
@@ -100,17 +107,32 @@ module Fatty
100
107
  win = context.alert_win
101
108
  return unless win
102
109
 
110
+ alert = alert_session.current
103
111
  text = alert ? alert.format : ""
104
- text = Fatty::Ansi.strip(text)
105
- role = alert ? alert.role : :alert
106
- attr = pair_attr(role, fallback: pair_attr(:alert, fallback: ::Curses::A_REVERSE))
112
+ role = alert ? alert_role(alert.role) : :alert_info
113
+ base_attr = pair_attr(role, fallback: pair_attr(:alert, fallback: ::Curses::A_REVERSE))
107
114
  cols = win.respond_to?(:maxx) ? win.maxx : screen.alert_rect.cols
108
115
 
109
- win.bkgdset(attr) if win.respond_to?(:bkgdset)
116
+ win.bkgdset(base_attr) if win.respond_to?(:bkgdset)
110
117
  win.erase
111
- win.attrset(attr)
118
+ win.attrset(base_attr)
112
119
  win.setpos(0, 0)
113
- win.addstr(text.ljust(cols)[0, cols])
120
+
121
+ segments = Fatty::Ansi.segment(text).map do |segment_text, style|
122
+ {
123
+ text: segment_text,
124
+ role: role,
125
+ style: style,
126
+ }
127
+ end
128
+
129
+ draw_status_row(
130
+ win,
131
+ row: 0,
132
+ cols: cols,
133
+ segments: segments,
134
+ base_attr: base_attr,
135
+ )
114
136
 
115
137
  stage_window(win)
116
138
  end
@@ -155,12 +177,14 @@ module Fatty
155
177
  field_win&.close if field_win&.respond_to?(:close)
156
178
  end
157
179
 
180
+ # Modal overlays, render_popup and render_prompt_popu are deliberately
181
+ # not state-cached.
182
+ #
183
+ # They must be restaged on every frame because they are drawn on top of the
184
+ # focused session. Even when the overlay's own state has not changed, the
185
+ # underlying session may have redrawn the area beneath it. Skipping the overlay
186
+ # render would let the underlying frame erase or visually punch through it.
158
187
  def render_popup(session:)
159
- state = popup_state(session)
160
- return if state == @last_popup_state
161
-
162
- @last_popup_state = state
163
-
164
188
  win = session.win
165
189
  return unless win
166
190
 
@@ -178,16 +202,7 @@ module Fatty
178
202
 
179
203
  win.attrset(frame_attr)
180
204
  draw_popup_frame(win, width: width, height: height)
181
-
182
- if session.title && !session.title.empty?
183
- title = Fatty::Ansi.strip(session.title.to_s).tr("\r\n", " ")
184
- title = Fatty::Ansi.truncate_visible(" #{title} ", [width - 4, 0].max)
185
-
186
- unless title.empty?
187
- win.setpos(0, 2)
188
- win.addstr(title)
189
- end
190
- end
205
+ render_popup_title(win, session: session, width: width)
191
206
 
192
207
  inner_h = height - 2
193
208
  inner_w = width - 2
@@ -197,72 +212,58 @@ module Fatty
197
212
  inner.bkgdset(popup_attr) if inner.respond_to?(:bkgdset)
198
213
  inner.erase
199
214
 
200
- row = 0
201
-
202
- if session.message && !session.message.empty?
203
- message = Fatty::Ansi.strip(session.message.to_s).tr("\r\n", " ")
204
- message = Fatty::Ansi.truncate_visible(message, inner_w)
215
+ row = render_popup_message(inner, session: session, row: 0, width: inner_w, attr: popup_attr)
205
216
 
206
- inner.attrset(popup_attr)
207
- inner.setpos(row, 0)
208
- inner.addstr(message.ljust(inner_w))
209
- row += 1
210
- end
217
+ counts_present = session.counts_present?
218
+ filter_present = session.filter_present?
211
219
 
212
- counts_present = !!session.counts
213
- input_row = inner_h - 1
214
- counts_row = counts_present ? input_row - 1 : nil
220
+ input_row = inner_h - 1 if filter_present
221
+ counts_row =
222
+ if counts_present
223
+ filter_present ? input_row - 1 : inner_h - 1
224
+ end
215
225
 
216
226
  list_row = row
217
- list_h = input_row - list_row
218
- list_h -= 1 if counts_present
219
- list_h = [list_h, 0].max
220
-
221
- items = session.displayed
222
- selected = session.selected
223
- start = session.scroll_start(list_h: list_h)
224
-
225
- (0...list_h).each do |offset|
226
- item_index = start + offset
227
- item_selected = item_index == selected
228
- attr = item_selected ? selected_attr : popup_attr
229
-
230
- line = ""
231
- if item_index < items.length
232
- item = items[item_index]
233
- gutter = session.gutter_for(item: item, selected: item_selected)
234
- text = Fatty::Ansi.strip(item.to_s).tr("\r\n", " ")
235
- available = [inner_w - Fatty::Ansi.visible_length(gutter), 0].max
236
- text = Fatty::Ansi.truncate_visible(text, available)
237
- line = Fatty::Ansi.truncate_visible(gutter + text, inner_w)
227
+ list_end =
228
+ if counts_present
229
+ counts_row
230
+ elsif filter_present
231
+ input_row
232
+ else
233
+ inner_h
238
234
  end
239
235
 
240
- inner.attrset(attr)
241
- inner.setpos(list_row + offset, 0)
242
- inner.addstr(line.ljust(inner_w))
243
- end
244
-
245
- if counts_present && counts_row && counts_row >= 0
246
- counts_text = Fatty::Ansi.truncate_visible(popup_counts_text(session), inner_w)
236
+ list_h = [list_end - list_row, 0].max
247
237
 
248
- inner.attrset(counts_attr)
249
- inner.setpos(counts_row, 0)
250
- inner.addstr(counts_text.ljust(inner_w))
251
- end
238
+ render_popup_items(
239
+ inner,
240
+ session: session,
241
+ row: list_row,
242
+ height: list_h,
243
+ width: inner_w,
244
+ popup_attr: popup_attr,
245
+ selected_attr: selected_attr,
246
+ )
252
247
 
253
- render_field_into(
254
- win: inner,
255
- field: session.field,
256
- row: input_row,
248
+ render_popup_counts(
249
+ inner,
250
+ session: session,
251
+ row: counts_row,
257
252
  width: inner_w,
258
- base_role: :popup_input,
259
- base_attr: input_attr,
260
- region_attr: pair_attr(:region, fallback: ::Curses::A_REVERSE),
261
- suggestion_attr: pair_attr(:input_suggestion, fallback: input_attr),
253
+ attr: counts_attr,
262
254
  )
263
255
 
264
- cursor_x = session.field.cursor_x.to_i.clamp(0, [inner_w - 1, 0].max)
265
- win.setpos(1 + input_row, 1 + cursor_x)
256
+ if filter_present
257
+ render_popup_filter(
258
+ inner,
259
+ session: session,
260
+ row: input_row,
261
+ width: inner_w,
262
+ attr: input_attr,
263
+ )
264
+ cursor_x = session.field.cursor_x.to_i.clamp(0, [inner_w - 1, 0].max)
265
+ win.setpos(1 + input_row, 1 + cursor_x)
266
+ end
266
267
 
267
268
  stage_window(win)
268
269
  rescue RuntimeError => e
@@ -275,11 +276,6 @@ module Fatty
275
276
  end
276
277
 
277
278
  def render_prompt_popup(session:)
278
- state = prompt_popup_state(session)
279
- return if state == @last_prompt_popup_state
280
-
281
- @last_prompt_popup_state = state
282
-
283
279
  win = session.win
284
280
  return unless win
285
281
 
@@ -331,6 +327,7 @@ module Fatty
331
327
  )
332
328
 
333
329
  cursor_x = session.field.cursor_x.to_i.clamp(0, [inner_w - 1, 0].max)
330
+ show_cursor
334
331
  win.setpos(1 + input_row, 1 + cursor_x)
335
332
 
336
333
  stage_window(inner)
@@ -382,15 +379,117 @@ module Fatty
382
379
  stage_window(win)
383
380
  end
384
381
 
382
+ def hide_cursor
383
+ ::Curses.curs_set(0)
384
+ self
385
+ end
386
+
387
+ def show_cursor
388
+ ::Curses.curs_set(1)
389
+ self
390
+ end
391
+
385
392
  private
386
393
 
394
+ # simplecov:disable
395
+
396
+ def render_popup_title(win, session:, width:)
397
+ return unless session.title && !session.title.empty?
398
+
399
+ title = Fatty::Ansi.strip(session.title.to_s).tr("\r\n", " ")
400
+ title = Fatty::Ansi.truncate_visible(" #{title} ", [width - 4, 0].max)
401
+ return if title.empty?
402
+
403
+ win.setpos(0, 2)
404
+ win.addstr(title)
405
+ end
406
+
407
+ def render_popup_message(win, session:, row:, width:, attr:)
408
+ return row unless session.message && !session.message.empty?
409
+
410
+ message = Fatty::Ansi.strip(session.message.to_s).tr("\r\n", " ")
411
+ message = Fatty::Ansi.truncate_visible(message, width)
412
+ render_popup_plain_row(win, row: row, width: width, text: message, attr: attr)
413
+
414
+ row + 1
415
+ end
416
+
417
+ def render_popup_items(win, session:, row:, height:, width:, popup_attr:, selected_attr:)
418
+ items = session.displayed
419
+ selected = session.current
420
+ start = session.scroll_start(list_h: height)
421
+
422
+ (0...height).each do |offset|
423
+ item_index = start + offset
424
+ item_selected = item_index == selected
425
+ attr = item_selected ? selected_attr : popup_attr
426
+ text = popup_item_text(session, items: items, item_index: item_index, selected: item_selected, width: width)
427
+
428
+ render_popup_plain_row(
429
+ win,
430
+ row: row + offset,
431
+ width: width,
432
+ text: text,
433
+ attr: attr,
434
+ )
435
+ end
436
+ end
437
+
438
+ def popup_item_text(session, items:, item_index:, selected:, width:)
439
+ line = ""
440
+
441
+ if item_index < items.length
442
+ item = items[item_index]
443
+ gutter = session.gutter_for(item: item, selected: selected)
444
+ text = Fatty::Ansi.strip(item.to_s).tr("\r\n", " ")
445
+ available = [width - Fatty::Ansi.visible_length(gutter), 0].max
446
+ text = Fatty::Ansi.truncate_visible(text, available)
447
+ line = Fatty::Ansi.truncate_visible(gutter + text, width)
448
+ end
449
+
450
+ line
451
+ end
452
+
453
+ def render_popup_counts(win, session:, row:, width:, attr:)
454
+ return unless row && row >= 0
455
+
456
+ text = Fatty::Ansi.truncate_visible(popup_counts_text(session), width)
457
+ render_popup_plain_row(win, row: row, width: width, text: text, attr: attr)
458
+ end
459
+
460
+ def render_popup_filter(win, session:, row:, width:, attr:)
461
+ render_popup_plain_row(win, row: row, width: width, text: "", attr: attr)
462
+
463
+ render_field_into(
464
+ win: win,
465
+ field: session.field,
466
+ row: row,
467
+ width: width,
468
+ base_role: :popup_input,
469
+ base_attr: attr,
470
+ region_attr: pair_attr(:region, fallback: ::Curses::A_REVERSE),
471
+ suggestion_attr: pair_attr(:input_suggestion, fallback: attr),
472
+ )
473
+ end
474
+
475
+ def render_popup_plain_row(win, row:, width:, text:, attr:)
476
+ line = Fatty::Ansi.truncate_visible(text.to_s, width)
477
+ line = line.ljust(width)
478
+
479
+ win.attrset(attr)
480
+ win.setpos(row, 0)
481
+ win.addstr(line)
482
+ end
483
+
387
484
  def available_colors
388
485
  ::Curses.colors
389
486
  end
390
487
 
488
+ def ansi_lines?(lines)
489
+ lines.any? { |line| line.to_s.include?("\e[") }
490
+ end
491
+
391
492
  def after_apply_theme!
392
- @ansi_pair_cache = {}
393
- @next_ansi_pair_id = nil
394
493
  context.apply_palette(palette)
395
494
  end
396
495
 
@@ -409,49 +508,105 @@ module Fatty
409
508
  delta != 0 && delta.abs < curr[:height]
410
509
  end
411
510
 
412
- def scroll_output_window_delta!(prev:, curr:)
511
+ def scroll_output_window_delta!(delta:, lines:, viewport:, highlights:)
413
512
  Fatty.debug("calling scroll_output_window_delta!", tag: :scrolling)
414
513
  win = context.output_win
415
- delta = curr[:top] - prev[:top]
416
- base_attr = pair_attr(:output, fallback: ::Curses::A_NORMAL)
514
+ return unless win
417
515
 
516
+ height = viewport.height
517
+ base_attr = pair_attr(:output, fallback: ::Curses::A_NORMAL)
418
518
  win.attrset(base_attr)
419
519
  win.scrl(delta)
420
-
421
- if delta > 0
422
- start_y = curr[:height] - delta
520
+ if delta.positive?
521
+ start_y = height - delta
423
522
  start_y = 0 if start_y < 0
424
523
 
425
- (start_y...curr[:height]).each do |y|
426
- line = curr[:lines][y]
427
- abs_line = curr[:top] + y
524
+ (start_y...height).each do |y|
428
525
  draw_output_row(
429
526
  win,
430
- line: line,
527
+ line: lines[y],
431
528
  y: y,
432
- abs_line: abs_line,
433
- highlights: curr[:highlights],
529
+ abs_line: viewport.top + y,
530
+ highlights: highlights,
434
531
  )
435
532
  end
436
533
  else
437
534
  count = -delta
438
- count = curr[:height] if count > curr[:height]
535
+ count = height if count > height
536
+
439
537
  (0...count).each do |y|
440
- line = curr[:lines][y]
441
- abs_line = curr[:top] + y
442
538
  draw_output_row(
443
539
  win,
444
- line: line,
540
+ line: lines[y],
445
541
  y: y,
446
- abs_line: abs_line,
447
- highlights: curr[:highlights],
542
+ abs_line: viewport.top + y,
543
+ highlights: highlights,
448
544
  )
449
545
  end
450
546
  end
451
547
  stage_window(win)
452
548
  end
453
549
 
454
- def draw_output_lines(lines, viewport:, highlights: nil)
550
+ def draw_status_lines(status_session)
551
+ win = context.status_win
552
+ return unless win
553
+
554
+ rect = screen.status_rect
555
+ base_attr = pair_attr(status_session.role, fallback: ::Curses::A_REVERSE)
556
+ lines = status_segment_lines(status_session)
557
+ win.bkgdset(base_attr) if win.respond_to?(:bkgdset)
558
+ win.erase
559
+ rect.rows.times do |row|
560
+ draw_status_row(
561
+ win,
562
+ row: row,
563
+ cols: rect.cols,
564
+ segments: lines[row] || [],
565
+ base_attr: base_attr,
566
+ )
567
+ end
568
+ stage_window(win)
569
+ end
570
+
571
+ def draw_status_row(win, row:, cols:, segments:, base_attr:)
572
+ win.setpos(row, 0)
573
+ remaining = cols
574
+
575
+ segments.each do |segment|
576
+ break if remaining <= 0
577
+
578
+ text = segment[:text].to_s.tr("\r\n", " ")
579
+ text = Fatty::Ansi.truncate_visible(text, remaining)
580
+ next if text.empty?
581
+
582
+ segment_role = segment[:role] || :status
583
+ role_attr = pair_attr(segment_role, fallback: base_attr)
584
+
585
+ attr =
586
+ if segment[:style]
587
+ ansi_style_attr(
588
+ segment[:style],
589
+ fallback: role_attr,
590
+ fallback_role: segment_role,
591
+ )
592
+ else
593
+ role_attr
594
+ end
595
+
596
+ win.attrset(attr)
597
+ win.addstr(text)
598
+ remaining -= Fatty::Ansi.visible_length(text)
599
+ end
600
+
601
+ if remaining.positive?
602
+ win.attrset(base_attr)
603
+ win.addstr(" " * remaining)
604
+ end
605
+ end
606
+
607
+ def draw_output_lines(lines, viewport:, highlights: nil, line_number_width: nil)
608
+ context.reset_ansi_pairs!
609
+
455
610
  win = context.output_win
456
611
  base_attr = pair_attr(:output, fallback: ::Curses::A_NORMAL)
457
612
 
@@ -459,40 +614,48 @@ module Fatty
459
614
  win.bkgdset(base_attr) if win.respond_to?(:bkgdset)
460
615
  win.erase
461
616
 
462
- lines.each_with_index do |line, y|
463
- abs_line = viewport.top + y
617
+ lines.each_with_index do |visible_line, y|
464
618
  draw_output_row(
465
619
  win,
466
- line: line,
620
+ line: visible_line.text,
467
621
  y: y,
468
- abs_line: abs_line,
622
+ abs_line: visible_line.number - 1,
469
623
  highlights: highlights,
624
+ line_number: visible_line.number,
625
+ line_number_width: line_number_width,
470
626
  )
471
627
  end
472
628
  stage_window(win)
473
629
  end
474
630
 
475
- def draw_output_row(win, line:, y:, abs_line:, highlights:)
631
+ def draw_output_row(win, line:, y:, abs_line:, highlights:, line_number: nil, line_number_width: nil)
476
632
  base_attr = pair_attr(:output, fallback: ::Curses::A_NORMAL)
633
+ line_number_attr = base_attr | ::Curses::A_DIM
477
634
  hi_attr = pair_attr(:match_current, fallback: ::Curses::A_REVERSE)
478
635
  hi2_attr = pair_attr(:match_other, fallback: hi_attr)
479
636
 
480
637
  semantic_ranges = highlight_ranges_for_line(highlights, abs_line)
481
638
  win.setpos(y, 0)
482
639
  win.attrset(base_attr)
640
+
641
+ line_number_attr = pair_attr(:line_number, fallback: base_attr | ::Curses::A_DIM)
642
+ if line_number && line_number_width
643
+ number_text = line_number.to_s.rjust(line_number_width, "0")
644
+ win.attrset(line_number_attr)
645
+ win.addstr("#{number_text}: ")
646
+ win.attrset(base_attr)
647
+ end
648
+
483
649
  curses_ranges =
484
650
  Array(semantic_ranges).map do |from, to, role|
485
- attr =
486
- case role
487
- when :secondary then hi2_attr
488
- else hi_attr
489
- end
490
- [from.to_i, to.to_i, attr]
491
- end
492
- # plain = Fatty::Ansi.plain_text(line.to_s)
493
- # slices = build_line_slices(plain, ranges: curses_ranges) do |_style|
494
- # base_attr
495
- # end
651
+ attr =
652
+ case role
653
+ when :secondary then hi2_attr
654
+ else hi_attr
655
+ end
656
+ [from.to_i, to.to_i, attr]
657
+ end
658
+
496
659
  slices = build_line_slices(line.to_s, ranges: curses_ranges) do |style|
497
660
  ansi_style_attr(style, fallback: base_attr)
498
661
  end
@@ -625,13 +788,16 @@ module Fatty
625
788
  break if remaining <= 0
626
789
 
627
790
  attr =
628
- case segment[:role]
629
- when :region then region_attr
630
- when :input_suggestion then suggestion_attr
631
- else base_attr
791
+ if segment[:style]
792
+ ansi_style_attr(segment[:style], fallback: base_attr, fallback_role: base_role)
793
+ else
794
+ case segment[:role]
795
+ when :region then region_attr
796
+ when :input_suggestion then suggestion_attr
797
+ else base_attr
798
+ end
632
799
  end
633
-
634
- text = Fatty::Ansi.strip(segment[:text].to_s).tr("\r\n", " ")
800
+ text = segment[:text].to_s.tr("\r\n", " ")
635
801
  text = Fatty::Ansi.truncate_visible(text, remaining)
636
802
  next if text.empty?
637
803
 
@@ -641,18 +807,27 @@ module Fatty
641
807
  end
642
808
  end
643
809
 
644
- def ansi_style_attr(style, fallback:)
810
+ def ansi_style_attr(style, fallback:, fallback_role: :output)
645
811
  attr = fallback
812
+
646
813
  if style.fg || style.bg
647
814
  fg = curses_color_for_style(style.fg)
648
815
  bg = curses_color_for_style(style.bg)
649
816
 
650
- output_spec = palette[:output] || {}
651
- fg = output_spec[:fg] if fg.nil?
652
- bg = output_spec[:bg] if bg.nil?
817
+ role_spec = palette[fallback_role] || palette[:output] || {}
818
+ fg = role_spec[:fg] if fg.nil?
819
+ bg = role_spec[:bg] if bg.nil?
653
820
 
654
- attr = ::Curses.color_pair(ansi_pair_for(fg, bg))
821
+ fallback_pair_id = Fatty::Colors::Pairs::ROLE_TO_PAIR.fetch(fallback_role)
822
+ pair_id = context.ansi_pair_for(
823
+ fg,
824
+ bg,
825
+ fallback_pair_id: fallback_pair_id,
826
+ )
827
+
828
+ attr = ::Curses.color_pair(pair_id)
655
829
  end
830
+
656
831
  attr |= ::Curses::A_BOLD if style.bold
657
832
  attr |= ::Curses::A_DIM if style.respond_to?(:dim) && style.dim
658
833
  attr |= ::Curses::A_UNDERLINE if style.underline
@@ -673,25 +848,6 @@ module Fatty
673
848
  )
674
849
  end
675
850
  end
676
-
677
- def ansi_pair_for(fg, bg)
678
- @ansi_pair_cache ||= {}
679
- key = [fg.to_i, bg.to_i]
680
-
681
- @ansi_pair_cache[key] ||=
682
- begin
683
- pair_id = next_ansi_pair_id
684
- ::Curses.init_pair(pair_id, fg.to_i, bg.to_i)
685
- pair_id
686
- end
687
- end
688
-
689
- def next_ansi_pair_id
690
- @next_ansi_pair_id ||= Fatty::Colors::Pairs::ROLE_TO_PAIR.values.max + 1
691
- id = @next_ansi_pair_id
692
- @next_ansi_pair_id += 1
693
- id
694
- end
695
851
  end
696
852
  end
697
853
  end