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.
- checksums.yaml +4 -4
- data/CHANGELOG.org +11 -29
- data/README.md +2207 -18
- data/README.org +2066 -104
- data/exe/fatty +236 -93
- data/help/help.md +233 -0
- data/lib/fatty/action.rb +3 -7
- data/lib/fatty/actionable.rb +2 -0
- data/lib/fatty/alert.rb +65 -39
- data/lib/fatty/ansi/renderer.rb +26 -6
- data/lib/fatty/ansi.rb +1 -0
- data/lib/fatty/api/alert.rb +15 -0
- data/lib/fatty/api/choose.rb +145 -0
- data/lib/fatty/api/environment.rb +12 -0
- data/lib/fatty/api/keytest.rb +11 -0
- data/lib/fatty/api/menu.rb +104 -0
- data/lib/fatty/api/output.rb +55 -0
- data/lib/fatty/api/progress.rb +24 -0
- data/lib/fatty/api/prompt.rb +77 -0
- data/lib/fatty/api/status.rb +55 -0
- data/lib/fatty/api.rb +38 -0
- data/lib/fatty/callback_environment.rb +74 -0
- data/lib/fatty/colors/color.rb +366 -232
- data/lib/fatty/colors/pairs.rb +2 -0
- data/lib/fatty/command.rb +78 -0
- data/lib/fatty/config.rb +227 -39
- data/lib/fatty/config_files/config.yml +4 -12
- data/lib/fatty/config_files/keydefs.yml +13 -23
- data/lib/fatty/config_files/themes/capuchin_monk.yml +140 -0
- data/lib/fatty/config_files/themes/catppuccin_latte.yml +140 -0
- data/lib/fatty/config_files/themes/catppuccin_mocha.yml +140 -0
- data/lib/fatty/config_files/themes/cyberpunk.yml +141 -0
- data/lib/fatty/config_files/themes/dracula.yml +140 -0
- data/lib/fatty/config_files/themes/everforest_dark.yml +140 -0
- data/lib/fatty/config_files/themes/gruvbox_dark.yml +140 -0
- data/lib/fatty/config_files/themes/gruvbox_light.yml +140 -0
- data/lib/fatty/config_files/themes/mono.yml +119 -47
- data/lib/fatty/config_files/themes/monokai.yml +140 -0
- data/lib/fatty/config_files/themes/nordic.yml +1 -0
- data/lib/fatty/config_files/themes/onedark.yml +140 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +1 -0
- data/lib/fatty/config_files/themes/solarized_light.yml +140 -0
- data/lib/fatty/config_files/themes/terminal.yml +67 -0
- data/lib/fatty/config_files/themes/tokyo_night.yml +140 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +80 -9
- data/lib/fatty/core_ext/hash.rb +19 -0
- data/lib/fatty/core_ext/string.rb +19 -12
- data/lib/fatty/core_ext.rb +1 -0
- data/lib/fatty/curses/context.rb +86 -84
- data/lib/fatty/curses/event_source.rb +128 -40
- data/lib/fatty/curses/key_decoder.rb +77 -11
- data/lib/fatty/env.rb +21 -20
- data/lib/fatty/help.rb +1 -29
- data/lib/fatty/history/entry.rb +2 -0
- data/lib/fatty/history.rb +17 -14
- data/lib/fatty/input_buffer.rb +71 -47
- data/lib/fatty/input_field.rb +216 -149
- data/lib/fatty/key_event.rb +67 -66
- data/lib/fatty/key_map.rb +54 -16
- data/lib/fatty/keymaps/emacs.rb +40 -6
- data/lib/fatty/log_formats/json.rb +2 -0
- data/lib/fatty/log_formats/text.rb +2 -0
- data/lib/fatty/logger.rb +7 -2
- data/lib/fatty/markdown/ansi_renderer.rb +75 -6
- data/lib/fatty/markdown/render.rb +2 -3
- data/lib/fatty/mouse_event.rb +142 -2
- data/lib/fatty/output_buffer.rb +2 -0
- data/lib/fatty/pager.rb +158 -47
- data/lib/fatty/progress.rb +417 -0
- data/lib/fatty/renderer/curses.rb +340 -184
- data/lib/fatty/renderer/truecolor.rb +266 -163
- data/lib/fatty/renderer.rb +115 -97
- data/lib/fatty/screen.rb +2 -0
- data/lib/fatty/session/alert_session.rb +39 -21
- data/lib/fatty/session/isearch_session.rb +92 -38
- data/lib/fatty/session/keytest_session.rb +105 -85
- data/lib/fatty/session/modal_session.rb +38 -1
- data/lib/fatty/session/output_session.rb +472 -21
- data/lib/fatty/session/popup_session.rb +251 -134
- data/lib/fatty/session/prompt_session.rb +68 -26
- data/lib/fatty/session/search_session.rb +79 -31
- data/lib/fatty/session/shell_session.rb +383 -368
- data/lib/fatty/session/status_session.rb +167 -0
- data/lib/fatty/session.rb +95 -95
- data/lib/fatty/terminal/popup_owner.rb +14 -11
- data/lib/fatty/terminal.rb +515 -813
- data/lib/fatty/themes/loader.rb +4 -2
- data/lib/fatty/themes/manager.rb +4 -0
- data/lib/fatty/themes/registry.rb +2 -0
- data/lib/fatty/themes/resolver.rb +175 -162
- data/lib/fatty/version.rb +1 -1
- data/lib/fatty/viewport.rb +8 -1
- data/lib/fatty.rb +35 -7
- metadata +42 -40
- data/.envrc +0 -2
- data/.simplecov +0 -23
- data/.yardopts +0 -4
- data/CHANGELOG.md +0 -34
- data/Rakefile +0 -15
- data/TODO.org +0 -163
- data/examples/markdown/native-markdown.md +0 -370
- data/examples/markdown/ox-gfm-markdown.md +0 -373
- data/examples/markdown/ox-gfm-markdown.org +0 -376
- data/fatty.gemspec +0 -42
- data/lib/fatty/accept_env.rb +0 -32
- data/lib/fatty/config_files/help.md +0 -120
- data/lib/fatty/config_files/help.org +0 -124
- data/lib/fatty/config_files/themes/wordperfect_light.yml +0 -77
- data/lib/fatty/menu_env.rb +0 -22
- data/lib/fatty/session/input_session.rb +0 -99
- data/lib/fatty/sessions.rb +0 -14
- data/lib/fatty/terminal/progress.rb +0 -374
- data/lib/fatty/view/alert_view.rb +0 -14
- data/lib/fatty/view/cursor_view.rb +0 -18
- data/lib/fatty/view/input_view.rb +0 -9
- data/lib/fatty/view/output_view.rb +0 -9
- data/lib/fatty/view/status_view.rb +0 -14
- data/lib/fatty/view.rb +0 -33
- data/lib/fatty/views.rb +0 -9
- data/sig/fatty.rbs +0 -4
data/lib/fatty/curses/context.rb
CHANGED
|
@@ -19,9 +19,9 @@ module Fatty
|
|
|
19
19
|
# Those responsibilities belong to EventSource and Renderer.
|
|
20
20
|
#
|
|
21
21
|
# Context exists so that all curses state is centralized and never leaks
|
|
22
|
-
# into Sessions
|
|
22
|
+
# into Sessions or Terminal.
|
|
23
23
|
class Context
|
|
24
|
-
DEFAULT_ESC_DELAY =
|
|
24
|
+
DEFAULT_ESC_DELAY = 0
|
|
25
25
|
|
|
26
26
|
attr_reader :input_win, :output_win, :status_win, :alert_win
|
|
27
27
|
attr_reader :rows, :cols, :palette, :truecolor
|
|
@@ -33,8 +33,8 @@ module Fatty
|
|
|
33
33
|
def start
|
|
34
34
|
return self if @started
|
|
35
35
|
|
|
36
|
-
::Curses.init_screen
|
|
37
36
|
configure_escape_delay!
|
|
37
|
+
::Curses.init_screen
|
|
38
38
|
MouseConstants.ensure!
|
|
39
39
|
|
|
40
40
|
::Curses.raw
|
|
@@ -49,6 +49,33 @@ module Fatty
|
|
|
49
49
|
self
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def started?
|
|
53
|
+
@started
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def environment
|
|
57
|
+
data = {
|
|
58
|
+
started: started?,
|
|
59
|
+
truecolor: truecolor,
|
|
60
|
+
key_min: ::Curses::KEY_MIN,
|
|
61
|
+
key_max: ::Curses::KEY_MAX,
|
|
62
|
+
}
|
|
63
|
+
if started?
|
|
64
|
+
data.merge(
|
|
65
|
+
lines: ::Curses.lines,
|
|
66
|
+
cols: ::Curses.cols,
|
|
67
|
+
has_colors: ::Curses.has_colors?,
|
|
68
|
+
colors: ::Curses.colors,
|
|
69
|
+
color_pairs: ::Curses.color_pairs,
|
|
70
|
+
can_change_color: ::Curses.can_change_color?,
|
|
71
|
+
)
|
|
72
|
+
else
|
|
73
|
+
data
|
|
74
|
+
end
|
|
75
|
+
rescue StandardError
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
|
|
52
79
|
def configure_escape_delay!
|
|
53
80
|
delay =
|
|
54
81
|
if ENV["ESCDELAY"]
|
|
@@ -56,7 +83,7 @@ module Fatty
|
|
|
56
83
|
else
|
|
57
84
|
Fatty::Config.config.dig(:esc_delay)&.to_i
|
|
58
85
|
end
|
|
59
|
-
delay = DEFAULT_ESC_DELAY if delay.nil? || delay
|
|
86
|
+
delay = DEFAULT_ESC_DELAY if delay.nil? || delay.negative?
|
|
60
87
|
if ::Curses.respond_to?(:set_escdelay)
|
|
61
88
|
::Curses.set_escdelay(delay)
|
|
62
89
|
else
|
|
@@ -136,122 +163,74 @@ module Fatty
|
|
|
136
163
|
when "false", "no", "off", "0"
|
|
137
164
|
false
|
|
138
165
|
else
|
|
139
|
-
|
|
166
|
+
Fatty::Env.truecolor_detected?
|
|
140
167
|
end
|
|
141
168
|
Fatty.info(
|
|
142
169
|
"truecolor=#{@truecolor} setting=#{setting.inspect} " \
|
|
143
170
|
"TERM=#{ENV['TERM'].inspect} COLORTERM=#{ENV['COLORTERM'].inspect} " \
|
|
144
171
|
"TERM_PROGRAM=#{ENV['TERM_PROGRAM'].inspect} TMUX=#{ENV.key?('TMUX')}",
|
|
145
|
-
tag: :
|
|
172
|
+
tag: :theme,
|
|
146
173
|
)
|
|
147
174
|
@truecolor
|
|
148
175
|
end
|
|
149
176
|
|
|
150
|
-
def truecolor_env?
|
|
151
|
-
colorterm = ENV["COLORTERM"].to_s
|
|
152
|
-
term = ENV["TERM"].to_s
|
|
153
|
-
term_program = ENV["TERM_PROGRAM"].to_s
|
|
154
|
-
|
|
155
|
-
colorterm.match?(/truecolor|24bit/i) ||
|
|
156
|
-
term.match?(/truecolor|24bit|direct/i) ||
|
|
157
|
-
term.match?(/kitty|wezterm|alacritty|ghostty|foot/i) ||
|
|
158
|
-
term_program.match?(/kitty|wezterm|alacritty|ghostty|iTerm/i)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Map a Fatty::Ansi::Style to a curses attribute.
|
|
162
|
-
#
|
|
163
|
-
# - If style has no explicit fg/bg, keep the themed role pair.
|
|
164
|
-
# - If style specifies fg/bg, allocate/init a curses pair on demand.
|
|
165
|
-
#
|
|
166
|
-
# This is intentionally independent of theme roles; it is for SGR output
|
|
167
|
-
# runs inside the output pane.
|
|
168
|
-
def ansi_attr(style, fallback_role: :output)
|
|
169
|
-
base_pair_id = Fatty::Colors::Pairs::ROLE_TO_PAIR.fetch(fallback_role)
|
|
170
|
-
base_attr = ::Curses.color_pair(base_pair_id)
|
|
171
|
-
|
|
172
|
-
has_explicit = !(style.fg.nil? && style.bg.nil?)
|
|
173
|
-
attr =
|
|
174
|
-
if has_explicit
|
|
175
|
-
pair_id = ansi_pair_id(style.fg, style.bg, fallback_pair_id: base_pair_id)
|
|
176
|
-
::Curses.color_pair(pair_id)
|
|
177
|
-
else
|
|
178
|
-
base_attr
|
|
179
|
-
end
|
|
180
|
-
attr |= ::Curses::A_BOLD if style.bold
|
|
181
|
-
attr |= ::Curses::A_UNDERLINE if style.underline
|
|
182
|
-
attr |= ::Curses::A_REVERSE if style.reverse
|
|
183
|
-
if style.italic && defined?(::Curses::A_ITALIC)
|
|
184
|
-
attr |= ::Curses::A_ITALIC
|
|
185
|
-
end
|
|
186
|
-
if style.strike && defined?(::Curses::A_HORIZONTAL)
|
|
187
|
-
attr |= ::Curses::A_HORIZONTAL
|
|
188
|
-
end
|
|
189
|
-
attr
|
|
190
|
-
end
|
|
191
|
-
|
|
192
177
|
def apply_palette(palette)
|
|
178
|
+
reset_ansi_pairs!
|
|
179
|
+
|
|
193
180
|
if ::Curses.has_colors?
|
|
194
181
|
::Curses.start_color
|
|
195
182
|
::Curses.use_default_colors if ::Curses.respond_to?(:use_default_colors)
|
|
183
|
+
|
|
196
184
|
palette.each_value do |entry|
|
|
197
185
|
next unless entry[:pair]
|
|
198
186
|
|
|
199
187
|
::Curses.init_pair(entry[:pair], entry[:fg], entry[:bg])
|
|
200
188
|
end
|
|
201
189
|
end
|
|
202
|
-
@palette = palette
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
private
|
|
206
|
-
|
|
207
|
-
def ensure_started!
|
|
208
|
-
return if @started
|
|
209
|
-
|
|
210
|
-
raise "Curses::Context not started"
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def close_windows
|
|
214
|
-
@output_win&.close
|
|
215
|
-
@input_win&.close
|
|
216
|
-
@alert_win&.close
|
|
217
|
-
@status_win&.close
|
|
218
190
|
|
|
219
|
-
@
|
|
220
|
-
@input_win = nil
|
|
221
|
-
@alert_win = nil
|
|
222
|
-
@status_win = nil
|
|
191
|
+
@palette = palette
|
|
223
192
|
end
|
|
224
193
|
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
194
|
+
# Return a curses color-pair id for an ANSI foreground/background pair.
|
|
195
|
+
#
|
|
196
|
+
# This owns the dynamic ANSI pair cache. Renderers decide which fg/bg
|
|
197
|
+
# indexes they want; Context decides whether a pair can be allocated.
|
|
198
|
+
def ansi_pair_for(fg, bg, fallback_pair_id:)
|
|
199
|
+
return fallback_pair_id unless ::Curses.has_colors?
|
|
229
200
|
|
|
230
201
|
@ansi_pairs ||= {}
|
|
231
202
|
@ansi_pair_limit ||= begin
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
203
|
+
::Curses.color_pairs.to_i
|
|
204
|
+
rescue StandardError
|
|
205
|
+
256
|
|
206
|
+
end
|
|
236
207
|
|
|
237
|
-
key = [fg
|
|
208
|
+
key = [fg.nil? ? -1 : fg.to_i, bg.nil? ? -1 : bg.to_i]
|
|
238
209
|
cached = @ansi_pairs[key]
|
|
239
210
|
return cached if cached
|
|
240
211
|
|
|
241
|
-
|
|
242
|
-
start_id = 64
|
|
212
|
+
start_id = Fatty::Colors::Pairs::ROLE_TO_PAIR.values.max + 1
|
|
243
213
|
@ansi_next_pair_id ||= start_id
|
|
244
214
|
@ansi_next_pair_id = start_id if @ansi_next_pair_id < start_id
|
|
245
215
|
|
|
246
|
-
if @ansi_next_pair_id >= @ansi_pair_limit
|
|
216
|
+
if @ansi_pair_limit.positive? && @ansi_next_pair_id >= @ansi_pair_limit
|
|
217
|
+
Fatty.debug(
|
|
218
|
+
"ansi_pair_for exhausted next=#{@ansi_next_pair_id.inspect} " \
|
|
219
|
+
"limit=#{@ansi_pair_limit.inspect} fg=#{fg.inspect} bg=#{bg.inspect} " \
|
|
220
|
+
"fallback=#{fallback_pair_id.inspect}",
|
|
221
|
+
tag: :theme,
|
|
222
|
+
)
|
|
247
223
|
@ansi_pairs[key] = fallback_pair_id
|
|
248
224
|
else
|
|
249
225
|
pair_id = @ansi_next_pair_id
|
|
250
226
|
@ansi_next_pair_id += 1
|
|
251
227
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
228
|
+
::Curses.init_pair(
|
|
229
|
+
pair_id,
|
|
230
|
+
fg.nil? ? -1 : fg.to_i,
|
|
231
|
+
bg.nil? ? -1 : bg.to_i,
|
|
232
|
+
)
|
|
233
|
+
|
|
255
234
|
@ansi_pairs[key] = pair_id
|
|
256
235
|
end
|
|
257
236
|
|
|
@@ -260,7 +239,30 @@ module Fatty
|
|
|
260
239
|
|
|
261
240
|
def reset_ansi_pairs!
|
|
262
241
|
@ansi_pairs = {}
|
|
263
|
-
@ansi_next_pair_id =
|
|
242
|
+
@ansi_next_pair_id = nil
|
|
243
|
+
@ansi_pair_limit = nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
private
|
|
247
|
+
|
|
248
|
+
# simplecov:disable
|
|
249
|
+
|
|
250
|
+
def ensure_started!
|
|
251
|
+
return if @started
|
|
252
|
+
|
|
253
|
+
raise "Curses::Context not started"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def close_windows
|
|
257
|
+
@output_win&.close
|
|
258
|
+
@input_win&.close
|
|
259
|
+
@alert_win&.close
|
|
260
|
+
@status_win&.close
|
|
261
|
+
|
|
262
|
+
@output_win = nil
|
|
263
|
+
@input_win = nil
|
|
264
|
+
@alert_win = nil
|
|
265
|
+
@status_win = nil
|
|
264
266
|
end
|
|
265
267
|
|
|
266
268
|
def enable_bracketed_paste!
|
|
@@ -23,6 +23,13 @@ module Fatty
|
|
|
23
23
|
::Curses::BUTTON3_TRIPLE_CLICKED => :right_triple_clicked
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
|
+
SCROLL_UP_BSTATE = ::Curses::BUTTON4_PRESSED
|
|
27
|
+
SCROLL_DOWN_BSTATE = 0x10000000
|
|
28
|
+
MOUSE_MODIFIER_MASK =
|
|
29
|
+
::Curses::BUTTON_CTRL |
|
|
30
|
+
::Curses::BUTTON_ALT |
|
|
31
|
+
::Curses::BUTTON_SHIFT
|
|
32
|
+
|
|
26
33
|
ESCAPE_LOOKAHEAD_MS = 0
|
|
27
34
|
|
|
28
35
|
BRACKETED_PASTE_START = "[200~"
|
|
@@ -30,32 +37,64 @@ module Fatty
|
|
|
30
37
|
|
|
31
38
|
attr_reader :context, :key_decoder
|
|
32
39
|
|
|
33
|
-
def initialize(context
|
|
40
|
+
def initialize(context: Context.new, key_decoder: nil, poll_ms: 200)
|
|
34
41
|
@context = context
|
|
35
|
-
@key_decoder = key_decoder
|
|
42
|
+
@key_decoder = key_decoder || KeyDecoder.new
|
|
36
43
|
@poll_ms = poll_ms.to_i
|
|
37
44
|
configure_input_polling!
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
def next_event
|
|
41
|
-
|
|
48
|
+
pending_events.shift || read_event
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def interrupt_pending?
|
|
52
|
+
command = read_event(timeout_ms: 0)
|
|
53
|
+
interrupted = interrupt_command?(command)
|
|
54
|
+
|
|
55
|
+
pending_events << command if command && !interrupted
|
|
56
|
+
interrupted
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# simplecov:disable
|
|
62
|
+
|
|
63
|
+
def read_event(timeout_ms: @poll_ms)
|
|
64
|
+
raw = read_raw(timeout_ms:)
|
|
42
65
|
return unless raw
|
|
43
66
|
|
|
44
67
|
if raw.is_a?(Fatty::MouseEvent)
|
|
45
|
-
|
|
68
|
+
Command.session(:active, :key, event: raw)
|
|
69
|
+
elsif raw.is_a?(Array) && raw.first == :paste
|
|
70
|
+
# This :terminal_paste is a terminal bracketed-paste event, not an
|
|
71
|
+
# editor paste action. Treat the pasted bytes as one insert
|
|
72
|
+
# operation instead of decoding them as individual key events.
|
|
73
|
+
Command.session(:active, :terminal_paste, text: raw.last)
|
|
74
|
+
elsif (ev = @key_decoder.decode(raw))
|
|
75
|
+
if ev.key == :resize
|
|
76
|
+
Command.terminal(:resize)
|
|
77
|
+
else
|
|
78
|
+
Command.session(:active, :key, event: ev)
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
Fatty.warn("undecoded input: #{raw.inspect}", tag: :input)
|
|
82
|
+
nil
|
|
46
83
|
end
|
|
84
|
+
end
|
|
47
85
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
86
|
+
def interrupt_command?(command)
|
|
87
|
+
return false unless command&.action == :key
|
|
51
88
|
|
|
52
|
-
|
|
53
|
-
return unless
|
|
89
|
+
event = command.payload[:event]
|
|
90
|
+
return false unless event.is_a?(Fatty::KeyEvent)
|
|
54
91
|
|
|
55
|
-
[:
|
|
92
|
+
event.ctrl? && [:c, :g].include?(event.key)
|
|
56
93
|
end
|
|
57
94
|
|
|
58
|
-
|
|
95
|
+
def pending_events
|
|
96
|
+
@pending_events ||= []
|
|
97
|
+
end
|
|
59
98
|
|
|
60
99
|
def window
|
|
61
100
|
context.input_win
|
|
@@ -70,54 +109,98 @@ module Fatty
|
|
|
70
109
|
win.timeout = @poll_ms if win.respond_to?(:timeout=)
|
|
71
110
|
end
|
|
72
111
|
|
|
73
|
-
def read_raw
|
|
112
|
+
def read_raw(timeout_ms: @poll_ms)
|
|
74
113
|
return unless window
|
|
75
114
|
|
|
76
|
-
|
|
115
|
+
pending = read_pending_raw
|
|
116
|
+
return pending if pending
|
|
117
|
+
|
|
118
|
+
window.timeout = timeout_ms if window.respond_to?(:timeout=)
|
|
77
119
|
|
|
78
120
|
ch = window.getch
|
|
79
121
|
return if ch == -1
|
|
80
122
|
return unless ch
|
|
81
123
|
|
|
124
|
+
log_keycode(ch)
|
|
125
|
+
|
|
82
126
|
if ch.is_a?(Integer) && ch == ::Curses::KEY_MOUSE
|
|
83
127
|
mouse = ::Curses.getmouse
|
|
84
128
|
Fatty.debug("EventSource#read_raw: bstate=#{mouse&.bstate}", tag: :mouse)
|
|
85
129
|
return decode_mouse(mouse)
|
|
86
130
|
end
|
|
87
131
|
|
|
88
|
-
|
|
89
|
-
Fatty.debug(
|
|
90
|
-
:curses_getch,
|
|
91
|
-
tag: :keycode,
|
|
92
|
-
ch_class: ch.class.name,
|
|
93
|
-
ch_inspect: ch.inspect,
|
|
94
|
-
ch_int: (ch.is_a?(Integer) ? ch : nil),
|
|
95
|
-
ch_chr: (ch.is_a?(Integer) && ch.between?(0, 255) ? ch.chr : nil),
|
|
96
|
-
)
|
|
97
|
-
end
|
|
132
|
+
return ch unless escape_char?(ch)
|
|
98
133
|
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
read_escape_raw(ch)
|
|
135
|
+
end
|
|
101
136
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
[ch, nxt]
|
|
112
|
-
end
|
|
137
|
+
def read_escape_raw(ch)
|
|
138
|
+
nxt = with_window_timeout(escape_lookahead_ms) { window.getch }
|
|
139
|
+
return ch if nxt == -1 || !nxt
|
|
140
|
+
|
|
141
|
+
if csi_prefix?(nxt)
|
|
142
|
+
suffix = read_escape_suffix
|
|
143
|
+
seq = "[" + suffix
|
|
144
|
+
if seq == BRACKETED_PASTE_START
|
|
145
|
+
[:paste, read_bracketed_paste]
|
|
113
146
|
else
|
|
147
|
+
push_pending_raw(*suffix.chars.reverse)
|
|
148
|
+
push_pending_raw(nxt)
|
|
149
|
+
ch
|
|
150
|
+
end
|
|
151
|
+
elsif ss3_prefix?(nxt)
|
|
152
|
+
suffix = with_window_timeout(escape_lookahead_ms) { window.getch }
|
|
153
|
+
if suffix == -1 || !suffix
|
|
114
154
|
[ch, nxt]
|
|
155
|
+
else
|
|
156
|
+
[ch, nxt, suffix]
|
|
115
157
|
end
|
|
116
158
|
else
|
|
117
|
-
ch
|
|
159
|
+
[ch, nxt]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ss3_prefix?(ch)
|
|
164
|
+
ch == "O".ord || ch == "O"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def read_pending_raw
|
|
168
|
+
@pending_raw ||= []
|
|
169
|
+
@pending_raw.shift
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def push_pending_raw(*values)
|
|
173
|
+
@pending_raw ||= []
|
|
174
|
+
values.each do |value|
|
|
175
|
+
@pending_raw.unshift(value)
|
|
118
176
|
end
|
|
119
177
|
end
|
|
120
178
|
|
|
179
|
+
def escape_lookahead_ms
|
|
180
|
+
Fatty::Config.config.fetch(:esc_delay, ESCAPE_LOOKAHEAD_MS).to_i
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def escape_char?(ch)
|
|
184
|
+
ch.is_a?(Integer) && ch == 27
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def csi_prefix?(ch)
|
|
188
|
+
ch == "[".ord || ch == "["
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def log_keycode(ch)
|
|
192
|
+
return unless Fatty::Config.config.dig(:log, :tags)&.include?(:keycode)
|
|
193
|
+
|
|
194
|
+
Fatty.debug(
|
|
195
|
+
:curses_getch,
|
|
196
|
+
tag: :keycode,
|
|
197
|
+
ch_class: ch.class.name,
|
|
198
|
+
ch_inspect: ch.inspect,
|
|
199
|
+
ch_int: (ch.is_a?(Integer) ? ch : nil),
|
|
200
|
+
ch_chr: (ch.is_a?(Integer) && ch.between?(0, 255) ? ch.chr : nil),
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
121
204
|
def with_window_timeout(ms)
|
|
122
205
|
result = nil
|
|
123
206
|
win = window
|
|
@@ -158,16 +241,21 @@ module Fatty
|
|
|
158
241
|
end
|
|
159
242
|
|
|
160
243
|
def mouse_button_from_bstate(bstate)
|
|
161
|
-
|
|
162
|
-
return :
|
|
244
|
+
button_state = bstate & ~MOUSE_MODIFIER_MASK
|
|
245
|
+
return :scroll_up if (button_state & SCROLL_UP_BSTATE).positive?
|
|
246
|
+
return :scroll_down if (button_state & SCROLL_DOWN_BSTATE).positive?
|
|
163
247
|
|
|
164
248
|
MOUSE_BSTATE_BUTTON_MAP.each do |mask, button|
|
|
165
|
-
return button if (
|
|
249
|
+
return button if (button_state & mask).positive?
|
|
166
250
|
end
|
|
167
251
|
|
|
168
252
|
nil
|
|
169
253
|
end
|
|
170
254
|
|
|
255
|
+
def mouse_button_pressed?(button_state, const_name)
|
|
256
|
+
::Curses.const_defined?(const_name) && (button_state & ::Curses.const_get(const_name)).positive?
|
|
257
|
+
end
|
|
258
|
+
|
|
171
259
|
def read_escape_suffix
|
|
172
260
|
suffix = +""
|
|
173
261
|
done = false
|
|
@@ -9,9 +9,29 @@ module Fatty
|
|
|
9
9
|
# :ctrl, and :meta, in the KeyEvent so that the KeyMap can assign different
|
|
10
10
|
# actions to the modified keys.
|
|
11
11
|
class KeyDecoder
|
|
12
|
+
SS3_TO_EVENT = {
|
|
13
|
+
"j" => KeyEvent.new(key: :keypad_multiply),
|
|
14
|
+
"k" => KeyEvent.new(key: :keypad_plus),
|
|
15
|
+
"l" => KeyEvent.new(key: :keypad_comma),
|
|
16
|
+
"m" => KeyEvent.new(key: :keypad_minus),
|
|
17
|
+
"n" => KeyEvent.new(key: :keypad_decimal),
|
|
18
|
+
"o" => KeyEvent.new(key: :keypad_divide),
|
|
19
|
+
"p" => KeyEvent.new(key: :keypad_0),
|
|
20
|
+
"q" => KeyEvent.new(key: :keypad_1),
|
|
21
|
+
"r" => KeyEvent.new(key: :keypad_2),
|
|
22
|
+
"s" => KeyEvent.new(key: :keypad_3),
|
|
23
|
+
"t" => KeyEvent.new(key: :keypad_4),
|
|
24
|
+
"u" => KeyEvent.new(key: :keypad_5),
|
|
25
|
+
"v" => KeyEvent.new(key: :keypad_6),
|
|
26
|
+
"w" => KeyEvent.new(key: :keypad_7),
|
|
27
|
+
"x" => KeyEvent.new(key: :keypad_8),
|
|
28
|
+
"y" => KeyEvent.new(key: :keypad_9),
|
|
29
|
+
"M" => KeyEvent.new(key: :keypad_enter),
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
12
32
|
attr_reader :map, :env
|
|
13
33
|
|
|
14
|
-
def initialize(env:)
|
|
34
|
+
def initialize(env: Env.detect)
|
|
15
35
|
@env = env
|
|
16
36
|
@map = {}
|
|
17
37
|
load_builtin_map
|
|
@@ -26,7 +46,11 @@ module Fatty
|
|
|
26
46
|
result =
|
|
27
47
|
case raw
|
|
28
48
|
when Array
|
|
29
|
-
|
|
49
|
+
if raw.length == 3 && ss3_sequence?(raw)
|
|
50
|
+
decode_ss3(raw)
|
|
51
|
+
else
|
|
52
|
+
decode_meta(raw)
|
|
53
|
+
end
|
|
30
54
|
else
|
|
31
55
|
decode_single(raw)
|
|
32
56
|
end
|
|
@@ -36,6 +60,32 @@ module Fatty
|
|
|
36
60
|
|
|
37
61
|
private
|
|
38
62
|
|
|
63
|
+
# simplecov:disable
|
|
64
|
+
|
|
65
|
+
def ss3_sequence?(raw)
|
|
66
|
+
raw[0] == 27 && raw[1] == "O"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def decode_ss3(raw)
|
|
70
|
+
_esc, _prefix, suffix = raw
|
|
71
|
+
event = SS3_TO_EVENT[raw_char_to_s(suffix)]
|
|
72
|
+
if event
|
|
73
|
+
KeyEvent.new(
|
|
74
|
+
key: event.key,
|
|
75
|
+
ctrl: event.ctrl?,
|
|
76
|
+
meta: event.meta?,
|
|
77
|
+
shift: event.shift?,
|
|
78
|
+
raw: raw,
|
|
79
|
+
)
|
|
80
|
+
else
|
|
81
|
+
KeyEvent.new(key: raw_char_to_s(suffix).to_sym, raw: raw)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def raw_char_to_s(value)
|
|
86
|
+
value.is_a?(Integer) ? value.chr : value.to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
39
89
|
# Handle the decoding of meta keys which are returned as an Array of the
|
|
40
90
|
# escape code followed by another key, nxt, which can be either a String
|
|
41
91
|
# or an Integer.
|
|
@@ -143,21 +193,35 @@ module Fatty
|
|
|
143
193
|
# different terminal programs process keys.
|
|
144
194
|
def load_user_config
|
|
145
195
|
config = Fatty::Config.keydefs
|
|
146
|
-
|
|
147
|
-
return unless config
|
|
196
|
+
return nil unless config
|
|
148
197
|
|
|
149
198
|
terminal = @env[:terminal]
|
|
150
199
|
Fatty.info("KeyDecoder#load_user_config: detected terminal `#{terminal}`")
|
|
151
200
|
Fatty.info("KeyDecoder#load_user_config: only keydefs for `#{terminal}` will be loaded")
|
|
152
|
-
section = config
|
|
201
|
+
section = keydef_section(config, terminal.to_s)
|
|
153
202
|
return unless section
|
|
154
203
|
|
|
155
204
|
section.each do |code, spec|
|
|
156
|
-
|
|
157
|
-
Fatty.debug("KeyDecoder#load_user_config: user keydef: code: #{code} -> #{spec}")
|
|
205
|
+
register_user_keydef(code.to_i, spec)
|
|
158
206
|
end
|
|
159
207
|
end
|
|
160
208
|
|
|
209
|
+
def register_user_keydef(code, spec)
|
|
210
|
+
@map[Integer(code.to_s)] = KeyEvent.new(**normalize_spec(spec))
|
|
211
|
+
Fatty.debug("KeyDecoder#load_user_config: user keydef: code: #{code} -> #{spec}", tag: :keycode)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Because =fat_config= canonicalizes keys by translating hyphen (-) to
|
|
215
|
+
# underscore (_) so that the keys are valid ruby varaible names, we need
|
|
216
|
+
# to look for that variation of the terminal name to get the right
|
|
217
|
+
# section of the keydefs.yml config Hash.
|
|
218
|
+
def keydef_section(config, terminal)
|
|
219
|
+
config[terminal.to_sym] ||
|
|
220
|
+
config[terminal] ||
|
|
221
|
+
config[terminal.tr("-", "_").to_sym] ||
|
|
222
|
+
config[terminal.tr("-", "_")]
|
|
223
|
+
end
|
|
224
|
+
|
|
161
225
|
# Add to the @map keydefs as assumed by the curses library, defined in the
|
|
162
226
|
# constant Fatty::CUSRSES_TO_EVENT. These can be overridden by the the
|
|
163
227
|
# user config in #load_user_config.
|
|
@@ -171,11 +235,13 @@ module Fatty
|
|
|
171
235
|
|
|
172
236
|
# Make sure the user config Hash uses symbols for its keys. Return the
|
|
173
237
|
# Hash as so corrected.
|
|
174
|
-
def normalize_spec(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
spec[:key] = spec[:key].to_sym
|
|
238
|
+
def normalize_spec(value)
|
|
239
|
+
unless value.is_a?(Hash)
|
|
240
|
+
raise ArgumentError, "keydef spec must be a Hash, got #{value.class}: #{value.inspect}"
|
|
178
241
|
end
|
|
242
|
+
|
|
243
|
+
spec = value.transform_keys(&:to_sym)
|
|
244
|
+
spec[:key] = spec[:key].to_sym if spec[:key].is_a?(String)
|
|
179
245
|
spec
|
|
180
246
|
end
|
|
181
247
|
end
|
data/lib/fatty/env.rb
CHANGED
|
@@ -6,19 +6,22 @@ module Fatty
|
|
|
6
6
|
|
|
7
7
|
def detect
|
|
8
8
|
{
|
|
9
|
-
os: os,
|
|
10
9
|
arch: arch,
|
|
10
|
+
os: os,
|
|
11
11
|
ruby_platform: RUBY_PLATFORM,
|
|
12
|
+
screen: screen?,
|
|
13
|
+
ssh: ssh?,
|
|
12
14
|
term: ENV["TERM"],
|
|
13
15
|
terminal: detect_terminal_program,
|
|
14
16
|
terminal_version: ENV["KONSOLE_VERSION"] || ENV["TERM_PROGRAM_VERSION"],
|
|
15
17
|
tmux: tmux?,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
curses: curses_info,
|
|
18
|
+
truecolor_detected: truecolor_detected?,
|
|
19
|
+
curses: {},
|
|
19
20
|
}
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
private
|
|
24
|
+
|
|
22
25
|
# -----------------------
|
|
23
26
|
# OS / platform
|
|
24
27
|
# -----------------------
|
|
@@ -60,16 +63,29 @@ module Fatty
|
|
|
60
63
|
return "ghostty" if ENV["TERM_PROGRAM"]&.downcase == "ghostty"
|
|
61
64
|
return "alacritty" if ENV.key?("ALACRITTY_LOG")
|
|
62
65
|
return "terminator" if ENV.key?("TERMINATOR_UUID")
|
|
66
|
+
return "gnome-terminal" if ENV.key?("GNOME_TERMINAL_SCREEN") || ENV.key?("GNOME_TERMINAL_SERVICE")
|
|
67
|
+
return "vte" if ENV.key?("VTE_VERSION")
|
|
63
68
|
|
|
64
69
|
if ENV.key?("TERM_PROGRAM")
|
|
65
70
|
ENV["TERM_PROGRAM"]&.downcase
|
|
66
71
|
elsif ENV.key?("TERM")
|
|
67
72
|
ENV["TERM"]&.downcase
|
|
68
73
|
else
|
|
69
|
-
|
|
74
|
+
"unknown"
|
|
70
75
|
end
|
|
71
76
|
end
|
|
72
77
|
|
|
78
|
+
def self.truecolor_detected?
|
|
79
|
+
colorterm = ENV["COLORTERM"].to_s
|
|
80
|
+
term = ENV["TERM"].to_s
|
|
81
|
+
term_program = ENV["TERM_PROGRAM"].to_s
|
|
82
|
+
|
|
83
|
+
colorterm.match?(/truecolor|24bit/i) ||
|
|
84
|
+
term.match?(/truecolor|24bit|direct/i) ||
|
|
85
|
+
term.match?(/kitty|wezterm|alacritty|ghostty|foot/i) ||
|
|
86
|
+
term_program.match?(/kitty|wezterm|alacritty|ghostty|iTerm/i)
|
|
87
|
+
end
|
|
88
|
+
|
|
73
89
|
def tmux?
|
|
74
90
|
ENV.key?("TMUX")
|
|
75
91
|
end
|
|
@@ -81,20 +97,5 @@ module Fatty
|
|
|
81
97
|
def ssh?
|
|
82
98
|
ENV.key?("SSH_TTY") || ENV.key?("SSH_CONNECTION")
|
|
83
99
|
end
|
|
84
|
-
|
|
85
|
-
# -----------------------
|
|
86
|
-
# ncurses capabilities
|
|
87
|
-
# -----------------------
|
|
88
|
-
|
|
89
|
-
def curses_info
|
|
90
|
-
return {} unless defined?(::Curses)
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
key_min: ::Curses::KEY_MIN,
|
|
94
|
-
key_max: ::Curses::KEY_MAX,
|
|
95
|
-
}
|
|
96
|
-
rescue StandardError
|
|
97
|
-
{}
|
|
98
|
-
end
|
|
99
100
|
end
|
|
100
101
|
end
|