fatty 0.99.0 → 0.99.4

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 +421 -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 +47 -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
@@ -0,0 +1,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class Progress
5
+ PARTIAL_BLOCKS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"].freeze
6
+ FULL_BLOCK = "█"
7
+ EMPTY_BAR = "."
8
+
9
+ SHADE_EMPTY = "░"
10
+ SHADE_HALF = "▒"
11
+ SHADE_FULL = "▓"
12
+
13
+ BRAILLE_STEPS = ["⠀", "⣀", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"].freeze
14
+
15
+ STYLES = %i[
16
+ count
17
+ percent
18
+ count_percent
19
+ trail
20
+ bar
21
+ unicode_bar
22
+ braille_bar
23
+ spinner
24
+ ].freeze
25
+
26
+ DEFAULT_LABEL = "Progress"
27
+ DEFAULT_ROLE = :info
28
+ DEFAULT_STYLE = :percent
29
+ DEFAULT_WIDTH = 40
30
+
31
+ attr_reader :terminal, :label, :total, :current, :style, :role, :width
32
+
33
+ # The width parameter's purpose varies by style:
34
+ #
35
+ # | Style | Width Purpose |
36
+ # |----------------+---------------------------------------------------|
37
+ # | :trail | width = max visible width of the trail portion |
38
+ # | :bar | width = max bar width |
39
+ # | :unicode_bar | width = max bar width |
40
+ # | :braille_bar | width = max bar width |
41
+ # | :spinner | width ignored, unless later used for suffix/trail |
42
+ # | :count | width ignored |
43
+ # | :percent | width ignored |
44
+ # | :count_percent | width ignored |
45
+ def initialize(
46
+ terminal:,
47
+ label: DEFAULT_LABEL,
48
+ total: nil,
49
+ style: DEFAULT_STYLE,
50
+ role: DEFAULT_ROLE,
51
+ width: DEFAULT_WIDTH
52
+ )
53
+ @terminal = terminal
54
+ @label = normalize_label(label)
55
+ @total = normalize_total(total)
56
+ @style = normalize_style(style)
57
+ @role = normalize_role(role)
58
+ @width = normalize_width(width)
59
+ @trail = []
60
+ @current = 0
61
+ @spinner_index = 0
62
+ validate_total_requirement!
63
+ refresh
64
+ end
65
+
66
+ def update(current: nil, indicator: nil, render: false)
67
+ @current = normalize_current(current) unless current.nil?
68
+ if style == :spinner
69
+ advance_spinner
70
+ else
71
+ append_indicator(indicator)
72
+ end
73
+ refresh(render:)
74
+ self
75
+ end
76
+
77
+ def finish(message = nil, clear: false, role: @role, render: false)
78
+ if clear
79
+ clear_status
80
+ else
81
+ text =
82
+ if message && !message.empty?
83
+ render_text(suffix: message)
84
+ else
85
+ render_text
86
+ end
87
+ show_status(text, role: role)
88
+ end
89
+ terminal.render_frame if render
90
+ self
91
+ end
92
+
93
+ def clear
94
+ clear_status
95
+ self
96
+ end
97
+
98
+ private
99
+
100
+ # simplecov:disable
101
+
102
+ def normalize_label(value)
103
+ text = value.to_s.strip
104
+ text.empty? ? DEFAULT_LABEL : text
105
+ end
106
+
107
+ # Return the total but not less than 1 to avoid division-by-zero errors
108
+ # when calculating percentages.
109
+ def normalize_total(value)
110
+ return if value.nil?
111
+
112
+ val = Integer(value, exception: false)
113
+ val = 1 unless val
114
+ [1, val].max
115
+ end
116
+
117
+ def normalize_style(value)
118
+ candidate = value&.to_sym || DEFAULT_STYLE
119
+ return candidate if STYLES.include?(candidate)
120
+
121
+ raise ArgumentError, "unknown progress style: #{value.inspect}"
122
+ end
123
+
124
+ def normalize_role(value)
125
+ text = value.to_s.strip
126
+ return DEFAULT_ROLE if text.empty?
127
+
128
+ candidate = text.downcase.to_sym
129
+ return candidate if %i[good info warn error].include?(candidate)
130
+
131
+ raise ArgumentError, "progress role must be :good, :info, :warn, or :error"
132
+ end
133
+
134
+ def normalize_width(value)
135
+ candidate = Integer(value, exception: false)
136
+ candidate&.positive? ? candidate : DEFAULT_WIDTH
137
+ end
138
+
139
+ def normalize_current(value)
140
+ candidate = Integer(value, exception: false)
141
+ return candidate if candidate
142
+
143
+ raise ArgumentError, "progress current must be an integer: #{value.inspect}"
144
+ end
145
+
146
+ def show_status(text, role:)
147
+ terminal.apply_command(Command.session(:status, :show, text: text, role: role))
148
+ nil
149
+ end
150
+
151
+ def clear_status
152
+ terminal.apply_command(Command.session(:status, :clear))
153
+ nil
154
+ end
155
+
156
+ def append_indicator(indicator)
157
+ return if indicator.nil?
158
+
159
+ item = normalize_indicator(indicator)
160
+ return if renderable_indicator_text(item).empty?
161
+
162
+ @trail << item
163
+ end
164
+
165
+ def refresh(render: false)
166
+ terminal.without_cursor_restore do
167
+ terminal.apply_command(
168
+ Command.session(
169
+ :status,
170
+ :show,
171
+ text: render_text,
172
+ role: role,
173
+ )
174
+ )
175
+ terminal.render_frame if render
176
+ end
177
+ nil
178
+ end
179
+
180
+ def render_text(suffix: nil)
181
+ case style
182
+ when :count
183
+ base = total ? "#{label} [#{current}/#{total}]" : "#{label} [#{current}]"
184
+ suffix_text(base, suffix)
185
+ when :percent
186
+ base = total && total > 0 ? "#{label} #{percent}%" : label.to_s
187
+ suffix_text(base, suffix)
188
+ when :count_percent
189
+ base =
190
+ if total && total > 0
191
+ "#{label} [#{current}/#{total}] #{percent}%"
192
+ else
193
+ "#{label} [#{current}]"
194
+ end
195
+ suffix_text(base, suffix)
196
+ when :trail
197
+ render_trail_text(suffix:)
198
+ when :bar
199
+ render_bar_text(suffix:, mode: :solid)
200
+ when :unicode_bar
201
+ render_bar_text(suffix:, mode: :unicode)
202
+ when :braille_bar
203
+ render_bar_text(suffix:, mode: :braille)
204
+ when :spinner
205
+ render_spinner_text(suffix:)
206
+ else
207
+ raise ArgumentError, "unknown progress style: #{style.inspect}"
208
+ end
209
+ end
210
+
211
+ def percent
212
+ ((current.to_f / total.to_f) * 100).round
213
+ end
214
+
215
+ def suffix_text(base, suffix)
216
+ extra = suffix.to_s
217
+ return base if extra.empty?
218
+
219
+ "#{base} #{extra}"
220
+ end
221
+
222
+ def render_trail_text(suffix: nil)
223
+ base =
224
+ if total && total > 0
225
+ "#{label} [#{current}/#{total}] #{percent}%"
226
+ else
227
+ "#{label} [#{current}]"
228
+ end
229
+
230
+ trailer = suffix.to_s.empty? ? "" : " #{suffix}"
231
+ return "#{base}#{trailer}" if @trail.empty?
232
+
233
+ [base, " ", *@trail, trailer]
234
+ end
235
+
236
+ def visible_length(value)
237
+ if value.is_a?(Hash) && value.key?(:text)
238
+ Fatty::Ansi.visible_length(value[:text].to_s)
239
+ else
240
+ Fatty::Ansi.visible_length(value.to_s)
241
+ end
242
+ end
243
+
244
+ def normalize_indicator(indicator)
245
+ if indicator.is_a?(Hash) && indicator.key?(:text)
246
+ indicator.merge(text: renderable_indicator_text(indicator))
247
+ else
248
+ renderable_indicator_text(indicator)
249
+ end
250
+ end
251
+
252
+ def renderable_indicator_text(item)
253
+ text =
254
+ if item.is_a?(Hash) && item.key?(:text)
255
+ item[:text].to_s
256
+ else
257
+ item.to_s
258
+ end
259
+ text.gsub(/\s+/, " ").strip
260
+ end
261
+
262
+ def render_bar_text(suffix: nil, mode: :solid)
263
+ base = label.to_s
264
+ info = progress_info_text
265
+ extra = suffix.to_s
266
+ trailer = extra.empty? ? "" : " #{extra}"
267
+
268
+ bar_width = bar_width_for(base, info, trailer)
269
+ bar =
270
+ case mode
271
+ when :unicode
272
+ unicode_bar(bar_width)
273
+ when :braille
274
+ braille_bar(bar_width)
275
+ else
276
+ solid_bar(bar_width)
277
+ end
278
+
279
+ pieces = [base]
280
+ pieces << "[#{bar}]" unless bar.empty?
281
+ pieces << info unless info.empty?
282
+
283
+ text = pieces.join(" ")
284
+ text = "#{text}#{trailer}" unless trailer.empty?
285
+ text
286
+ end
287
+
288
+ def render_spinner_text(suffix: nil)
289
+ frame = spinner_frames[@spinner_index % spinner_frames.length]
290
+ base =
291
+ if total && total > 0
292
+ "#{label} #{frame} #{percent}%"
293
+ else
294
+ "#{label} #{frame}"
295
+ end
296
+
297
+ suffix_text(base, suffix)
298
+ end
299
+
300
+ def progress_info_text
301
+ if total && total > 0
302
+ "#{percent}% [#{current}/#{total}]"
303
+ else
304
+ "[#{current}]"
305
+ end
306
+ end
307
+
308
+ def bar_width_for(base, info, trailer)
309
+ cols =
310
+ if terminal.screen
311
+ terminal.screen.cols.to_i
312
+ else
313
+ 80
314
+ end
315
+
316
+ reserved = visible_length(base)
317
+ reserved += 1 + visible_length(info) unless info.empty?
318
+ reserved += visible_length(trailer) unless trailer.empty?
319
+
320
+ # Space for:
321
+ # " ["
322
+ # "]"
323
+ # around the bar
324
+ available = cols - reserved - 4
325
+ available = 0 if available < 0
326
+ [available, @width].min
327
+ end
328
+
329
+ def solid_bar(width)
330
+ return "" if width <= 0
331
+
332
+ ratio = progress_ratio
333
+ filled = (ratio * width).round
334
+ filled = 0 if filled < 0
335
+ filled = width if filled > width
336
+
337
+ empty = width - filled
338
+ (FULL_BLOCK * filled) + (EMPTY_BAR * empty)
339
+ end
340
+
341
+ def unicode_bar(width)
342
+ return "" if width <= 0
343
+
344
+ raw = progress_ratio * width
345
+ full = raw.floor
346
+
347
+ # Show a transition block whenever the bar is in progress but not complete,
348
+ # provided there is space for it.
349
+ half =
350
+ if progress_ratio.positive? && progress_ratio < 1.0 && full < width
351
+ 1
352
+ else
353
+ 0
354
+ end
355
+
356
+ bar = +""
357
+ bar << (SHADE_FULL * full)
358
+ bar << SHADE_HALF if half == 1
359
+
360
+ remaining = width - Fatty::Ansi.visible_length(bar)
361
+ remaining = 0 if remaining < 0
362
+ bar << (SHADE_EMPTY * remaining)
363
+ bar
364
+ end
365
+
366
+ def braille_bar(width)
367
+ return "" if width <= 0
368
+
369
+ raw = progress_ratio * width
370
+ full = raw.floor
371
+ remainder = raw - full
372
+
373
+ partial_idx = (remainder * (BRAILLE_STEPS.length - 1)).round
374
+ partial_idx = 0 if partial_idx < 0
375
+ partial_idx = BRAILLE_STEPS.length - 1 if partial_idx >= BRAILLE_STEPS.length
376
+
377
+ bar = +""
378
+ bar << (BRAILLE_STEPS[-1] * full)
379
+
380
+ if progress_ratio.positive? && progress_ratio < 1.0 && full < width
381
+ partial =
382
+ if partial_idx.zero?
383
+ BRAILLE_STEPS[1]
384
+ else
385
+ BRAILLE_STEPS[partial_idx]
386
+ end
387
+ bar << partial
388
+ end
389
+
390
+ remaining = width - Fatty::Ansi.visible_length(bar)
391
+ remaining = 0 if remaining < 0
392
+ bar << (BRAILLE_STEPS[0] * remaining)
393
+ bar
394
+ end
395
+
396
+ def progress_ratio
397
+ return 0.0 unless total && total > 0
398
+
399
+ ratio = current.to_f / total.to_f
400
+ ratio = 0.0 if ratio < 0.0
401
+ ratio = 1.0 if ratio > 1.0
402
+ ratio
403
+ end
404
+
405
+ def validate_total_requirement!
406
+ return if style == :spinner && total.nil?
407
+ return if total&.positive?
408
+
409
+ raise ArgumentError,
410
+ "progress style #{style.inspect} requires a positive integer total:"
411
+ end
412
+
413
+ def advance_spinner
414
+ @spinner_index = (@spinner_index + 1) % spinner_frames.length
415
+ end
416
+
417
+ def spinner_frames
418
+ BRAILLE_STEPS[1..]
419
+ end
420
+ end
421
+ end