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.
- checksums.yaml +7 -0
- data/.envrc +2 -0
- data/.simplecov +23 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +34 -0
- data/CHANGELOG.org +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +31 -0
- data/README.org +166 -0
- data/Rakefile +15 -0
- data/TODO.org +163 -0
- data/examples/markdown/native-markdown.md +370 -0
- data/examples/markdown/ox-gfm-markdown.md +373 -0
- data/examples/markdown/ox-gfm-markdown.org +376 -0
- data/exe/fatty +275 -0
- data/fatty.gemspec +42 -0
- data/lib/fatty/accept_env.rb +32 -0
- data/lib/fatty/action.rb +103 -0
- data/lib/fatty/action_environment.rb +42 -0
- data/lib/fatty/actionable.rb +73 -0
- data/lib/fatty/alert.rb +93 -0
- data/lib/fatty/ansi/renderer.rb +168 -0
- data/lib/fatty/ansi.rb +352 -0
- data/lib/fatty/colors/color.rb +379 -0
- data/lib/fatty/colors/pairs.rb +73 -0
- data/lib/fatty/colors/palette.rb +73 -0
- data/lib/fatty/colors/rgb.txt +788 -0
- data/lib/fatty/colors.rb +5 -0
- data/lib/fatty/config.rb +86 -0
- data/lib/fatty/config_files/config.yml +50 -0
- data/lib/fatty/config_files/help.md +120 -0
- data/lib/fatty/config_files/help.org +124 -0
- data/lib/fatty/config_files/keybindings.yml +49 -0
- data/lib/fatty/config_files/keydefs.yml +23 -0
- data/lib/fatty/config_files/themes/mono.yml +76 -0
- data/lib/fatty/config_files/themes/nordic.yml +77 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
- data/lib/fatty/config_files/themes/terminal.yml +90 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
- data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
- data/lib/fatty/core_ext/string.rb +21 -0
- data/lib/fatty/core_ext.rb +3 -0
- data/lib/fatty/counter.rb +81 -0
- data/lib/fatty/curses/context.rb +279 -0
- data/lib/fatty/curses/curses_coder.rb +684 -0
- data/lib/fatty/curses/event_source.rb +230 -0
- data/lib/fatty/curses/key_decoder.rb +183 -0
- data/lib/fatty/curses/patch.rb +116 -0
- data/lib/fatty/curses/window_styling.rb +32 -0
- data/lib/fatty/curses.rb +16 -0
- data/lib/fatty/env.rb +100 -0
- data/lib/fatty/help.rb +41 -0
- data/lib/fatty/history/entry.rb +71 -0
- data/lib/fatty/history.rb +289 -0
- data/lib/fatty/input_buffer.rb +998 -0
- data/lib/fatty/input_field.rb +507 -0
- data/lib/fatty/key_event.rb +342 -0
- data/lib/fatty/key_map.rb +392 -0
- data/lib/fatty/keymaps/emacs.rb +189 -0
- data/lib/fatty/log_formats/json.rb +47 -0
- data/lib/fatty/log_formats/text.rb +67 -0
- data/lib/fatty/logger.rb +142 -0
- data/lib/fatty/markdown/ansi_renderer.rb +373 -0
- data/lib/fatty/markdown/render.rb +22 -0
- data/lib/fatty/markdown.rb +4 -0
- data/lib/fatty/menu_env.rb +22 -0
- data/lib/fatty/mouse_event.rb +32 -0
- data/lib/fatty/output_buffer.rb +78 -0
- data/lib/fatty/pager.rb +801 -0
- data/lib/fatty/prompt.rb +40 -0
- data/lib/fatty/renderer/curses.rb +697 -0
- data/lib/fatty/renderer/truecolor.rb +607 -0
- data/lib/fatty/renderer.rb +419 -0
- data/lib/fatty/screen.rb +96 -0
- data/lib/fatty/search.rb +43 -0
- data/lib/fatty/session/alert_session.rb +52 -0
- data/lib/fatty/session/input_session.rb +99 -0
- data/lib/fatty/session/isearch_session.rb +172 -0
- data/lib/fatty/session/keytest_session.rb +236 -0
- data/lib/fatty/session/modal_session.rb +61 -0
- data/lib/fatty/session/output_session.rb +105 -0
- data/lib/fatty/session/popup_session.rb +540 -0
- data/lib/fatty/session/prompt_session.rb +157 -0
- data/lib/fatty/session/search_session.rb +136 -0
- data/lib/fatty/session/shell_session.rb +566 -0
- data/lib/fatty/session.rb +173 -0
- data/lib/fatty/sessions.rb +14 -0
- data/lib/fatty/terminal/popup_owner.rb +26 -0
- data/lib/fatty/terminal/progress.rb +374 -0
- data/lib/fatty/terminal.rb +1067 -0
- data/lib/fatty/themes/loader.rb +136 -0
- data/lib/fatty/themes/manager.rb +71 -0
- data/lib/fatty/themes/registry.rb +64 -0
- data/lib/fatty/themes/resolver.rb +224 -0
- data/lib/fatty/themes/themes.rb +131 -0
- data/lib/fatty/themes.rb +6 -0
- data/lib/fatty/version.rb +5 -0
- data/lib/fatty/view/alert_view.rb +14 -0
- data/lib/fatty/view/cursor_view.rb +18 -0
- data/lib/fatty/view/input_view.rb +9 -0
- data/lib/fatty/view/output_view.rb +9 -0
- data/lib/fatty/view/status_view.rb +14 -0
- data/lib/fatty/view.rb +33 -0
- data/lib/fatty/viewport.rb +90 -0
- data/lib/fatty/views.rb +9 -0
- data/lib/fatty.rb +55 -0
- data/sig/fatty.rbs +4 -0
- 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
|