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.
- checksums.yaml +7 -0
- data/.envrc +2 -0
- data/.simplecov +23 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +34 -0
- data/CHANGELOG.org +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +31 -0
- data/README.org +166 -0
- data/Rakefile +15 -0
- data/TODO.org +163 -0
- data/examples/markdown/native-markdown.md +370 -0
- data/examples/markdown/ox-gfm-markdown.md +373 -0
- data/examples/markdown/ox-gfm-markdown.org +376 -0
- data/exe/fatty +275 -0
- data/fatty.gemspec +42 -0
- data/lib/fatty/accept_env.rb +32 -0
- data/lib/fatty/action.rb +103 -0
- data/lib/fatty/action_environment.rb +42 -0
- data/lib/fatty/actionable.rb +73 -0
- data/lib/fatty/alert.rb +93 -0
- data/lib/fatty/ansi/renderer.rb +168 -0
- data/lib/fatty/ansi.rb +352 -0
- data/lib/fatty/colors/color.rb +379 -0
- data/lib/fatty/colors/pairs.rb +73 -0
- data/lib/fatty/colors/palette.rb +73 -0
- data/lib/fatty/colors/rgb.txt +788 -0
- data/lib/fatty/colors.rb +5 -0
- data/lib/fatty/config.rb +86 -0
- data/lib/fatty/config_files/config.yml +50 -0
- data/lib/fatty/config_files/help.md +120 -0
- data/lib/fatty/config_files/help.org +124 -0
- data/lib/fatty/config_files/keybindings.yml +49 -0
- data/lib/fatty/config_files/keydefs.yml +23 -0
- data/lib/fatty/config_files/themes/mono.yml +76 -0
- data/lib/fatty/config_files/themes/nordic.yml +77 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
- data/lib/fatty/config_files/themes/terminal.yml +90 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
- data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
- data/lib/fatty/core_ext/string.rb +21 -0
- data/lib/fatty/core_ext.rb +3 -0
- data/lib/fatty/counter.rb +81 -0
- data/lib/fatty/curses/context.rb +279 -0
- data/lib/fatty/curses/curses_coder.rb +684 -0
- data/lib/fatty/curses/event_source.rb +230 -0
- data/lib/fatty/curses/key_decoder.rb +183 -0
- data/lib/fatty/curses/patch.rb +116 -0
- data/lib/fatty/curses/window_styling.rb +32 -0
- data/lib/fatty/curses.rb +16 -0
- data/lib/fatty/env.rb +100 -0
- data/lib/fatty/help.rb +41 -0
- data/lib/fatty/history/entry.rb +71 -0
- data/lib/fatty/history.rb +289 -0
- data/lib/fatty/input_buffer.rb +998 -0
- data/lib/fatty/input_field.rb +507 -0
- data/lib/fatty/key_event.rb +342 -0
- data/lib/fatty/key_map.rb +392 -0
- data/lib/fatty/keymaps/emacs.rb +189 -0
- data/lib/fatty/log_formats/json.rb +47 -0
- data/lib/fatty/log_formats/text.rb +67 -0
- data/lib/fatty/logger.rb +142 -0
- data/lib/fatty/markdown/ansi_renderer.rb +373 -0
- data/lib/fatty/markdown/render.rb +22 -0
- data/lib/fatty/markdown.rb +4 -0
- data/lib/fatty/menu_env.rb +22 -0
- data/lib/fatty/mouse_event.rb +32 -0
- data/lib/fatty/output_buffer.rb +78 -0
- data/lib/fatty/pager.rb +801 -0
- data/lib/fatty/prompt.rb +40 -0
- data/lib/fatty/renderer/curses.rb +697 -0
- data/lib/fatty/renderer/truecolor.rb +607 -0
- data/lib/fatty/renderer.rb +419 -0
- data/lib/fatty/screen.rb +96 -0
- data/lib/fatty/search.rb +43 -0
- data/lib/fatty/session/alert_session.rb +52 -0
- data/lib/fatty/session/input_session.rb +99 -0
- data/lib/fatty/session/isearch_session.rb +172 -0
- data/lib/fatty/session/keytest_session.rb +236 -0
- data/lib/fatty/session/modal_session.rb +61 -0
- data/lib/fatty/session/output_session.rb +105 -0
- data/lib/fatty/session/popup_session.rb +540 -0
- data/lib/fatty/session/prompt_session.rb +157 -0
- data/lib/fatty/session/search_session.rb +136 -0
- data/lib/fatty/session/shell_session.rb +566 -0
- data/lib/fatty/session.rb +173 -0
- data/lib/fatty/sessions.rb +14 -0
- data/lib/fatty/terminal/popup_owner.rb +26 -0
- data/lib/fatty/terminal/progress.rb +374 -0
- data/lib/fatty/terminal.rb +1067 -0
- data/lib/fatty/themes/loader.rb +136 -0
- data/lib/fatty/themes/manager.rb +71 -0
- data/lib/fatty/themes/registry.rb +64 -0
- data/lib/fatty/themes/resolver.rb +224 -0
- data/lib/fatty/themes/themes.rb +131 -0
- data/lib/fatty/themes.rb +6 -0
- data/lib/fatty/version.rb +5 -0
- data/lib/fatty/view/alert_view.rb +14 -0
- data/lib/fatty/view/cursor_view.rb +18 -0
- data/lib/fatty/view/input_view.rb +9 -0
- data/lib/fatty/view/output_view.rb +9 -0
- data/lib/fatty/view/status_view.rb +14 -0
- data/lib/fatty/view.rb +33 -0
- data/lib/fatty/viewport.rb +90 -0
- data/lib/fatty/views.rb +9 -0
- data/lib/fatty.rb +55 -0
- data/sig/fatty.rbs +4 -0
- 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
|