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.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +2 -0
  3. data/.simplecov +23 -0
  4. data/.yardopts +4 -0
  5. data/CHANGELOG.md +34 -0
  6. data/CHANGELOG.org +38 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +31 -0
  9. data/README.org +166 -0
  10. data/Rakefile +15 -0
  11. data/TODO.org +163 -0
  12. data/examples/markdown/native-markdown.md +370 -0
  13. data/examples/markdown/ox-gfm-markdown.md +373 -0
  14. data/examples/markdown/ox-gfm-markdown.org +376 -0
  15. data/exe/fatty +275 -0
  16. data/fatty.gemspec +42 -0
  17. data/lib/fatty/accept_env.rb +32 -0
  18. data/lib/fatty/action.rb +103 -0
  19. data/lib/fatty/action_environment.rb +42 -0
  20. data/lib/fatty/actionable.rb +73 -0
  21. data/lib/fatty/alert.rb +93 -0
  22. data/lib/fatty/ansi/renderer.rb +168 -0
  23. data/lib/fatty/ansi.rb +352 -0
  24. data/lib/fatty/colors/color.rb +379 -0
  25. data/lib/fatty/colors/pairs.rb +73 -0
  26. data/lib/fatty/colors/palette.rb +73 -0
  27. data/lib/fatty/colors/rgb.txt +788 -0
  28. data/lib/fatty/colors.rb +5 -0
  29. data/lib/fatty/config.rb +86 -0
  30. data/lib/fatty/config_files/config.yml +50 -0
  31. data/lib/fatty/config_files/help.md +120 -0
  32. data/lib/fatty/config_files/help.org +124 -0
  33. data/lib/fatty/config_files/keybindings.yml +49 -0
  34. data/lib/fatty/config_files/keydefs.yml +23 -0
  35. data/lib/fatty/config_files/themes/mono.yml +76 -0
  36. data/lib/fatty/config_files/themes/nordic.yml +77 -0
  37. data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
  38. data/lib/fatty/config_files/themes/terminal.yml +90 -0
  39. data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
  40. data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
  41. data/lib/fatty/core_ext/string.rb +21 -0
  42. data/lib/fatty/core_ext.rb +3 -0
  43. data/lib/fatty/counter.rb +81 -0
  44. data/lib/fatty/curses/context.rb +279 -0
  45. data/lib/fatty/curses/curses_coder.rb +684 -0
  46. data/lib/fatty/curses/event_source.rb +230 -0
  47. data/lib/fatty/curses/key_decoder.rb +183 -0
  48. data/lib/fatty/curses/patch.rb +116 -0
  49. data/lib/fatty/curses/window_styling.rb +32 -0
  50. data/lib/fatty/curses.rb +16 -0
  51. data/lib/fatty/env.rb +100 -0
  52. data/lib/fatty/help.rb +41 -0
  53. data/lib/fatty/history/entry.rb +71 -0
  54. data/lib/fatty/history.rb +289 -0
  55. data/lib/fatty/input_buffer.rb +998 -0
  56. data/lib/fatty/input_field.rb +507 -0
  57. data/lib/fatty/key_event.rb +342 -0
  58. data/lib/fatty/key_map.rb +392 -0
  59. data/lib/fatty/keymaps/emacs.rb +189 -0
  60. data/lib/fatty/log_formats/json.rb +47 -0
  61. data/lib/fatty/log_formats/text.rb +67 -0
  62. data/lib/fatty/logger.rb +142 -0
  63. data/lib/fatty/markdown/ansi_renderer.rb +373 -0
  64. data/lib/fatty/markdown/render.rb +22 -0
  65. data/lib/fatty/markdown.rb +4 -0
  66. data/lib/fatty/menu_env.rb +22 -0
  67. data/lib/fatty/mouse_event.rb +32 -0
  68. data/lib/fatty/output_buffer.rb +78 -0
  69. data/lib/fatty/pager.rb +801 -0
  70. data/lib/fatty/prompt.rb +40 -0
  71. data/lib/fatty/renderer/curses.rb +697 -0
  72. data/lib/fatty/renderer/truecolor.rb +607 -0
  73. data/lib/fatty/renderer.rb +419 -0
  74. data/lib/fatty/screen.rb +96 -0
  75. data/lib/fatty/search.rb +43 -0
  76. data/lib/fatty/session/alert_session.rb +52 -0
  77. data/lib/fatty/session/input_session.rb +99 -0
  78. data/lib/fatty/session/isearch_session.rb +172 -0
  79. data/lib/fatty/session/keytest_session.rb +236 -0
  80. data/lib/fatty/session/modal_session.rb +61 -0
  81. data/lib/fatty/session/output_session.rb +105 -0
  82. data/lib/fatty/session/popup_session.rb +540 -0
  83. data/lib/fatty/session/prompt_session.rb +157 -0
  84. data/lib/fatty/session/search_session.rb +136 -0
  85. data/lib/fatty/session/shell_session.rb +566 -0
  86. data/lib/fatty/session.rb +173 -0
  87. data/lib/fatty/sessions.rb +14 -0
  88. data/lib/fatty/terminal/popup_owner.rb +26 -0
  89. data/lib/fatty/terminal/progress.rb +374 -0
  90. data/lib/fatty/terminal.rb +1067 -0
  91. data/lib/fatty/themes/loader.rb +136 -0
  92. data/lib/fatty/themes/manager.rb +71 -0
  93. data/lib/fatty/themes/registry.rb +64 -0
  94. data/lib/fatty/themes/resolver.rb +224 -0
  95. data/lib/fatty/themes/themes.rb +131 -0
  96. data/lib/fatty/themes.rb +6 -0
  97. data/lib/fatty/version.rb +5 -0
  98. data/lib/fatty/view/alert_view.rb +14 -0
  99. data/lib/fatty/view/cursor_view.rb +18 -0
  100. data/lib/fatty/view/input_view.rb +9 -0
  101. data/lib/fatty/view/output_view.rb +9 -0
  102. data/lib/fatty/view/status_view.rb +14 -0
  103. data/lib/fatty/view.rb +33 -0
  104. data/lib/fatty/viewport.rb +90 -0
  105. data/lib/fatty/views.rb +9 -0
  106. data/lib/fatty.rb +55 -0
  107. data/sig/fatty.rbs +4 -0
  108. metadata +250 -0
@@ -0,0 +1,392 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'keymaps/emacs'
4
+
5
+ module Fatty
6
+ # This struct is used as a key into the KeyMap. It represents only those
7
+ # parts of the KeyEvent that are relevant for resolving bindings. It also
8
+ # normalizes the use of uppercase letters, like 'G' to convert them to a
9
+ # canonical :g with shift: true. That way the user can bind either to mean
10
+ # the same thing.
11
+ # KeyGesture = Struct.new(:key, :ctrl, :meta, :shift, keyword_init: true) do
12
+ class KeyGesture
13
+ attr_reader :key, :ctrl, :meta, :shift
14
+
15
+ def initialize(key:, ctrl:, meta:, shift:)
16
+ @key = key
17
+ @ctrl = ctrl
18
+ @meta = meta
19
+ @shift = shift
20
+ end
21
+
22
+ def self.from_event(ev)
23
+ k = ev&.key
24
+ ctrl = !!ev&.ctrl
25
+ meta = !!ev&.meta
26
+ shift = !!ev&.shift
27
+
28
+ if k.is_a?(Symbol)
29
+ s = k.to_s
30
+ if s.length == 1
31
+ ch = s[0]
32
+ if ('A'..'Z').cover?(ch)
33
+ # Decoder (or config) used :G. Canonicalize to :g + shift.
34
+ k = ch.downcase.to_sym
35
+ shift = true
36
+ elsif ('a'..'z').cover?(ch)
37
+ # Decoder used :g. Keep it, but allow explicit shift to mean uppercase.
38
+ k = ch.to_sym
39
+ end
40
+ end
41
+ end
42
+
43
+ new(key: k, ctrl: ctrl, meta: meta, shift: shift)
44
+ end
45
+
46
+ def ==(other)
47
+ other.is_a?(self.class) &&
48
+ key == other.key &&
49
+ ctrl == other.ctrl &&
50
+ meta == other.meta &&
51
+ shift == other.shift
52
+ end
53
+ alias_method :eql?, :==
54
+
55
+ def hash
56
+ [self.class, key, ctrl, meta, shift].hash
57
+ end
58
+
59
+ def inspect
60
+ "#<#{self.class} key=#{key.inspect} ctrl=#{ctrl} meta=#{meta} shift=#{shift}>"
61
+ end
62
+ end
63
+
64
+ class MouseGesture
65
+ attr_reader :button, :ctrl, :meta, :shift
66
+
67
+ def initialize(button:, ctrl:, meta:, shift:)
68
+ @button = button
69
+ @ctrl = ctrl
70
+ @meta = meta
71
+ @shift = shift
72
+ end
73
+
74
+ def self.from_event(event)
75
+ new(
76
+ button: event.button,
77
+ ctrl: !!event.ctrl,
78
+ meta: !!event.meta,
79
+ shift: !!event.shift,
80
+ )
81
+ end
82
+
83
+ def ==(other)
84
+ other.is_a?(self.class) &&
85
+ button == other.button &&
86
+ ctrl == other.ctrl &&
87
+ meta == other.meta &&
88
+ shift == other.shift
89
+ end
90
+ alias_method :eql?, :==
91
+
92
+ def hash
93
+ [self.class, button, ctrl, meta, shift].hash
94
+ end
95
+
96
+ def inspect
97
+ "#<#{self.class} button=#{button.inspect} ctrl=#{ctrl} meta=#{meta} shift=#{shift}>"
98
+ end
99
+ end
100
+
101
+ # The KeyMap class maintains the mapping between KeyEvent's and action names
102
+ # that can be handled by Terminal. Terminal can delegate the action to any
103
+ # of its controlled components or handle the action itself. Currently,
104
+ # there are two contexts for KeyBinding: :input (for composing a line at the
105
+ # Terminal) and :paging (for controlling the display of output that is
106
+ # longer than the Viewport). The default keybinding context is :input.
107
+ class KeyMap
108
+ attr_reader :bindings
109
+
110
+ DEFAULT_CONTEXT = :text
111
+
112
+ class << self
113
+ attr_accessor :active
114
+ end
115
+
116
+ def self.registered_contexts
117
+ @registered_contexts ||= [DEFAULT_CONTEXT, :input, :paging]
118
+ end
119
+
120
+ def self.valid_contexts
121
+ registered_contexts.map(&:to_s).sort
122
+ end
123
+
124
+ # Register a context (optionally before/after another for precedence/printing)
125
+ def self.register_context(ctx, before: nil, after: nil)
126
+ ctx = ctx.to_s.to_sym
127
+ list = registered_contexts
128
+ return ctx if list.include?(ctx)
129
+
130
+ if before
131
+ i = list.index(before.to_s.to_sym) || 0
132
+ list.insert(i, ctx)
133
+ elsif after
134
+ i = list.index(after.to_s.to_sym)
135
+ i ? list.insert(i + 1, ctx) : list << ctx
136
+ else
137
+ list << ctx
138
+ end
139
+
140
+ ctx
141
+ end
142
+
143
+ def initialize
144
+ @bindings = Hash.new { |h, ctx| h[ctx] = {} }
145
+ self.class.active ||= self
146
+ end
147
+
148
+ def activate!
149
+ self.class.active = self
150
+ self
151
+ end
152
+
153
+ # Bind a KeyEvent to an action in the given context.
154
+ def bind(context: DEFAULT_CONTEXT, key:, ctrl: false, meta: false, shift: false, action: nil)
155
+ bind_str = "#{KeyEvent.key_to_str(key:, ctrl:, meta:, shift:)} -> #{action} in context: #{context}"
156
+ Fatty.info("KeyMap#bind: (#{bind_str})", tag: :keybinding)
157
+
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
+ self.class.register_context(context)
165
+
166
+ evt = KeyEvent.new(
167
+ key:,
168
+ ctrl: truthy?(ctrl),
169
+ meta: truthy?(meta),
170
+ shift: truthy?(shift),
171
+ )
172
+ gest = KeyGesture.from_event(evt)
173
+ @bindings[context][gest] = action
174
+ self
175
+ end
176
+
177
+ def bind_mouse(context: DEFAULT_CONTEXT, button:, ctrl: false, meta: false, shift: false, action: nil)
178
+ bind_str = "#{KeyEvent.key_to_str(key: button, ctrl:, meta:, shift:)} -> #{action} in context: #{context}"
179
+ Fatty.info("KeyMap#bind_mouse(#{bind_str})", tag: :keybinding)
180
+
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
+ self.class.register_context(context)
188
+
189
+ gest = MouseGesture.new(
190
+ button: button,
191
+ ctrl: truthy?(ctrl),
192
+ meta: truthy?(meta),
193
+ shift: truthy?(shift),
194
+ )
195
+ @bindings[context][gest] = action
196
+ self
197
+ end
198
+
199
+ def resolve(event, contexts: [])
200
+ result = binding_for(event, contexts: contexts)
201
+ map_str = "#{event} -> #{result.inspect} in contexts: #{contexts} "
202
+ Fatty.debug("KeyMap#resolve: #{map_str}", tag: :keybinding)
203
+ result
204
+ end
205
+
206
+ # Return [action, args] where args is always an Array (possibly empty).
207
+ def resolve_action(event, contexts: [])
208
+ binding = resolve(event, contexts: contexts)
209
+
210
+ if binding
211
+ if binding.is_a?(Array)
212
+ action = binding[0]
213
+ args = binding[1..] || []
214
+ Fatty.debug("KeyMap.resolve_action: action: #{action.inspect}, args: #{args.inspect}", tag: :keybinding)
215
+ [action, args]
216
+ else
217
+ Fatty.debug("KeyMap.resolve_action: action: #{binding.inspect}, args: []", tag: :keybinding)
218
+ [binding, []]
219
+ end
220
+ elsif event&.printable? && (contexts.include?(:text) || contexts.include?(:pager_input))
221
+ # Default: treat printable characters as a self-insert but only in
222
+ # those contexts designed to take text input, not contexts like
223
+ # :paging where keys are meant for control.
224
+ [:self_insert, [event.text]]
225
+ else
226
+ [nil, []]
227
+ end
228
+ end
229
+
230
+ # Return all contexts currently used in this instance
231
+ def contexts
232
+ @bindings.keys
233
+ end
234
+
235
+ # Make the bindings from the user's config file, usually at
236
+ # ~/.config/fatty/keybindings.yml
237
+ def load_user_config
238
+ Fatty.info("Read keybindings from #{Config.user_keybindings_path}", tag: :keybinding)
239
+ data = Fatty::Config.keybindings
240
+ return self unless data.is_a?(Array)
241
+
242
+ Fatty.info("User keybindings", config: data, tag: :keybinding)
243
+ data.each_with_index do |entry, idx|
244
+ bind_entry(entry, idx)
245
+ end
246
+ self
247
+ rescue Psych::SyntaxError => e
248
+ Fatty.error("KeyMap#load_config syntax error in keybindings: #{e.message}", tag: :keybinding)
249
+ self
250
+ end
251
+
252
+ # Bind the digits in the given context with either meta, ctrl, neither or
253
+ # both as the required modifier keys.
254
+ def bind_digits(context: DEFAULT_CONTEXT, meta: nil, ctrl: nil)
255
+ meta = !!meta
256
+ ctrl = !!ctrl
257
+ (0..9).each do |n|
258
+ bind(
259
+ context: context,
260
+ key: n.to_s.to_sym,
261
+ meta: meta,
262
+ ctrl: ctrl,
263
+ action: [:count_digit, n],
264
+ )
265
+ end
266
+ end
267
+
268
+ # Return the keybinding for event in contexts without executing. For
269
+ # reporting purposes, especially in Session::KeyTest.
270
+ def binding_for(event, contexts: [])
271
+ return unless event
272
+
273
+ ctxs = normalize_contexts(contexts)
274
+ gest = gesture_from_event(event)
275
+
276
+ result = nil
277
+ ctxs.each do |ctx|
278
+ map = @bindings.fetch(ctx, nil)
279
+ next unless map
280
+
281
+ result = map[gest]
282
+ break if result
283
+ end
284
+ result
285
+ end
286
+
287
+ # Return all keybindings for event across every context actually loaded
288
+ # without executing.
289
+ def bindings_for(event)
290
+ result = {}
291
+ return result unless event
292
+
293
+ gest = gesture_from_event(event)
294
+
295
+ @bindings.each do |context, map|
296
+ binding = map[gest]
297
+ result[context] = binding if binding
298
+ end
299
+ result
300
+ end
301
+
302
+ private
303
+
304
+ def gesture_from_event(event)
305
+ case event
306
+ when Fatty::MouseEvent
307
+ MouseGesture.from_event(event)
308
+ else
309
+ KeyGesture.from_event(event)
310
+ end
311
+ end
312
+
313
+ # Make a binding from an entry Hash that has keys for context, key (the
314
+ # unmodified key name), the modifiers, 'ctrl', 'meta', and 'shift'. The
315
+ # valid keynames, apart from all the printable characters on the keyboard,
316
+ # are given in the constant Fatty::CURSES_TO_EVENT map defined in the
317
+ # curses_coder file.
318
+ def bind_entry(entry, idx, default_context: DEFAULT_CONTEXT)
319
+ unless entry.is_a?(Hash)
320
+ Fatty.error("KeyMap#bind_entry invalid keybinding at index #{idx} (not a map)", tag: :keybinding)
321
+ return
322
+ end
323
+
324
+ key = entry["key"] || entry[:key]
325
+ button = entry["button"] || entry[:button]
326
+ action = entry["action"] || entry[:action]
327
+ ctx =
328
+ if entry.key?("context")
329
+ entry["context"]
330
+ elsif entry.key?(:context)
331
+ entry[:context]
332
+ else
333
+ default_context
334
+ end
335
+
336
+ unless (key || button) && action
337
+ Fatty.error(
338
+ "KeyMap#bind_entry missing key/button or action for context `#{ctx}` at index #{idx}",
339
+ tag: :keybinding,
340
+ )
341
+ return
342
+ end
343
+
344
+ if button
345
+ bind_mouse(
346
+ context: ctx.to_sym,
347
+ button: button.to_sym,
348
+ ctrl: entry["ctrl"] || entry[:ctrl] || false,
349
+ meta: entry["meta"] || entry[:meta] || false,
350
+ shift: entry["shift"] || entry[:shift] || false,
351
+ action: action.to_sym,
352
+ )
353
+ else
354
+ bind(
355
+ context: ctx.to_sym,
356
+ key: key.to_sym,
357
+ ctrl: entry["ctrl"] || entry[:ctrl] || false,
358
+ meta: entry["meta"] || entry[:meta] || false,
359
+ shift: entry["shift"] || entry[:shift] || false,
360
+ action: action.to_sym,
361
+ )
362
+ end
363
+ self
364
+ end
365
+
366
+ # Allow the user to supply an empty Array or nil to search all contexts.
367
+ def normalize_contexts(contexts)
368
+ case contexts
369
+ when Array
370
+ if contexts.empty?
371
+ self.class.registered_contexts
372
+ else
373
+ contexts.compact.map { |c| c.to_s.to_sym }
374
+ end
375
+ when String, Symbol
376
+ [contexts.to_s.to_sym]
377
+ when NilClass
378
+ self.class.registered_contexts
379
+ else
380
+ raise ArgumentError, "contexts must be Symbol/String, Array, or nil"
381
+ end
382
+ end
383
+
384
+ def truthy?(str)
385
+ return false unless str
386
+ return true if str == true
387
+ return true if str.to_s.match?(/\At/i)
388
+
389
+ false
390
+ end
391
+ end
392
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ module Keymaps
5
+ def self.emacs
6
+ map = KeyMap.new
7
+
8
+ # Motion
9
+ map.bind(key: :f, ctrl: true, action: :move_right)
10
+ map.bind(key: :b, ctrl: true, action: :move_left)
11
+ map.bind(key: :right, action: :move_right)
12
+ map.bind(key: :left, action: :move_left)
13
+ map.bind(key: :f, meta: true, action: :move_word_right)
14
+ map.bind(key: :b, meta: true, action: :move_word_left)
15
+ map.bind(key: :right, meta: true, action: :move_word_right)
16
+ map.bind(key: :left, meta: true, action: :move_word_left)
17
+ map.bind(key: :right, ctrl: true, action: :move_word_right)
18
+ map.bind(key: :left, ctrl: true, action: :move_word_left)
19
+
20
+ map.bind(key: :t, ctrl: true, context: :input, action: :transpose_chars)
21
+ map.bind(key: :t, meta: true, context: :input, action: :transpose_words)
22
+
23
+ map.bind(key: :a, ctrl: true, action: :bol)
24
+ map.bind(key: :e, ctrl: true, action: :eol)
25
+ map.bind(key: :home, action: :bol)
26
+ map.bind(key: :end, action: :eol)
27
+
28
+ # Deletion
29
+ map.bind(key: :delete, action: :delete_char_forward)
30
+ map.bind(key: :d, ctrl: true, action: :delete_char_forward)
31
+ map.bind(key: :backspace, action: :delete_char_backward)
32
+ map.bind(key: :backspace, meta: true, action: :kill_word_backward)
33
+ map.bind(key: :w, ctrl: true, action: :kill_word_backward)
34
+ map.bind(key: :d, meta: true, action: :kill_word_forward)
35
+ map.bind(key: :k, ctrl: true, action: :kill_to_eol)
36
+
37
+ # Undo / Redo
38
+ map.bind(key: :/, ctrl: true, action: :undo)
39
+ map.bind(key: :_, ctrl: true, action: :undo)
40
+ map.bind(key: :/, ctrl: true, meta: true, action: :redo)
41
+ map.bind(key: :/, meta: true, action: :redo)
42
+
43
+ # Region / Mark
44
+ map.bind(key: :space, ctrl: true, action: :set_mark)
45
+ map.bind(key: :'@', ctrl: true, action: :set_mark)
46
+ map.bind(key: :g, ctrl: true, action: :clear_mark)
47
+ map.bind(key: :w, ctrl: true, action: :kill_region)
48
+ map.bind(key: :w, meta: true, action: :copy_region)
49
+
50
+ # Yank / Kill ring
51
+ map.bind(key: :y, ctrl: true, action: :yank)
52
+ map.bind(key: :y, meta: true, action: :yank_pop)
53
+
54
+ # Counts (prefix arg)
55
+ map.bind(key: :u, ctrl: true, action: :universal_argument)
56
+ map.bind_digits(context: :text, meta: true)
57
+ map.bind_digits(context: :popup)
58
+ map.bind_digits(context: :paging)
59
+ # ShellSession uses contexts [:paging, :terminal] while the pager is
60
+ # active. Bind C-u in :terminal so it works as a prefix during paging
61
+ # (e.g. C-u / to start a regex search).
62
+ map.bind(context: :terminal, key: :u, ctrl: true, action: :universal_argument)
63
+
64
+ # History
65
+ map.bind(context: :input, key: :p, ctrl: true, action: :history_prev)
66
+ map.bind(context: :input, key: :n, ctrl: true, action: :history_next)
67
+ map.bind(context: :input, key: :up, action: :history_prev)
68
+ map.bind(context: :input, key: :down, action: :history_next)
69
+ map.bind(context: :input, key: :r, ctrl: true, action: :history_search)
70
+
71
+ # Completion on input
72
+ map.bind(context: :input, key: :tab, action: :complete)
73
+ map.bind(context: :input, key: :tab, meta: true, action: :completion_popup)
74
+
75
+ # Popup
76
+ map.bind(context: :popup, key: :c, ctrl: true, action: :popup_cancel)
77
+ map.bind(context: :popup, key: :g, ctrl: true, action: :popup_cancel)
78
+ map.bind(context: :popup, key: :escape, action: :popup_cancel)
79
+ map.bind(context: :popup, key: :enter, action: :popup_accept)
80
+ map.bind(context: :popup, key: :tab, action: :popup_accept)
81
+ map.bind(context: :popup, key: :return, action: :popup_accept)
82
+ map.bind(context: :popup, key: :up, action: :popup_prev)
83
+ map.bind(context: :popup, key: :down, action: :popup_next)
84
+ map.bind(context: :popup, key: :p, ctrl: true, action: :popup_prev)
85
+ map.bind(context: :popup, key: :n, ctrl: true, action: :popup_next)
86
+ map.bind(context: :popup, key: :page_up, action: :popup_page_up)
87
+ map.bind(context: :popup, key: :page_down, action: :popup_page_down)
88
+ map.bind(context: :popup, key: :v, meta: true, action: :popup_page_up)
89
+ map.bind(context: :popup, key: :v, ctrl: true, action: :popup_page_down)
90
+ map.bind(context: :popup, key: :home, action: :popup_top)
91
+ map.bind(context: :popup, key: :end, action: :popup_bottom)
92
+ map.bind(context: :popup, key: :'<', meta: true, action: :popup_top)
93
+ map.bind(context: :popup, key: :'>', meta: true, action: :popup_bottom)
94
+ map.bind(context: :popup, key: :l, ctrl: true, action: :popup_recenter)
95
+
96
+ # Popups that present multiple items for selection have a special
97
+ # context that steals the SPACE key for toggling selection
98
+ map.bind(context: :popup_multi, key: :space, action: :popup_toggle_selected)
99
+
100
+ # Prompt
101
+ map.bind(context: :prompt, key: :c, ctrl: true, action: :prompt_cancel)
102
+ map.bind(context: :prompt, key: :g, ctrl: true, action: :prompt_cancel)
103
+ map.bind(context: :prompt, key: :enter, action: :prompt_accept)
104
+ map.bind(context: :prompt, key: :return, action: :prompt_accept)
105
+ map.bind(context: :prompt, key: :j, ctrl: true, action: :prompt_accept)
106
+
107
+ #
108
+ # Themes
109
+ map.bind(context: :terminal, key: :t, meta: true, ctrl: true, action: :cycle_theme)
110
+ map.bind(context: :terminal, key: :'=', meta: true, action: :choose_theme)
111
+
112
+ # Final States
113
+ map.bind(context: :terminal, key: :c, ctrl: true, action: :interrupt)
114
+ map.bind(context: :input, key: :d, ctrl: true, action: :interrupt_if_empty)
115
+ map.bind(context: :input, key: :enter, action: :submit_line)
116
+ map.bind(context: :input, key: :return, action: :submit_line)
117
+ map.bind(context: :input, key: :j, ctrl: true, action: :submit_line)
118
+ map.bind(context: :input, key: :j, ctrl: true, meta: true, action: :submit_and_scroll)
119
+
120
+ # Output control
121
+ map.bind(key: :l, ctrl: true, action: :clear_output)
122
+ map.bind(context: :paging, key: :up, action: :line_up)
123
+ map.bind(context: :paging, key: :down, action: :line_down)
124
+ map.bind(context: :paging, key: :k, action: :line_up)
125
+ map.bind(context: :paging, key: :j, action: :line_down)
126
+ map.bind(context: :paging, key: :page_up, action: :page_up)
127
+ map.bind(context: :paging, key: :b, action: :page_up)
128
+ map.bind(context: :paging, key: :u, action: :page_up)
129
+ map.bind(context: :paging, key: :h, ctrl: true, action: :page_up)
130
+ map.bind(context: :paging, key: :page_down, action: :page_down)
131
+ map.bind(context: :paging, key: :f, action: :page_down)
132
+ map.bind(context: :paging, key: :d, action: :page_down)
133
+ map.bind(context: :paging, key: :space, action: :page_down)
134
+ map.bind(context: :paging, key: :v, ctrl: true, action: :page_down)
135
+ map.bind(context: :paging, key: :v, meta: true, action: :page_up)
136
+ map.bind(context: :paging, key: :home, action: :page_top)
137
+ map.bind(context: :paging, key: :g, action: :page_top)
138
+ map.bind(context: :paging, key: :'<', meta: true, action: :page_top)
139
+ map.bind(context: :paging, key: :end, action: :page_bottom)
140
+ map.bind(context: :paging, key: :G, action: :page_bottom)
141
+ map.bind(context: :paging, key: :'>', meta: true, action: :page_bottom)
142
+ map.bind(context: :paging, key: :s, meta: true, action: :toggle_paging)
143
+ map.bind(context: :terminal, key: :s, meta: true, action: :toggle_paging)
144
+ map.bind(context: :paging, key: :c, ctrl: true, action: :quit_paging)
145
+ map.bind(context: :paging, key: :g, ctrl: true, action: :quit_paging)
146
+ map.bind(context: :paging, key: :q, action: :quit_paging)
147
+
148
+ # Paging search (opens SearchSession via ShellSession actions)
149
+ map.bind(context: :paging, key: :/, action: :pager_search_forward)
150
+ map.bind(context: :paging, key: :'?', action: :pager_search_backward)
151
+ # Incremental (string-only) search like Emacs isearch.
152
+ # Regex is intentionally non-incremental (use C-u / or / then toggle).
153
+ map.bind(context: :paging, key: :s, ctrl: true, action: :pager_isearch_forward)
154
+ map.bind(context: :paging, key: :r, ctrl: true, action: :pager_isearch_backward)
155
+
156
+ # I-search controls (within ISearchSession)
157
+ map.bind(context: :isearch, key: :enter, action: :isearch_accept)
158
+ map.bind(context: :isearch, key: :return, action: :isearch_accept)
159
+ map.bind(context: :isearch, key: :j, ctrl: true, action: :isearch_accept)
160
+ map.bind(context: :isearch, key: :s, ctrl: true, action: :isearch_next)
161
+ map.bind(context: :isearch, key: :r, ctrl: true, action: :isearch_prev)
162
+ map.bind(context: :isearch, key: :c, ctrl: true, action: :isearch_cancel)
163
+ map.bind(context: :isearch, key: :g, ctrl: true, action: :isearch_cancel)
164
+ map.bind(context: :isearch, key: :escape, action: :isearch_cancel)
165
+
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
+ # Search controls (within SearchSession)
171
+ map.bind(context: :search, key: :enter, action: :search_accept)
172
+ map.bind(context: :search, key: :return, action: :search_accept)
173
+ map.bind(context: :search, key: :j, ctrl: true, action: :search_accept)
174
+ map.bind(context: :search, key: :s, ctrl: true, action: :search_step_forward)
175
+ map.bind(context: :search, key: :r, ctrl: true, action: :search_step_backward)
176
+ map.bind(context: :search, key: :r, meta: true, action: :search_toggle_regex)
177
+ map.bind(context: :search, key: :c, ctrl: true, action: :search_cancel)
178
+ map.bind(context: :search, key: :g, ctrl: true, action: :search_cancel)
179
+ map.bind(context: :search, key: :escape, action: :search_cancel)
180
+
181
+ # Mouse scrolling in paging
182
+ map.bind_mouse(context: :paging, button: :scroll_up, action: :scroll_up)
183
+ map.bind_mouse(context: :paging, button: :scroll_down, action: :scroll_down)
184
+
185
+ # Load the user's keybindings
186
+ map.load_user_config.activate!
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ module Logger
5
+ class JsonFormatter < ::Logger::Formatter
6
+ def call(level, time, progname, msg)
7
+ rec = {
8
+ t: time.utc.iso8601(6),
9
+ sev: level,
10
+ prog: progname
11
+ }
12
+
13
+ case msg
14
+ when Hash
15
+ rec.merge!(stringify_keys(msg))
16
+ else
17
+ merge_string_message!(rec, msg)
18
+ end
19
+
20
+ rec.to_json << "\n"
21
+ end
22
+
23
+ private
24
+
25
+ def stringify_keys(hash)
26
+ hash.each_with_object({}) do |(k, v), memo|
27
+ memo[k.to_s] = v
28
+ end
29
+ end
30
+
31
+ def merge_string_message!(rec, msg)
32
+ text = msg.to_s
33
+
34
+ begin
35
+ parsed = JSON.parse(text)
36
+ if parsed.is_a?(Hash)
37
+ rec.merge!(parsed)
38
+ else
39
+ rec["msg"] = parsed
40
+ end
41
+ rescue JSON::ParserError
42
+ rec["msg"] = text
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # def render_hash(msg)
4
+ # event = msg[:event] || msg["event"]
5
+ # tag = msg[:tag] || msg["tag"]
6
+
7
+ # extras =
8
+ # msg.reject do |k, _|
9
+ # k.to_s == "event" || k.to_s == "tag"
10
+ # end
11
+
12
+ # parts = []
13
+ # parts << "[#{tag}]" if tag && !tag.to_s.empty?
14
+ # parts << event.to_s if event
15
+
16
+ # unless extras.empty?
17
+ # parts << "\n" + PP.pp(extras, +"").rstrip
18
+ # end
19
+
20
+ # parts.join(" ")
21
+ # end
22
+ # end
23
+ # end
24
+ # end
25
+
26
+ module Fatty
27
+ module Logger
28
+ class TextFormatter < ::Logger::Formatter
29
+ def call(level, time, progname, msg)
30
+ ts = time.utc.iso8601(6)
31
+
32
+ body =
33
+ case msg
34
+ when Hash
35
+ render_hash(msg)
36
+ else
37
+ msg.to_s
38
+ end
39
+
40
+ "#{ts} #{level} #{progname} #{body}\n"
41
+ end
42
+
43
+ private
44
+
45
+ def render_hash(msg)
46
+ event = msg[:event] || msg["event"]
47
+ tag = msg[:tag] || msg["tag"]
48
+
49
+ extras =
50
+ msg.reject do |k, _|
51
+ k.to_s == "event" || k.to_s == "tag"
52
+ end
53
+
54
+ parts = []
55
+ parts << "[#{tag}]" if tag && !tag.to_s.empty?
56
+ parts << event.to_s if event
57
+
58
+ unless extras.empty?
59
+ rendered = PP.pp(extras, +"").rstrip
60
+ parts << "\n#{rendered}"
61
+ end
62
+
63
+ parts.join(" ")
64
+ end
65
+ end
66
+ end
67
+ end