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
@@ -0,0 +1,417 @@
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
+ def normalize_total(value)
108
+ return if value.nil?
109
+
110
+ Integer(value, exception: false)
111
+ end
112
+
113
+ def normalize_style(value)
114
+ candidate = value&.to_sym || DEFAULT_STYLE
115
+ return candidate if STYLES.include?(candidate)
116
+
117
+ raise ArgumentError, "unknown progress style: #{value.inspect}"
118
+ end
119
+
120
+ def normalize_role(value)
121
+ text = value.to_s.strip
122
+ return DEFAULT_ROLE if text.empty?
123
+
124
+ candidate = text.downcase.to_sym
125
+ return candidate if %i[good info warn error].include?(candidate)
126
+
127
+ raise ArgumentError, "progress role must be :good, :info, :warn, or :error"
128
+ end
129
+
130
+ def normalize_width(value)
131
+ candidate = Integer(value, exception: false)
132
+ candidate&.positive? ? candidate : DEFAULT_WIDTH
133
+ end
134
+
135
+ def normalize_current(value)
136
+ candidate = Integer(value, exception: false)
137
+ return candidate if candidate
138
+
139
+ raise ArgumentError, "progress current must be an integer: #{value.inspect}"
140
+ end
141
+
142
+ def show_status(text, role:)
143
+ terminal.apply_command(Command.session(:status, :show, text: text, role: role))
144
+ nil
145
+ end
146
+
147
+ def clear_status
148
+ terminal.apply_command(Command.session(:status, :clear))
149
+ nil
150
+ end
151
+
152
+ def append_indicator(indicator)
153
+ return if indicator.nil?
154
+
155
+ item = normalize_indicator(indicator)
156
+ return if renderable_indicator_text(item).empty?
157
+
158
+ @trail << item
159
+ end
160
+
161
+ def refresh(render: false)
162
+ terminal.without_cursor_restore do
163
+ terminal.apply_command(
164
+ Command.session(
165
+ :status,
166
+ :show,
167
+ text: render_text,
168
+ role: role,
169
+ )
170
+ )
171
+ terminal.render_frame if render
172
+ end
173
+ nil
174
+ end
175
+
176
+ def render_text(suffix: nil)
177
+ case style
178
+ when :count
179
+ base = total ? "#{label} [#{current}/#{total}]" : "#{label} [#{current}]"
180
+ suffix_text(base, suffix)
181
+ when :percent
182
+ base = total && total > 0 ? "#{label} #{percent}%" : label.to_s
183
+ suffix_text(base, suffix)
184
+ when :count_percent
185
+ base =
186
+ if total && total > 0
187
+ "#{label} [#{current}/#{total}] #{percent}%"
188
+ else
189
+ "#{label} [#{current}]"
190
+ end
191
+ suffix_text(base, suffix)
192
+ when :trail
193
+ render_trail_text(suffix:)
194
+ when :bar
195
+ render_bar_text(suffix:, mode: :solid)
196
+ when :unicode_bar
197
+ render_bar_text(suffix:, mode: :unicode)
198
+ when :braille_bar
199
+ render_bar_text(suffix:, mode: :braille)
200
+ when :spinner
201
+ render_spinner_text(suffix:)
202
+ else
203
+ raise ArgumentError, "unknown progress style: #{style.inspect}"
204
+ end
205
+ end
206
+
207
+ def percent
208
+ ((current.to_f / total.to_f) * 100).round
209
+ end
210
+
211
+ def suffix_text(base, suffix)
212
+ extra = suffix.to_s
213
+ return base if extra.empty?
214
+
215
+ "#{base} #{extra}"
216
+ end
217
+
218
+ def render_trail_text(suffix: nil)
219
+ base =
220
+ if total && total > 0
221
+ "#{label} [#{current}/#{total}] #{percent}%"
222
+ else
223
+ "#{label} [#{current}]"
224
+ end
225
+
226
+ trailer = suffix.to_s.empty? ? "" : " #{suffix}"
227
+ return "#{base}#{trailer}" if @trail.empty?
228
+
229
+ [base, " ", *@trail, trailer]
230
+ end
231
+
232
+ def visible_length(value)
233
+ if value.is_a?(Hash) && value.key?(:text)
234
+ Fatty::Ansi.visible_length(value[:text].to_s)
235
+ else
236
+ Fatty::Ansi.visible_length(value.to_s)
237
+ end
238
+ end
239
+
240
+ def normalize_indicator(indicator)
241
+ if indicator.is_a?(Hash) && indicator.key?(:text)
242
+ indicator.merge(text: renderable_indicator_text(indicator))
243
+ else
244
+ renderable_indicator_text(indicator)
245
+ end
246
+ end
247
+
248
+ def renderable_indicator_text(item)
249
+ text =
250
+ if item.is_a?(Hash) && item.key?(:text)
251
+ item[:text].to_s
252
+ else
253
+ item.to_s
254
+ end
255
+ text.gsub(/\s+/, " ").strip
256
+ end
257
+
258
+ def render_bar_text(suffix: nil, mode: :solid)
259
+ base = label.to_s
260
+ info = progress_info_text
261
+ extra = suffix.to_s
262
+ trailer = extra.empty? ? "" : " #{extra}"
263
+
264
+ bar_width = bar_width_for(base, info, trailer)
265
+ bar =
266
+ case mode
267
+ when :unicode
268
+ unicode_bar(bar_width)
269
+ when :braille
270
+ braille_bar(bar_width)
271
+ else
272
+ solid_bar(bar_width)
273
+ end
274
+
275
+ pieces = [base]
276
+ pieces << "[#{bar}]" unless bar.empty?
277
+ pieces << info unless info.empty?
278
+
279
+ text = pieces.join(" ")
280
+ text = "#{text}#{trailer}" unless trailer.empty?
281
+ text
282
+ end
283
+
284
+ def render_spinner_text(suffix: nil)
285
+ frame = spinner_frames[@spinner_index % spinner_frames.length]
286
+ base =
287
+ if total && total > 0
288
+ "#{label} #{frame} #{percent}%"
289
+ else
290
+ "#{label} #{frame}"
291
+ end
292
+
293
+ suffix_text(base, suffix)
294
+ end
295
+
296
+ def progress_info_text
297
+ if total && total > 0
298
+ "#{percent}% [#{current}/#{total}]"
299
+ else
300
+ "[#{current}]"
301
+ end
302
+ end
303
+
304
+ def bar_width_for(base, info, trailer)
305
+ cols =
306
+ if terminal.screen
307
+ terminal.screen.cols.to_i
308
+ else
309
+ 80
310
+ end
311
+
312
+ reserved = visible_length(base)
313
+ reserved += 1 + visible_length(info) unless info.empty?
314
+ reserved += visible_length(trailer) unless trailer.empty?
315
+
316
+ # Space for:
317
+ # " ["
318
+ # "]"
319
+ # around the bar
320
+ available = cols - reserved - 4
321
+ available = 0 if available < 0
322
+ [available, @width].min
323
+ end
324
+
325
+ def solid_bar(width)
326
+ return "" if width <= 0
327
+
328
+ ratio = progress_ratio
329
+ filled = (ratio * width).round
330
+ filled = 0 if filled < 0
331
+ filled = width if filled > width
332
+
333
+ empty = width - filled
334
+ (FULL_BLOCK * filled) + (EMPTY_BAR * empty)
335
+ end
336
+
337
+ def unicode_bar(width)
338
+ return "" if width <= 0
339
+
340
+ raw = progress_ratio * width
341
+ full = raw.floor
342
+
343
+ # Show a transition block whenever the bar is in progress but not complete,
344
+ # provided there is space for it.
345
+ half =
346
+ if progress_ratio.positive? && progress_ratio < 1.0 && full < width
347
+ 1
348
+ else
349
+ 0
350
+ end
351
+
352
+ bar = +""
353
+ bar << (SHADE_FULL * full)
354
+ bar << SHADE_HALF if half == 1
355
+
356
+ remaining = width - Fatty::Ansi.visible_length(bar)
357
+ remaining = 0 if remaining < 0
358
+ bar << (SHADE_EMPTY * remaining)
359
+ bar
360
+ end
361
+
362
+ def braille_bar(width)
363
+ return "" if width <= 0
364
+
365
+ raw = progress_ratio * width
366
+ full = raw.floor
367
+ remainder = raw - full
368
+
369
+ partial_idx = (remainder * (BRAILLE_STEPS.length - 1)).round
370
+ partial_idx = 0 if partial_idx < 0
371
+ partial_idx = BRAILLE_STEPS.length - 1 if partial_idx >= BRAILLE_STEPS.length
372
+
373
+ bar = +""
374
+ bar << (BRAILLE_STEPS[-1] * full)
375
+
376
+ if progress_ratio.positive? && progress_ratio < 1.0 && full < width
377
+ partial =
378
+ if partial_idx.zero?
379
+ BRAILLE_STEPS[1]
380
+ else
381
+ BRAILLE_STEPS[partial_idx]
382
+ end
383
+ bar << partial
384
+ end
385
+
386
+ remaining = width - Fatty::Ansi.visible_length(bar)
387
+ remaining = 0 if remaining < 0
388
+ bar << (BRAILLE_STEPS[0] * remaining)
389
+ bar
390
+ end
391
+
392
+ def progress_ratio
393
+ return 0.0 unless total && total > 0
394
+
395
+ ratio = current.to_f / total.to_f
396
+ ratio = 0.0 if ratio < 0.0
397
+ ratio = 1.0 if ratio > 1.0
398
+ ratio
399
+ end
400
+
401
+ def validate_total_requirement!
402
+ return if style == :spinner && total.nil?
403
+ return if total&.positive?
404
+
405
+ raise ArgumentError,
406
+ "progress style #{style.inspect} requires a positive integer total:"
407
+ end
408
+
409
+ def advance_spinner
410
+ @spinner_index = (@spinner_index + 1) % spinner_frames.length
411
+ end
412
+
413
+ def spinner_frames
414
+ BRAILLE_STEPS[1..]
415
+ end
416
+ end
417
+ end