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/key_map.rb CHANGED
@@ -12,7 +12,7 @@ module Fatty
12
12
  class KeyGesture
13
13
  attr_reader :key, :ctrl, :meta, :shift
14
14
 
15
- def initialize(key:, ctrl:, meta:, shift:)
15
+ def initialize(key:, ctrl: false, meta: false, shift: false)
16
16
  @key = key
17
17
  @ctrl = ctrl
18
18
  @meta = meta
@@ -64,7 +64,7 @@ module Fatty
64
64
  class MouseGesture
65
65
  attr_reader :button, :ctrl, :meta, :shift
66
66
 
67
- def initialize(button:, ctrl:, meta:, shift:)
67
+ def initialize(button:, ctrl: false, meta: false, shift: false)
68
68
  @button = button
69
69
  @ctrl = ctrl
70
70
  @meta = meta
@@ -152,15 +152,13 @@ module Fatty
152
152
 
153
153
  # Bind a KeyEvent to an action in the given context.
154
154
  def bind(context: DEFAULT_CONTEXT, key:, ctrl: false, meta: false, shift: false, action: nil)
155
+ context = normalize_binding_symbol(context, field: :context)
156
+ key = normalize_binding_symbol(key, field: :key)
157
+ action = normalize_action(action)
158
+
155
159
  bind_str = "#{KeyEvent.key_to_str(key:, ctrl:, meta:, shift:)} -> #{action} in context: #{context}"
156
160
  Fatty.info("KeyMap#bind: (#{bind_str})", tag: :keybinding)
157
161
 
158
- raise ArgumentError, "context must be a Symbol" unless context.is_a?(Symbol)
159
- raise ArgumentError, "key must be a Symbol" unless key.is_a?(Symbol)
160
- unless action.is_a?(Symbol) || (action.is_a?(Array) && action.first.is_a?(Symbol))
161
- raise ArgumentError, "action must be a Symbol or [Symbol, *args]"
162
- end
163
-
164
162
  self.class.register_context(context)
165
163
 
166
164
  evt = KeyEvent.new(
@@ -175,19 +173,17 @@ module Fatty
175
173
  end
176
174
 
177
175
  def bind_mouse(context: DEFAULT_CONTEXT, button:, ctrl: false, meta: false, shift: false, action: nil)
176
+ context = normalize_binding_symbol(context, field: :context)
177
+ button = normalize_binding_symbol(button, field: :button)
178
+ action = normalize_action(action)
179
+
178
180
  bind_str = "#{KeyEvent.key_to_str(key: button, ctrl:, meta:, shift:)} -> #{action} in context: #{context}"
179
181
  Fatty.info("KeyMap#bind_mouse(#{bind_str})", tag: :keybinding)
180
182
 
181
- raise ArgumentError, "context must be a Symbol" unless context.is_a?(Symbol)
182
- raise ArgumentError, "button must be a Symbol" unless button.is_a?(Symbol)
183
- unless action.is_a?(Symbol) || (action.is_a?(Array) && action.first.is_a?(Symbol))
184
- raise ArgumentError, "action must be a Symbol or [Symbol, *args]"
185
- end
186
-
187
183
  self.class.register_context(context)
188
184
 
189
185
  gest = MouseGesture.new(
190
- button: button,
186
+ button:,
191
187
  ctrl: truthy?(ctrl),
192
188
  meta: truthy?(meta),
193
189
  shift: truthy?(shift),
@@ -291,16 +287,58 @@ module Fatty
291
287
  return result unless event
292
288
 
293
289
  gest = gesture_from_event(event)
294
-
295
290
  @bindings.each do |context, map|
296
291
  binding = map[gest]
297
292
  result[context] = binding if binding
298
293
  end
294
+ if event.printable?
295
+ self_insert_contexts.each do |context|
296
+ result[context] ||= [:self_insert, event.text]
297
+ end
298
+ end
299
299
  result
300
300
  end
301
301
 
302
302
  private
303
303
 
304
+ # simplecov:disable
305
+
306
+ def self_insert_contexts
307
+ [:text, :pager_input].select { |context| self.class.registered_contexts.include?(context) }
308
+ end
309
+
310
+ def normalize_binding_symbol(value, field:)
311
+ case value
312
+ when Symbol
313
+ value
314
+ when String
315
+ normalize_symbol_string(value, field:)
316
+ else
317
+ raise ArgumentError, "#{field} must be a Symbol or String"
318
+ end
319
+ end
320
+
321
+ def normalize_action(action)
322
+ case action
323
+ when Symbol, String
324
+ normalize_binding_symbol(action, field: :action)
325
+ when Array
326
+ raise ArgumentError, "action must be a Symbol, String, or [Symbol/String, *args]" if action.empty?
327
+
328
+ [normalize_binding_symbol(action.first, field: :action), *action.drop(1)]
329
+ else
330
+ raise ArgumentError, "action must be a Symbol, String, or [Symbol/String, *args]"
331
+ end
332
+ end
333
+
334
+ def normalize_symbol_string(value, field:)
335
+ str = value.strip
336
+ str = str.delete_prefix(":")
337
+ raise ArgumentError, "#{field} must not be empty" if str.empty?
338
+
339
+ str.to_sym
340
+ end
341
+
304
342
  def gesture_from_event(event)
305
343
  case event
306
344
  when Fatty::MouseEvent
@@ -54,8 +54,8 @@ module Fatty
54
54
  # Counts (prefix arg)
55
55
  map.bind(key: :u, ctrl: true, action: :universal_argument)
56
56
  map.bind_digits(context: :text, meta: true)
57
- map.bind_digits(context: :popup)
58
57
  map.bind_digits(context: :paging)
58
+ map.bind_digits(context: :paging, meta: true)
59
59
  # ShellSession uses contexts [:paging, :terminal] while the pager is
60
60
  # active. Bind C-u in :terminal so it works as a prefix during paging
61
61
  # (e.g. C-u / to start a regex search).
@@ -70,6 +70,7 @@ module Fatty
70
70
 
71
71
  # Completion on input
72
72
  map.bind(context: :input, key: :tab, action: :complete)
73
+ map.bind(context: :input, key: :tab, shift: true, action: :complete_previous)
73
74
  map.bind(context: :input, key: :tab, meta: true, action: :completion_popup)
74
75
 
75
76
  # Popup
@@ -77,12 +78,13 @@ module Fatty
77
78
  map.bind(context: :popup, key: :g, ctrl: true, action: :popup_cancel)
78
79
  map.bind(context: :popup, key: :escape, action: :popup_cancel)
79
80
  map.bind(context: :popup, key: :enter, action: :popup_accept)
80
- map.bind(context: :popup, key: :tab, action: :popup_accept)
81
81
  map.bind(context: :popup, key: :return, action: :popup_accept)
82
82
  map.bind(context: :popup, key: :up, action: :popup_prev)
83
83
  map.bind(context: :popup, key: :down, action: :popup_next)
84
84
  map.bind(context: :popup, key: :p, ctrl: true, action: :popup_prev)
85
85
  map.bind(context: :popup, key: :n, ctrl: true, action: :popup_next)
86
+ map.bind(context: :popup, key: :p, meta: true, action: :history_prev)
87
+ map.bind(context: :popup, key: :n, meta: true, action: :history_next)
86
88
  map.bind(context: :popup, key: :page_up, action: :popup_page_up)
87
89
  map.bind(context: :popup, key: :page_down, action: :popup_page_down)
88
90
  map.bind(context: :popup, key: :v, meta: true, action: :popup_page_up)
@@ -92,6 +94,10 @@ module Fatty
92
94
  map.bind(context: :popup, key: :'<', meta: true, action: :popup_top)
93
95
  map.bind(context: :popup, key: :'>', meta: true, action: :popup_bottom)
94
96
  map.bind(context: :popup, key: :l, ctrl: true, action: :popup_recenter)
97
+ map.bind(context: :popup, key: :tab, action: :popup_tab)
98
+ map.bind(context: :popup, key: :tab, shift: true, action: :popup_backtab)
99
+ map.bind(context: :popup, key: :right, action: :popup_tab)
100
+ map.bind(context: :popup, key: :left, action: :popup_backtab)
95
101
 
96
102
  # Popups that present multiple items for selection have a special
97
103
  # context that steals the SPACE key for toggling selection
@@ -100,10 +106,16 @@ module Fatty
100
106
  # Prompt
101
107
  map.bind(context: :prompt, key: :c, ctrl: true, action: :prompt_cancel)
102
108
  map.bind(context: :prompt, key: :g, ctrl: true, action: :prompt_cancel)
109
+ map.bind(context: :prompt, key: :escape, action: :prompt_cancel)
103
110
  map.bind(context: :prompt, key: :enter, action: :prompt_accept)
104
111
  map.bind(context: :prompt, key: :return, action: :prompt_accept)
105
112
  map.bind(context: :prompt, key: :j, ctrl: true, action: :prompt_accept)
106
113
 
114
+ # Prompt history
115
+ map.bind(context: :prompt, key: :p, ctrl: true, action: :history_prev)
116
+ map.bind(context: :prompt, key: :n, ctrl: true, action: :history_next)
117
+ map.bind(context: :prompt, key: :up, action: :history_prev)
118
+ map.bind(context: :prompt, key: :down, action: :history_next)
107
119
  #
108
120
  # Themes
109
121
  map.bind(context: :terminal, key: :t, meta: true, ctrl: true, action: :cycle_theme)
@@ -144,14 +156,30 @@ module Fatty
144
156
  map.bind(context: :paging, key: :c, ctrl: true, action: :quit_paging)
145
157
  map.bind(context: :paging, key: :g, ctrl: true, action: :quit_paging)
146
158
  map.bind(context: :paging, key: :q, action: :quit_paging)
159
+ map.bind(context: :paging, key: :escape, action: :quit_paging)
147
160
 
148
161
  # Paging search (opens SearchSession via ShellSession actions)
149
162
  map.bind(context: :paging, key: :/, action: :pager_search_forward)
150
163
  map.bind(context: :paging, key: :'?', action: :pager_search_backward)
164
+ map.bind(context: :paging, key: :/, ctrl: true, action: :pager_regex_search_forward)
165
+ map.bind(context: :paging, key: :'?', ctrl: true, action: :pager_regex_search_backward)
151
166
  # Incremental (string-only) search like Emacs isearch.
152
167
  # Regex is intentionally non-incremental (use C-u / or / then toggle).
153
168
  map.bind(context: :paging, key: :s, ctrl: true, action: :pager_isearch_forward)
154
169
  map.bind(context: :paging, key: :r, ctrl: true, action: :pager_isearch_backward)
170
+ map.bind(context: :paging, key: :s, meta: true, ctrl: true, action: :pager_regex_search_forward)
171
+ map.bind(context: :paging, key: :r, meta: true, ctrl: true, action: :pager_regex_search_backward)
172
+
173
+ # Repeat last search (no minibuffer)
174
+ map.bind(context: :paging, key: :n, action: :pager_search_next)
175
+ map.bind(context: :paging, key: :N, action: :pager_search_prev)
176
+
177
+ # Line numbers
178
+ map.bind(context: :paging, key: :'=', action: :toggle_line_numbers)
179
+
180
+ # Narrowing
181
+ map.bind(context: :paging, key: :'<', action: :narrow_output)
182
+ map.bind(context: :paging, key: :'>', action: :widen_output)
155
183
 
156
184
  # I-search controls (within ISearchSession)
157
185
  map.bind(context: :isearch, key: :enter, action: :isearch_accept)
@@ -159,14 +187,14 @@ module Fatty
159
187
  map.bind(context: :isearch, key: :j, ctrl: true, action: :isearch_accept)
160
188
  map.bind(context: :isearch, key: :s, ctrl: true, action: :isearch_next)
161
189
  map.bind(context: :isearch, key: :r, ctrl: true, action: :isearch_prev)
190
+ map.bind(context: :isearch, key: :p, ctrl: true, action: :history_prev)
191
+ map.bind(context: :isearch, key: :n, ctrl: true, action: :history_next)
192
+ map.bind(context: :isearch, key: :up, action: :history_prev)
193
+ map.bind(context: :isearch, key: :down, action: :history_next)
162
194
  map.bind(context: :isearch, key: :c, ctrl: true, action: :isearch_cancel)
163
195
  map.bind(context: :isearch, key: :g, ctrl: true, action: :isearch_cancel)
164
196
  map.bind(context: :isearch, key: :escape, action: :isearch_cancel)
165
197
 
166
- # Repeat last search (no minibuffer)
167
- map.bind(context: :paging, key: :n, action: :pager_search_next)
168
- map.bind(context: :paging, key: :N, action: :pager_search_prev)
169
-
170
198
  # Search controls (within SearchSession)
171
199
  map.bind(context: :search, key: :enter, action: :search_accept)
172
200
  map.bind(context: :search, key: :return, action: :search_accept)
@@ -174,6 +202,10 @@ module Fatty
174
202
  map.bind(context: :search, key: :s, ctrl: true, action: :search_step_forward)
175
203
  map.bind(context: :search, key: :r, ctrl: true, action: :search_step_backward)
176
204
  map.bind(context: :search, key: :r, meta: true, action: :search_toggle_regex)
205
+ map.bind(context: :search, key: :p, ctrl: true, action: :history_prev)
206
+ map.bind(context: :search, key: :n, ctrl: true, action: :history_next)
207
+ map.bind(context: :search, key: :up, action: :history_prev)
208
+ map.bind(context: :search, key: :down, action: :history_next)
177
209
  map.bind(context: :search, key: :c, ctrl: true, action: :search_cancel)
178
210
  map.bind(context: :search, key: :g, ctrl: true, action: :search_cancel)
179
211
  map.bind(context: :search, key: :escape, action: :search_cancel)
@@ -181,6 +213,8 @@ module Fatty
181
213
  # Mouse scrolling in paging
182
214
  map.bind_mouse(context: :paging, button: :scroll_up, action: :scroll_up)
183
215
  map.bind_mouse(context: :paging, button: :scroll_down, action: :scroll_down)
216
+ map.bind_mouse(context: :paging, button: :scroll_up, ctrl: true, action: :page_up)
217
+ map.bind_mouse(context: :paging, button: :scroll_down, ctrl: true, action: :page_down)
184
218
 
185
219
  # Load the user's keybindings
186
220
  map.load_user_config.activate!
@@ -22,6 +22,8 @@ module Fatty
22
22
 
23
23
  private
24
24
 
25
+ # simplecov:disable
26
+
25
27
  def stringify_keys(hash)
26
28
  hash.each_with_object({}) do |(k, v), memo|
27
29
  memo[k.to_s] = v
@@ -42,6 +42,8 @@ module Fatty
42
42
 
43
43
  private
44
44
 
45
+ # simplecov:disable
46
+
45
47
  def render_hash(msg)
46
48
  event = msg[:event] || msg["event"]
47
49
  tag = msg[:tag] || msg["tag"]
data/lib/fatty/logger.rb CHANGED
@@ -66,17 +66,22 @@ module Fatty
66
66
  # - command:: Terminal.apply_command / :send dispatches
67
67
  # - session:: session update calls, mode/context stack changes
68
68
  # - render:: viewports, layout sizes, redraw triggers
69
- # - perf:: timings, frame time, slow paths
70
69
  # - all:: All of the above
70
+ # - perf:: timings, frame time, slow paths
71
71
  def self.log(event = nil, level: :debug, tag: nil, **data)
72
72
  return unless logger
73
73
 
74
74
  tags = active_tags
75
+
76
+ # The :perf tag has to be requested explicitly: it is not implied by
77
+ # :all.
78
+ return if tag == :perf && !tags.include?(:perf)
79
+
75
80
  if tag && !tags.include?(:all)
76
81
  return unless tags.include?(tag.to_sym)
77
82
  end
78
83
 
79
- payload = { event: event, tag: tag }
84
+ payload = { tag: tag, event: event }
80
85
  payload.merge!(data.reject { |k, _| k == :event || k == :tag })
81
86
 
82
87
  logger.add(severity(level), payload)
@@ -7,15 +7,17 @@ module Fatty
7
7
  class AnsiRenderer < Redcarpet::Render::Base
8
8
  HARD_BREAK = "\uE000"
9
9
 
10
- def initialize(width: 80, palette: nil)
10
+ def initialize(width: 80, palette: nil, theme: nil, truecolor: false)
11
11
  super()
12
12
  @width = width.to_i
13
13
  @palette = palette || {}
14
+ @theme = theme || {}
15
+ @truecolor = truecolor
14
16
  end
15
17
 
16
18
  def block_code(code, language)
17
19
  lexer = rouge_lexer(language.to_s, code.to_s)
18
- formatter = Rouge::Formatters::Terminal256.new
20
+ formatter = rouge_formatter
19
21
  gutter = md("│ ", :markdown_code_gutter)
20
22
 
21
23
  highlighted = formatter.format(lexer.lex(code.to_s))
@@ -271,6 +273,8 @@ module Fatty
271
273
 
272
274
  private
273
275
 
276
+ # simplecov:disable
277
+
274
278
  def wrap(text, first_prefix: "", rest_prefix: first_prefix)
275
279
  hard_lines = text.to_s.split(HARD_BREAK, -1)
276
280
 
@@ -336,20 +340,30 @@ module Fatty
336
340
  codes << 4 if attrs.include?(:underline)
337
341
  codes << 7 if attrs.include?(:reverse)
338
342
 
339
- if (fg_rgb = spec[:fg_rgb] || spec["fg_rgb"])
343
+ fg_rgb = spec[:fg_rgb] || spec["fg_rgb"]
344
+ bg_rgb = spec[:bg_rgb] || spec["bg_rgb"]
345
+ fg = spec[:fg] || spec["fg"]
346
+ bg = spec[:bg] || spec["bg"]
347
+
348
+ if fg_rgb
340
349
  codes.push(38, 2, *fg_rgb)
341
- elsif (fg = spec[:fg] || spec["fg"])
350
+ elsif !default_color?(fg)
342
351
  codes.concat(color_codes(fg, foreground: true))
343
352
  end
344
353
 
345
- if (bg_rgb = spec[:bg_rgb] || spec["bg_rgb"])
354
+ if bg_rgb
346
355
  codes.push(48, 2, *bg_rgb)
347
- elsif (bg = spec[:bg] || spec["bg"])
356
+ elsif !default_color?(bg)
348
357
  codes.concat(color_codes(bg, foreground: false))
349
358
  end
359
+
350
360
  codes
351
361
  end
352
362
 
363
+ def default_color?(color)
364
+ color.nil? || color.to_i == Fatty::Color::DEFAULT_INDEX
365
+ end
366
+
353
367
  def color_codes(color, foreground:)
354
368
  color = color.to_i
355
369
 
@@ -369,5 +383,60 @@ module Fatty
369
383
  [48, 5, color]
370
384
  end
371
385
  end
386
+
387
+ def rouge_theme
388
+ case markdown_code_theme
389
+ when :solarized_light
390
+ Rouge::Themes::Base16::Solarized.mode(:light).new
391
+ when :solarized_dark
392
+ Rouge::Themes::Base16::Solarized.mode(:dark).new
393
+ when :base16
394
+ Rouge::Themes::Base16.new
395
+ when :base16_monokai
396
+ Rouge::Themes::Base16::Monokai.mode(:dark).new
397
+ when :monokai
398
+ Rouge::Themes::Monokai.new
399
+ when :monokai_sublime
400
+ Rouge::Themes::MonokaiSublime.new
401
+ when :gruvbox_light
402
+ Rouge::Themes::Gruvbox.mode(:light).new
403
+ when :gruvbox_dark
404
+ Rouge::Themes::Gruvbox.mode(:dark).new
405
+ when :github
406
+ Rouge::Themes::Github.mode(:light).new
407
+ when :github_dark
408
+ Rouge::Themes::Github.mode(:dark).new
409
+ when :molokai
410
+ Rouge::Themes::Molokai.new
411
+ when :colorful
412
+ Rouge::Themes::Colorful.new
413
+ when :bw
414
+ Rouge::Themes::BlackWhiteTheme.new
415
+ when :pastie
416
+ Rouge::Themes::Pastie.new
417
+ when :tulip
418
+ Rouge::Themes::Tulip.new
419
+ when :igor_pro
420
+ Rouge::Themes::IgorPro.new
421
+ when :magritte
422
+ Rouge::Themes::Magritte.new
423
+ else
424
+ Rouge::Themes::ThankfulEyes.new
425
+ end
426
+ end
427
+
428
+ def markdown_code_theme
429
+ value = @theme[:markdown_code_theme] if @theme.respond_to?(:[])
430
+ value.to_s.tr("-", "_").to_sym
431
+ end
432
+
433
+ def rouge_formatter
434
+ formatter_class = truecolor? ? Rouge::Formatters::TerminalTruecolor : Rouge::Formatters::Terminal256
435
+ formatter_class.new(rouge_theme)
436
+ end
437
+
438
+ def truecolor?
439
+ @truecolor
440
+ end
372
441
  end
373
442
  end
@@ -2,10 +2,9 @@
2
2
 
3
3
  module Fatty
4
4
  module Markdown
5
- def self.render(text, width: 80, palette: nil)
5
+ def self.render(text, width: 80, palette: nil, theme: nil, truecolor: false) #
6
6
  markdown = Redcarpet::Markdown.new(
7
- Fatty::AnsiRenderer.new(width: width, palette: palette),
8
- no_intra_emphasis: true,
7
+ Fatty::AnsiRenderer.new(width: width, palette: palette, theme: theme, truecolor: truecolor),
9
8
  tables: true,
10
9
  fenced_code_blocks: true,
11
10
  autolink: true,
@@ -6,7 +6,7 @@ module Fatty
6
6
  class MouseEvent
7
7
  attr_reader :button, :x, :y, :ctrl, :meta, :shift, :raw
8
8
 
9
- def initialize(button:, x: nil, y:, raw: nil, ctrl: false, meta: false, shift: false)
9
+ def initialize(button:, x: 0, y: 0, raw: nil, ctrl: false, meta: false, shift: false)
10
10
  @button = button
11
11
  @x = x
12
12
  @y = y
@@ -17,8 +17,21 @@ module Fatty
17
17
  Fatty.debug("#{self.class}#new(#{self})", tag: :event)
18
18
  end
19
19
 
20
+ def key
21
+ :mouse
22
+ end
23
+
20
24
  def to_s
21
- "MouseEvent: button: `#{button}`, x: `#{x}`, y: `#{y}`, raw: `#{raw}`, ctrl: `#{ctrl}`, meta: `#{meta}`, shift: `#{shift}`"
25
+ self.class.button_to_str(button:, ctrl:, meta:, shift:)
26
+ end
27
+
28
+ def self.button_to_str(button:, ctrl: false, meta: false, shift: false)
29
+ mods = []
30
+ mods << "C" if ctrl
31
+ mods << "M" if meta
32
+ mods << "S" if shift
33
+ button_str = button.to_s
34
+ mods.empty? ? button_str : "#{mods.join('-')}-#{button_str}"
22
35
  end
23
36
 
24
37
  def printable?
@@ -28,5 +41,132 @@ module Fatty
28
41
  def mouse?
29
42
  true
30
43
  end
44
+
45
+ def ctrl?
46
+ @ctrl
47
+ end
48
+
49
+ def meta?
50
+ @meta
51
+ end
52
+
53
+ def shift?
54
+ @shift
55
+ end
56
+
57
+ def bindings
58
+ Fatty::KeyMap.active.bindings_for(self) || {}
59
+ end
60
+
61
+ def report
62
+ rule = "=" * 55 + "\n"
63
+ out = +""
64
+ out << rule
65
+ out << "Mouse report #{status_string}:\n"
66
+ out << " raw: #{raw.inspect}\n"
67
+ out << " button: #{button.inspect}\n"
68
+ out << " name: #{to_s}\n"
69
+ out << " x: #{x.inspect}\n"
70
+ out << " y: #{y.inspect}\n"
71
+ out << " modifiers: #{modifier_text}\n"
72
+ out << " bindings:\n#{bindings_text}\n"
73
+ out << "\n"
74
+ end
75
+
76
+ def suggested_snippet(_terminal_name)
77
+ bindings.empty? ? suggested_mouse_binding : ""
78
+ end
79
+
80
+ private
81
+
82
+ def status_string
83
+ bindings.empty? ? "(UNBOUND)" : "(BOUND)"
84
+ end
85
+
86
+ def modifier_text
87
+ mods = []
88
+ mods << "ctrl" if ctrl?
89
+ mods << "meta" if meta?
90
+ mods << "shift" if shift?
91
+ mods.empty? ? "(none)" : mods.join(", ")
92
+ end
93
+
94
+ def bindings_text
95
+ return "(none)" if bindings.empty?
96
+
97
+ bindings.map { |context, binding|
98
+ action, _args = binding_action(binding)
99
+ " context: #{context}: #{binding_text(binding)}#{action_target_text(action)}"
100
+ }.join("\n")
101
+ end
102
+
103
+ def binding_action(binding)
104
+ case binding
105
+ when Array
106
+ [binding[0], binding[1..] || []]
107
+ else
108
+ [binding, []]
109
+ end
110
+ end
111
+
112
+ def binding_text(binding)
113
+ case binding
114
+ when nil
115
+ "(none)"
116
+ when Array
117
+ action = binding[0]
118
+ args = binding[1..] || []
119
+ args.empty? ? action.inspect : "#{action.inspect} #{args.inspect}"
120
+ else
121
+ binding.inspect
122
+ end
123
+ end
124
+
125
+ def action_target_text(action)
126
+ return "" unless action
127
+
128
+ defn = Fatty::Actions.lookup(action)
129
+ return " (unregistered action)" unless defn
130
+
131
+ " (#{defn[:on]}: #{action_owner_name(defn[:owner])}##{defn[:method]})"
132
+ end
133
+
134
+ def action_owner_name(owner)
135
+ name =
136
+ if owner.respond_to?(:name) && owner.name
137
+ owner.name
138
+ else
139
+ owner.to_s
140
+ end
141
+ name.split("::").last
142
+ end
143
+
144
+ def suggested_mouse_binding
145
+ out = <<~TEXT
146
+
147
+ No action is bound to #{to_s}.
148
+
149
+ Suggested keybindings.yml entry:
150
+
151
+ ............>8 snip here 8<....................
152
+ #{yaml_desc}
153
+ ............>8 snip here 8<....................
154
+ TEXT
155
+
156
+ out << <<~TEXT
157
+
158
+ TEXT
159
+ out
160
+ end
161
+
162
+ def yaml_desc
163
+ lines = ["# #{to_s}", "- button: #{button}"]
164
+ lines << " shift: true" if shift?
165
+ lines << " ctrl: true" if ctrl?
166
+ lines << " meta: true" if meta?
167
+ lines << " context: <valid_context>"
168
+ lines << " action: <action_name>"
169
+ lines.join("\n")
170
+ end
31
171
  end
32
172
  end
@@ -52,6 +52,8 @@ module Fatty
52
52
 
53
53
  private
54
54
 
55
+ # simplecov:disable
56
+
55
57
  def append_fragment(fragment)
56
58
  ntrimmed = 0
57
59
  if @line_open && @lines.any?