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
data/lib/fatty/pager.rb CHANGED
@@ -2,16 +2,17 @@
2
2
 
3
3
  module Fatty
4
4
  class Pager
5
- SCROLL_WHEEL_LINES = 3
5
+ SCROLL_WHEEL_LINES = 6
6
6
 
7
7
  include Fatty::Actionable
8
8
 
9
9
  action_on :pager
10
10
  attr_reader :mode
11
11
 
12
- def initialize(output:, viewport:, mode: :paging)
12
+ def initialize(output:, viewport: Viewport.new, mode: :paging, lines: nil)
13
13
  @output = output
14
14
  @viewport = viewport
15
+ @lines = lines || -> { @output.lines }
15
16
  # @mode can be :paging, or :scrolling
16
17
  # - paging: the viewport displays one page worth of output at a time,
17
18
  # - scrolling: the viewport displays output continuously as it is produced.
@@ -21,6 +22,8 @@ module Fatty
21
22
  @last_nav_dir = nil
22
23
  # Search state (used by SearchSession, but owned here so paging repeats and
23
24
  # highlighting survive after the SearchSession closes).
25
+ @isearch_snapshot = nil
26
+ @search_snapshot = nil
24
27
  @search = {
25
28
  pattern: nil,
26
29
  regex: false,
@@ -33,10 +36,48 @@ module Fatty
33
36
  }
34
37
  end
35
38
 
39
+ def quit
40
+ @paused = false
41
+ @mode = :paging
42
+ @anchor = nil
43
+ @autoscroll = false
44
+ end
45
+
36
46
  def paused?
37
47
  @mode == :paging && @paused
38
48
  end
39
49
 
50
+ def scrolling?
51
+ @mode == :scrolling
52
+ end
53
+
54
+ def toggle_paging_mode
55
+ @last_nav_dir = :down
56
+ if @mode == :paging
57
+ set_to_scrolling
58
+ else
59
+ set_to_paging
60
+ end
61
+ end
62
+
63
+ def set_to_scrolling
64
+ @mode = :scrolling
65
+ @paused = false
66
+ @autoscroll = true
67
+ end
68
+
69
+ def set_to_paging
70
+ @mode = :paging
71
+ @paused = true
72
+ @autoscroll = false
73
+ @anchor = nil
74
+ @viewport.clamp!(lines)
75
+ end
76
+
77
+ def active?
78
+ paused? || scrolling?
79
+ end
80
+
40
81
  def reserve_prompt_row?
41
82
  paused?
42
83
  end
@@ -57,23 +98,19 @@ module Fatty
57
98
  # Called by OutputSession after new text is appended.
58
99
  def on_append(ntrim:)
59
100
  @viewport.adjust_for_trim(ntrim)
60
- lines = @output.lines
61
- total = lines.size
62
-
101
+ current_lines = lines
102
+ total = current_lines.size
63
103
  case @mode
64
104
  when :scrolling
65
105
  # In scrolling mode, the viewport is allowed to move continuously.
66
106
  # Autoscroll animation (after switching from paging -> scrolling) is
67
107
  # driven by #autoscroll_step? in ShellSession#tick, not by incremental
68
108
  # scroll deltas during append.
69
- @viewport.clamp!(lines)
109
+ @viewport.clamp!(current_lines)
70
110
  when :paging
71
111
  if @anchor
72
112
  produced = total - @anchor
73
-
74
- if produced <= 0
75
- return
76
- end
113
+ return if produced <= 0
77
114
 
78
115
  # While producing the first page, keep the viewport pinned to the anchor.
79
116
  @viewport.top = @anchor
@@ -96,7 +133,7 @@ module Fatty
96
133
  @viewport.page_top
97
134
  end
98
135
  # Otherwise user is not at bottom; don't force movement.
99
- @viewport.clamp!(lines)
136
+ @viewport.clamp!(current_lines)
100
137
  end
101
138
  end
102
139
 
@@ -139,7 +176,7 @@ module Fatty
139
176
  @last_nav_dir = :down
140
177
  @anchor = nil
141
178
  @autoscroll = false
142
- @viewport.page_bottom(@output.lines)
179
+ @viewport.page_bottom(lines)
143
180
  end
144
181
 
145
182
  desc "One line up"
@@ -178,12 +215,16 @@ module Fatty
178
215
  clamp_to_page!
179
216
  end
180
217
 
181
- desc "Page top"
182
- action :page_top do
218
+ desc "Page top or go to an output line"
219
+ action :page_top do |count: nil|
183
220
  @mode = :paging
184
221
  @paused = true
185
222
  @last_nav_dir = :up
186
- page_top!
223
+ if count
224
+ go_to_line!(count)
225
+ else
226
+ page_top!
227
+ end
187
228
  end
188
229
 
189
230
  desc "Page bottom"
@@ -201,33 +242,19 @@ module Fatty
201
242
  @anchor = nil
202
243
  @last_nav_dir = :down
203
244
  @autoscroll = true
204
- @viewport.clamp!(@output.lines)
245
+ @viewport.clamp!(lines)
205
246
  # Make it visibly start immediately, even if no further appends happen.
206
247
  autoscroll_step?(max_lines: @viewport.height)
207
248
  end
208
249
 
209
250
  desc "Toggle between paging and scrolling"
210
251
  action :toggle_paging do
211
- @last_nav_dir = :down
212
- if @mode == :paging
213
- @mode = :scrolling
214
- @paused = false
215
- @autoscroll = true # keep if you like the animated “catch up”
216
- else
217
- @mode = :paging
218
- @paused = true
219
- @autoscroll = false
220
- @anchor = nil # ensure no command-anchored paging behavior kicks in
221
- @viewport.clamp!(@output.lines)
222
- end
252
+ toggle_paging_mode
223
253
  end
224
254
 
225
255
  desc "Exit paging and return control to normal input."
226
256
  action :quit_paging do
227
- @paused = false
228
- @mode = :paging
229
- @anchor = nil
230
- @autoscroll = false
257
+ quit
231
258
  end
232
259
 
233
260
  def at_top?
@@ -253,14 +280,14 @@ module Fatty
253
280
  end
254
281
 
255
282
  def max_page_top
256
- [@output.lines.size - page_height, 0].max
283
+ [lines.size - page_height, 0].max
257
284
  end
258
285
 
259
286
  def clamp!
260
287
  if paused?
261
288
  clamp_to_page!
262
289
  else
263
- @viewport.clamp!(@output.lines)
290
+ @viewport.clamp!(lines)
264
291
  end
265
292
  end
266
293
 
@@ -275,6 +302,17 @@ module Fatty
275
302
  @autoscroll = false
276
303
  end
277
304
 
305
+ def finish_command!
306
+ @anchor = nil
307
+ if @mode == :scrolling && lines.length > page_height
308
+ set_to_paging
309
+ @viewport.top = max_page_top
310
+ elsif @mode == :paging && lines.length > page_height
311
+ @paused = true
312
+ clamp_to_page!
313
+ end
314
+ end
315
+
278
316
  def reset!(total_lines: 0, mode: :paging)
279
317
  @mode = mode
280
318
  @paused = false
@@ -283,7 +321,6 @@ module Fatty
283
321
  end
284
322
 
285
323
  def autoscroll_step?(max_lines: 200)
286
- lines = @output.lines
287
324
  total = lines.length
288
325
  moved = false
289
326
  if total > 0
@@ -310,6 +347,25 @@ module Fatty
310
347
  !@search[:re].nil?
311
348
  end
312
349
 
350
+ def search_begin!
351
+ ensure_search_snapshot!
352
+ nil
353
+ end
354
+
355
+ def search_cancel!
356
+ if @search_snapshot
357
+ @viewport.top = @search_snapshot[:viewport_top]
358
+ @search = @search_snapshot[:search]
359
+ @search_snapshot = nil
360
+ end
361
+ nil
362
+ end
363
+
364
+ def search_commit!
365
+ @search_snapshot = nil
366
+ nil
367
+ end
368
+
313
369
  # Sets the search pattern and moves the viewport to the next match.
314
370
  # Returns a result hash:
315
371
  # { status: :moved }
@@ -366,14 +422,13 @@ module Fatty
366
422
  end
367
423
  return { status: :not_found } unless re
368
424
 
369
- lines = @output.lines
370
425
  total = lines.length
371
426
  return { status: :not_found } if total.zero?
372
427
 
373
428
  start = search_start_position(direction: direction, total: total, initial: initial)
374
429
  found = find_next_match(lines, re, start, direction: direction, wrap: false)
375
430
 
376
- if found
431
+ if valid_search_match?(found)
377
432
  apply_search_match(found)
378
433
  @search[:pending_wrap] = nil
379
434
  return { status: :moved }
@@ -385,7 +440,7 @@ module Fatty
385
440
  found = find_next_match(lines, re, wrap_start, direction: direction, wrap: true)
386
441
  @search[:pending_wrap] = nil
387
442
 
388
- if found
443
+ if valid_search_match?(found)
389
444
  apply_search_match(found)
390
445
  return { status: :moved, wrapped: true }
391
446
  end
@@ -435,8 +490,8 @@ module Fatty
435
490
  # the renderer’s slice planner / highlighting behavior.
436
491
  def search_visible_highlights(viewport:)
437
492
  return unless @search[:re]
493
+ return unless active?
438
494
 
439
- lines = @output.lines
440
495
  total = lines.length
441
496
  return if total.zero?
442
497
 
@@ -570,8 +625,10 @@ module Fatty
570
625
  "#{arrow} #{prefix}#{pat}"
571
626
  end
572
627
 
628
+ # --- ISearch -----------------------------------------------------------
629
+
573
630
  def isearch_update!(pattern:, direction:)
574
- ensure_isearch_snapshot!
631
+ isearch_begin!
575
632
  pattern = pattern.to_s
576
633
 
577
634
  result =
@@ -598,6 +655,16 @@ module Fatty
598
655
  { status: :not_found, message: "Invalid regexp: #{e.message}" }
599
656
  end
600
657
 
658
+ def isearch_begin!
659
+ ensure_isearch_snapshot!
660
+ nil
661
+ end
662
+
663
+ def isearch_commit!
664
+ @isearch_snapshot = nil
665
+ nil
666
+ end
667
+
601
668
  def isearch_step!(direction:)
602
669
  ensure_isearch_snapshot!
603
670
  search_step!(direction: direction, initial: false, update_origin: false)
@@ -612,19 +679,24 @@ module Fatty
612
679
  nil
613
680
  end
614
681
 
615
- def isearch_commit!(pattern:, direction:)
616
- ensure_isearch_snapshot!
682
+ def isearch_accept!(pattern:, direction:)
683
+ isearch_begin!
684
+
617
685
  pattern = pattern.to_s
618
686
  if pattern.strip.empty?
619
687
  isearch_cancel!
620
688
  return { status: :not_found }
621
689
  end
690
+
622
691
  result = isearch_update!(pattern: pattern, direction: direction)
623
692
  if result[:status] == :moved
624
693
  @search[:original_direction] = direction.to_sym
625
694
  @search[:pending_wrap] = nil
695
+ isearch_commit!
696
+ else
697
+ isearch_cancel!
626
698
  end
627
- @isearch_snapshot = nil
699
+
628
700
  result
629
701
  end
630
702
 
@@ -637,7 +709,7 @@ module Fatty
637
709
  end
638
710
 
639
711
  def preserve_after_resize!(was_at_bottom:)
640
- if @mode == :paging && @output.lines.any? && was_at_bottom
712
+ if @mode == :paging && lines.any? && was_at_bottom
641
713
  page_bottom!
642
714
  else
643
715
  clamp!
@@ -646,6 +718,35 @@ module Fatty
646
718
 
647
719
  private
648
720
 
721
+ # simplecov:disable
722
+
723
+ def go_to_line!(number)
724
+ target = [number.to_i, 1].max
725
+ current_lines = lines
726
+ index =
727
+ if current_lines.first.respond_to?(:number)
728
+ current_lines.bsearch_index { |line| line.number >= target }
729
+ else
730
+ target - 1
731
+ end
732
+ index ||= current_lines.length - 1
733
+ @viewport.top = [index, 0].max
734
+ clamp_to_page!
735
+ end
736
+
737
+ def lines
738
+ @lines.call
739
+ end
740
+
741
+ def ensure_search_snapshot!
742
+ return if @search_snapshot
743
+
744
+ @search_snapshot = {
745
+ viewport_top: @viewport.top,
746
+ search: @search.dup,
747
+ }
748
+ end
749
+
649
750
  def ensure_isearch_snapshot!
650
751
  return if @isearch_snapshot
651
752
 
@@ -697,7 +798,7 @@ module Fatty
697
798
  end
698
799
 
699
800
  def search_start_col_for(line_index)
700
- line = @output.lines[line_index]
801
+ line = lines[line_index]
701
802
  # Use end-of-line as the backward scan upper bound so the starting line
702
803
  # is included. +1 avoids edge weirdness when match ends exactly at len.
703
804
  if line
@@ -785,17 +886,27 @@ module Fatty
785
886
  end
786
887
 
787
888
  def apply_search_match(match)
889
+ unless valid_search_match?(match)
890
+ raise ArgumentError, "invalid search match: #{match.inspect}"
891
+ end
892
+
788
893
  @search[:last] = match
789
894
  line = match[:line]
790
- # Make sure we're in paging mode when navigating search results.
895
+
791
896
  @mode = :paging
792
897
  @paused = true
793
898
  @last_nav_dir = :down
794
899
 
795
- # Position the match line near the middle when possible.
796
900
  half = (@viewport.height / 2)
797
901
  @viewport.top = [line - half, 0].max
798
902
  @search[:last_view_top] = @viewport.top
799
903
  end
904
+
905
+ def valid_search_match?(match)
906
+ match.is_a?(Hash) &&
907
+ match[:line].is_a?(Integer) &&
908
+ match[:from].is_a?(Integer) &&
909
+ match[:to].is_a?(Integer)
910
+ end
800
911
  end
801
912
  end