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,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
module Curses
|
|
5
|
+
class EventSource
|
|
6
|
+
MOUSE_BSTATE_BUTTON_MAP = {
|
|
7
|
+
::Curses::BUTTON1_PRESSED => :left_pressed,
|
|
8
|
+
::Curses::BUTTON1_RELEASED => :left_released,
|
|
9
|
+
::Curses::BUTTON1_CLICKED => :left_clicked,
|
|
10
|
+
::Curses::BUTTON1_DOUBLE_CLICKED => :left_double_clicked,
|
|
11
|
+
::Curses::BUTTON1_TRIPLE_CLICKED => :left_triple_clicked,
|
|
12
|
+
|
|
13
|
+
::Curses::BUTTON2_PRESSED => :middle_pressed,
|
|
14
|
+
::Curses::BUTTON2_RELEASED => :middle_released,
|
|
15
|
+
::Curses::BUTTON2_CLICKED => :middle_clicked,
|
|
16
|
+
::Curses::BUTTON2_DOUBLE_CLICKED => :middle_double_clicked,
|
|
17
|
+
::Curses::BUTTON2_TRIPLE_CLICKED => :middle_triple_clicked,
|
|
18
|
+
|
|
19
|
+
::Curses::BUTTON3_PRESSED => :right_pressed,
|
|
20
|
+
::Curses::BUTTON3_RELEASED => :right_released,
|
|
21
|
+
::Curses::BUTTON3_CLICKED => :right_clicked,
|
|
22
|
+
::Curses::BUTTON3_DOUBLE_CLICKED => :right_double_clicked,
|
|
23
|
+
::Curses::BUTTON3_TRIPLE_CLICKED => :right_triple_clicked
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
ESCAPE_LOOKAHEAD_MS = 0
|
|
27
|
+
|
|
28
|
+
BRACKETED_PASTE_START = "[200~"
|
|
29
|
+
BRACKETED_PASTE_END = "\e[201~"
|
|
30
|
+
|
|
31
|
+
attr_reader :context, :key_decoder
|
|
32
|
+
|
|
33
|
+
def initialize(context:, key_decoder:, poll_ms: 200)
|
|
34
|
+
@context = context
|
|
35
|
+
@key_decoder = key_decoder
|
|
36
|
+
@poll_ms = poll_ms.to_i
|
|
37
|
+
configure_input_polling!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def next_event
|
|
41
|
+
raw = read_raw
|
|
42
|
+
return unless raw
|
|
43
|
+
|
|
44
|
+
if raw.is_a?(Fatty::MouseEvent)
|
|
45
|
+
return [:key, raw]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if raw.is_a?(Array) && raw.first == :paste
|
|
49
|
+
return [:cmd, :paste, { text: raw.last }]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
ev = @key_decoder.decode(raw)
|
|
53
|
+
return unless ev
|
|
54
|
+
|
|
55
|
+
[:key, ev]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def window
|
|
61
|
+
context.input_win
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def configure_input_polling!
|
|
65
|
+
win = window
|
|
66
|
+
return unless win
|
|
67
|
+
|
|
68
|
+
# Make getch return nil periodically so Terminal can keep rendering,
|
|
69
|
+
# which allows output to animate smoothly.
|
|
70
|
+
win.timeout = @poll_ms if win.respond_to?(:timeout=)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def read_raw
|
|
74
|
+
return unless window
|
|
75
|
+
|
|
76
|
+
window.timeout = @poll_ms if window.respond_to?(:timeout=)
|
|
77
|
+
|
|
78
|
+
ch = window.getch
|
|
79
|
+
return if ch == -1
|
|
80
|
+
return unless ch
|
|
81
|
+
|
|
82
|
+
if ch.is_a?(Integer) && ch == ::Curses::KEY_MOUSE
|
|
83
|
+
mouse = ::Curses.getmouse
|
|
84
|
+
Fatty.debug("EventSource#read_raw: bstate=#{mouse&.bstate}", tag: :mouse)
|
|
85
|
+
return decode_mouse(mouse)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if Fatty::Config.config.dig(:log, :tags)&.include?(:keycode)
|
|
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
|
|
98
|
+
|
|
99
|
+
if ch.is_a?(Integer) && ch == 27
|
|
100
|
+
nxt = with_window_timeout(ESCAPE_LOOKAHEAD_MS) { window.getch }
|
|
101
|
+
|
|
102
|
+
if nxt == -1 || !nxt
|
|
103
|
+
ch
|
|
104
|
+
elsif nxt == "[".ord || nxt == "["
|
|
105
|
+
suffix = read_escape_suffix
|
|
106
|
+
seq = "[" + suffix
|
|
107
|
+
|
|
108
|
+
if seq == BRACKETED_PASTE_START
|
|
109
|
+
[:paste, read_bracketed_paste]
|
|
110
|
+
else
|
|
111
|
+
[ch, nxt]
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
[ch, nxt]
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
ch
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def with_window_timeout(ms)
|
|
122
|
+
result = nil
|
|
123
|
+
win = window
|
|
124
|
+
if win&.respond_to?(:timeout=)
|
|
125
|
+
begin
|
|
126
|
+
win.timeout = ms
|
|
127
|
+
result = yield
|
|
128
|
+
ensure
|
|
129
|
+
win.timeout = @poll_ms
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
result = yield
|
|
133
|
+
end
|
|
134
|
+
result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def decode_mouse(mouse)
|
|
138
|
+
return unless mouse&.respond_to?(:bstate)
|
|
139
|
+
|
|
140
|
+
bstate = mouse.bstate
|
|
141
|
+
|
|
142
|
+
ctrl = (bstate & ::Curses::BUTTON_CTRL).positive?
|
|
143
|
+
meta = (bstate & ::Curses::BUTTON_ALT).positive?
|
|
144
|
+
shift = (bstate & ::Curses::BUTTON_SHIFT).positive?
|
|
145
|
+
|
|
146
|
+
button = mouse_button_from_bstate(bstate)
|
|
147
|
+
return unless button
|
|
148
|
+
|
|
149
|
+
Fatty::MouseEvent.new(
|
|
150
|
+
button: button,
|
|
151
|
+
x: mouse.x,
|
|
152
|
+
y: mouse.y,
|
|
153
|
+
ctrl: ctrl,
|
|
154
|
+
meta: meta,
|
|
155
|
+
shift: shift,
|
|
156
|
+
raw: mouse,
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def mouse_button_from_bstate(bstate)
|
|
161
|
+
return :scroll_up if (bstate & ::Curses::BUTTON4_PRESSED).positive?
|
|
162
|
+
return :scroll_down if (bstate & ::Curses::BUTTON5_PRESSED).positive?
|
|
163
|
+
|
|
164
|
+
MOUSE_BSTATE_BUTTON_MAP.each do |mask, button|
|
|
165
|
+
return button if (bstate & mask).positive?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def read_escape_suffix
|
|
172
|
+
suffix = +""
|
|
173
|
+
done = false
|
|
174
|
+
|
|
175
|
+
until done
|
|
176
|
+
ch = with_window_timeout(0) { window.getch }
|
|
177
|
+
|
|
178
|
+
if ch == -1 || !ch
|
|
179
|
+
done = true
|
|
180
|
+
else
|
|
181
|
+
suffix << raw_char_to_s(ch)
|
|
182
|
+
done = true if suffix.end_with?("~")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
suffix
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def read_bracketed_paste
|
|
190
|
+
text = +""
|
|
191
|
+
done = false
|
|
192
|
+
limit = 1_000_000
|
|
193
|
+
hit_limit = false
|
|
194
|
+
|
|
195
|
+
until done
|
|
196
|
+
ch = window.getch
|
|
197
|
+
|
|
198
|
+
if ch == -1 || !ch
|
|
199
|
+
done = true
|
|
200
|
+
else
|
|
201
|
+
text << raw_char_to_s(ch)
|
|
202
|
+
if text.end_with?(BRACKETED_PASTE_END)
|
|
203
|
+
text = text.delete_suffix(BRACKETED_PASTE_END)
|
|
204
|
+
done = true
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if text.length >= limit
|
|
209
|
+
hit_limit = true
|
|
210
|
+
done = true
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
Fatty.debug("EventSource#read_bracketed_paste: hit limit #{limit}", tag: :keycode) if hit_limit
|
|
215
|
+
text
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def raw_char_to_s(ch)
|
|
219
|
+
case ch
|
|
220
|
+
when String
|
|
221
|
+
ch
|
|
222
|
+
when Integer
|
|
223
|
+
ch.between?(0, 255) ? ch.chr : ""
|
|
224
|
+
else
|
|
225
|
+
""
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
module Curses
|
|
5
|
+
# The KeyDecoder class is responsible for converting raw key events returned
|
|
6
|
+
# by the curses library into KeyEvent's to be used by Fatty. This allows
|
|
7
|
+
# Fatty to deal with keyboard actions using friendly names, like
|
|
8
|
+
# :home. :page_down, :f5, and so forth. It also sets the modifiers, :shift,
|
|
9
|
+
# :ctrl, and :meta, in the KeyEvent so that the KeyMap can assign different
|
|
10
|
+
# actions to the modified keys.
|
|
11
|
+
class KeyDecoder
|
|
12
|
+
attr_reader :map, :env
|
|
13
|
+
|
|
14
|
+
def initialize(env:)
|
|
15
|
+
@env = env
|
|
16
|
+
@map = {}
|
|
17
|
+
load_builtin_map
|
|
18
|
+
load_user_config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Return a KeyEvent object based on the raw keyboard input as returned by
|
|
22
|
+
# Screen#read_raw.
|
|
23
|
+
def decode(raw)
|
|
24
|
+
return unless raw
|
|
25
|
+
|
|
26
|
+
result =
|
|
27
|
+
case raw
|
|
28
|
+
when Array
|
|
29
|
+
decode_meta(raw)
|
|
30
|
+
else
|
|
31
|
+
decode_single(raw)
|
|
32
|
+
end
|
|
33
|
+
Fatty.debug("#{self.class}#decode(raw: #{raw}) -> #{result}", tag: :keycode)
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Handle the decoding of meta keys which are returned as an Array of the
|
|
40
|
+
# escape code followed by another key, nxt, which can be either a String
|
|
41
|
+
# or an Integer.
|
|
42
|
+
def decode_meta((esc, nxt))
|
|
43
|
+
case nxt
|
|
44
|
+
when String
|
|
45
|
+
# Meta + printable => action key, not self-insert
|
|
46
|
+
KeyEvent.new(
|
|
47
|
+
key: nxt.to_sym,
|
|
48
|
+
meta: true,
|
|
49
|
+
raw: [esc, nxt],
|
|
50
|
+
)
|
|
51
|
+
when Integer
|
|
52
|
+
# Meta + NUL => Meta-Ctrl-@
|
|
53
|
+
if nxt == 0
|
|
54
|
+
return KeyEvent.new(
|
|
55
|
+
key: :'@',
|
|
56
|
+
ctrl: true,
|
|
57
|
+
meta: true,
|
|
58
|
+
raw: [esc, nxt],
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if (base = @map[nxt])
|
|
63
|
+
KeyEvent.new(
|
|
64
|
+
key: base.key,
|
|
65
|
+
ctrl: base.ctrl?,
|
|
66
|
+
shift: base.shift?,
|
|
67
|
+
meta: true,
|
|
68
|
+
raw: [esc, nxt],
|
|
69
|
+
)
|
|
70
|
+
else
|
|
71
|
+
KeyEvent.new(key: nxt, meta: true, raw: [esc, nxt])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Decode a raw input returned as a single character rather than an Array.
|
|
77
|
+
def decode_single(ch)
|
|
78
|
+
if ch.is_a?(Integer) && @map.key?(ch)
|
|
79
|
+
Fatty.debug("#{self.class}#decode_single: #{ch} found in @map -> #{@map[ch]}", tag: :keycode)
|
|
80
|
+
ev = @map[ch]
|
|
81
|
+
|
|
82
|
+
# Some terminals/configs map printable ASCII keycodes (like 97 for "a")
|
|
83
|
+
# through keydefs. Those KeyEvents often have nil text, which breaks
|
|
84
|
+
# self-insert in sessions that fall back on ev.text.
|
|
85
|
+
if ev.respond_to?(:text) && ev.text.nil? &&
|
|
86
|
+
ev.respond_to?(:ctrl?) && ev.respond_to?(:meta?) &&
|
|
87
|
+
!ev.ctrl? && !ev.meta? &&
|
|
88
|
+
ch.between?(32, 126)
|
|
89
|
+
|
|
90
|
+
KeyEvent.new(
|
|
91
|
+
key: ev.key,
|
|
92
|
+
text: ch.chr,
|
|
93
|
+
ctrl: ev.ctrl?,
|
|
94
|
+
meta: ev.meta?,
|
|
95
|
+
shift: ev.shift?,
|
|
96
|
+
raw: ch,
|
|
97
|
+
)
|
|
98
|
+
else
|
|
99
|
+
ev
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
fallback_decode(ch)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def fallback_decode(ch, raw: nil)
|
|
107
|
+
case ch
|
|
108
|
+
when Integer
|
|
109
|
+
# Many Ruby curses builds return Integers for printable characters
|
|
110
|
+
# (e.g. 97 for "a"). Treat printable ASCII as self-inserting text.
|
|
111
|
+
if (32..126).cover?(ch)
|
|
112
|
+
fallback_decode(ch.chr, raw: ch)
|
|
113
|
+
elsif ch == 10 || ch == 13
|
|
114
|
+
# Enter can arrive as LF (10) or CR (13). Do this BEFORE ctrl-letter mapping.
|
|
115
|
+
KeyEvent.new(key: :enter, text: "\n", raw: ch)
|
|
116
|
+
elsif ch == 0
|
|
117
|
+
# Ctrl-@ => NUL
|
|
118
|
+
KeyEvent.new(key: :'@', ctrl: true, raw: ch)
|
|
119
|
+
elsif (1..26).cover?(ch)
|
|
120
|
+
KeyEvent.new(key: (ch + 96).chr.to_sym, ctrl: true, raw: ch)
|
|
121
|
+
elsif ch == 27
|
|
122
|
+
KeyEvent.new(key: :escape, raw: ch)
|
|
123
|
+
else
|
|
124
|
+
KeyEvent.new(key: ch, raw: ch)
|
|
125
|
+
end
|
|
126
|
+
when String
|
|
127
|
+
key =
|
|
128
|
+
case ch
|
|
129
|
+
when " " then :space
|
|
130
|
+
when "\t" then :tab
|
|
131
|
+
when "\n", "\r" then :enter
|
|
132
|
+
else
|
|
133
|
+
# bindable “literal” key, e.g. "h" => :h, "x" => :x, "." => :"."
|
|
134
|
+
ch.to_sym
|
|
135
|
+
end
|
|
136
|
+
KeyEvent.new(key: key, text: ch, raw: ch)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Add to the @map any keydefs defined by the user for the terminal
|
|
141
|
+
# detected in the environment. The user config, usually in
|
|
142
|
+
# ~/.config/fatty/keydefs.yml, takes into account the vagaries of how
|
|
143
|
+
# different terminal programs process keys.
|
|
144
|
+
def load_user_config
|
|
145
|
+
config = Fatty::Config.keydefs
|
|
146
|
+
Fatty.info("KeyDecode#load_user_config", config: config, tag: :keycode)
|
|
147
|
+
return unless config
|
|
148
|
+
|
|
149
|
+
terminal = @env[:terminal]
|
|
150
|
+
Fatty.info("KeyDecoder#load_user_config: detected terminal `#{terminal}`")
|
|
151
|
+
Fatty.info("KeyDecoder#load_user_config: only keydefs for `#{terminal}` will be loaded")
|
|
152
|
+
section = config.dig(:terminal, terminal.to_sym, :map)
|
|
153
|
+
return unless section
|
|
154
|
+
|
|
155
|
+
section.each do |code, spec|
|
|
156
|
+
@map[Integer(code.to_s)] = KeyEvent.new(**normalize_spec(spec))
|
|
157
|
+
Fatty.debug("KeyDecoder#load_user_config: user keydef: code: #{code} -> #{spec}")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Add to the @map keydefs as assumed by the curses library, defined in the
|
|
162
|
+
# constant Fatty::CUSRSES_TO_EVENT. These can be overridden by the the
|
|
163
|
+
# user config in #load_user_config.
|
|
164
|
+
def load_builtin_map
|
|
165
|
+
Fatty.info("#{self.class}#load_builtin_map from CURSES_TO_EVENT", tag: :keycode)
|
|
166
|
+
CURSES_TO_EVENT.each do |code, event|
|
|
167
|
+
@map[code] = event
|
|
168
|
+
Fatty.debug("KeyDecoder#load_builtin_map: system keydef: code: #{code} -> event: #{event}", tag: :keycode)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Make sure the user config Hash uses symbols for its keys. Return the
|
|
173
|
+
# Hash as so corrected.
|
|
174
|
+
def normalize_spec(hash)
|
|
175
|
+
spec = hash.transform_keys(&:to_sym)
|
|
176
|
+
if spec[:key].is_a?(String)
|
|
177
|
+
spec[:key] = spec[:key].to_sym
|
|
178
|
+
end
|
|
179
|
+
spec
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This is here to supress a particular warning that is emitted by Curses mouse
|
|
4
|
+
# code.
|
|
5
|
+
module Warning
|
|
6
|
+
class << self
|
|
7
|
+
alias_method :fatty_orig_warn, :warn unless method_defined?(:fatty_orig_warn)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.warn(msg, category: nil, **kwargs)
|
|
11
|
+
return if msg.include?("undefining the allocator of T_DATA class Curses::MouseEvent")
|
|
12
|
+
|
|
13
|
+
fatty_orig_warn(msg, category: category, **kwargs)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Curses button-related constants:
|
|
18
|
+
#
|
|
19
|
+
# * Button1 (Left button)
|
|
20
|
+
# BUTTON1_PRESSED 0x00000002
|
|
21
|
+
# BUTTON1_RELEASED 0x00000001
|
|
22
|
+
# BUTTON1_CLICKED 0x00000004
|
|
23
|
+
# BUTTON1_DOUBLE_CLICKED 0x00000008
|
|
24
|
+
# BUTTON1_TRIPLE_CLICKED 0x00000010
|
|
25
|
+
#
|
|
26
|
+
# * Button2 (Middle button)
|
|
27
|
+
# BUTTON2_PRESSED 0x00000080
|
|
28
|
+
# BUTTON2_RELEASED 0x00000040
|
|
29
|
+
# BUTTON2_CLICKED 0x00000100
|
|
30
|
+
# BUTTON2_DOUBLE_CLICKED 0x00000200
|
|
31
|
+
# BUTTON2_TRIPLE_CLICKED 0x00000400
|
|
32
|
+
#
|
|
33
|
+
# * Button3 (Right button)
|
|
34
|
+
# BUTTON3_PRESSED 0x00002000
|
|
35
|
+
# BUTTON3_RELEASED 0x00001000
|
|
36
|
+
# BUTTON3_CLICKED 0x00004000
|
|
37
|
+
# BUTTON3_DOUBLE_CLICKED 0x00008000
|
|
38
|
+
# BUTTON3_TRIPLE_CLICKED 0x00010000
|
|
39
|
+
#
|
|
40
|
+
# * Button4 (wheel up)
|
|
41
|
+
# BUTTON4_PRESSED 0x00020000
|
|
42
|
+
# BUTTON4_RELEASED 0x00040000
|
|
43
|
+
# BUTTON4_CLICKED 0x00080000
|
|
44
|
+
# BUTTON4_DOUBLE_CLICKED 0x00100000
|
|
45
|
+
# BUTTON4_TRIPLE_CLICKED 0x00200000
|
|
46
|
+
#
|
|
47
|
+
# * Button5 (wheel down)
|
|
48
|
+
# BUTTON5_PRESSED 0x00200000
|
|
49
|
+
# BUTTON5_RELEASED 0x00400000
|
|
50
|
+
# BUTTON5_CLICKED 0x00800000
|
|
51
|
+
# BUTTON5_DOUBLE_CLICKED 0x01000000
|
|
52
|
+
# BUTTON5_TRIPLE_CLICKED 0x02000000
|
|
53
|
+
#
|
|
54
|
+
# * Modifier Flags
|
|
55
|
+
# BUTTON_SHIFT 0x04000000
|
|
56
|
+
# BUTTON_CTRL 0x08000000
|
|
57
|
+
# BUTTON_ALT 0x10000000
|
|
58
|
+
#
|
|
59
|
+
# The ruby implementation of curses apparently does not expose some
|
|
60
|
+
# mouse-related constants. Here we provide a method to ensure that they are
|
|
61
|
+
# defined.
|
|
62
|
+
module Fatty
|
|
63
|
+
module Curses
|
|
64
|
+
module MouseConstants
|
|
65
|
+
CONSTANTS = {
|
|
66
|
+
# Button 1 (left)
|
|
67
|
+
BUTTON1_RELEASED: 0x00000001,
|
|
68
|
+
BUTTON1_PRESSED: 0x00000002,
|
|
69
|
+
BUTTON1_CLICKED: 0x00000004,
|
|
70
|
+
BUTTON1_DOUBLE_CLICKED: 0x00000008,
|
|
71
|
+
BUTTON1_TRIPLE_CLICKED: 0x00000010,
|
|
72
|
+
|
|
73
|
+
# Button 2 (middle)
|
|
74
|
+
BUTTON2_RELEASED: 0x00000040,
|
|
75
|
+
BUTTON2_PRESSED: 0x00000080,
|
|
76
|
+
BUTTON2_CLICKED: 0x00000100,
|
|
77
|
+
BUTTON2_DOUBLE_CLICKED: 0x00000200,
|
|
78
|
+
BUTTON2_TRIPLE_CLICKED: 0x00000400,
|
|
79
|
+
|
|
80
|
+
# Button 3 (right)
|
|
81
|
+
BUTTON3_RELEASED: 0x00001000,
|
|
82
|
+
BUTTON3_PRESSED: 0x00002000,
|
|
83
|
+
BUTTON3_CLICKED: 0x00004000,
|
|
84
|
+
BUTTON3_DOUBLE_CLICKED: 0x00008000,
|
|
85
|
+
BUTTON3_TRIPLE_CLICKED: 0x00010000,
|
|
86
|
+
|
|
87
|
+
# Wheel up
|
|
88
|
+
BUTTON4_PRESSED: 0x00020000,
|
|
89
|
+
BUTTON4_RELEASED: 0x00040000,
|
|
90
|
+
BUTTON4_CLICKED: 0x00080000,
|
|
91
|
+
BUTTON4_DOUBLE_CLICKED: 0x00100000,
|
|
92
|
+
BUTTON4_TRIPLE_CLICKED: 0x00200000,
|
|
93
|
+
|
|
94
|
+
# Wheel down
|
|
95
|
+
BUTTON5_PRESSED: 0x00200000,
|
|
96
|
+
BUTTON5_RELEASED: 0x00400000,
|
|
97
|
+
BUTTON5_CLICKED: 0x00800000,
|
|
98
|
+
BUTTON5_DOUBLE_CLICKED: 0x01000000,
|
|
99
|
+
BUTTON5_TRIPLE_CLICKED: 0x02000000,
|
|
100
|
+
|
|
101
|
+
# Modifier masks
|
|
102
|
+
BUTTON_SHIFT: 0x04000000,
|
|
103
|
+
BUTTON_CTRL: 0x08000000,
|
|
104
|
+
BUTTON_ALT: 0x10000000,
|
|
105
|
+
}.freeze
|
|
106
|
+
|
|
107
|
+
def self.ensure!
|
|
108
|
+
CONSTANTS.each do |name, value|
|
|
109
|
+
next if ::Curses.const_defined?(name)
|
|
110
|
+
|
|
111
|
+
::Curses.const_set(name, value)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
module Curses
|
|
5
|
+
module WindowStyling
|
|
6
|
+
ATTR_FLAGS = {
|
|
7
|
+
bold: ::Curses::A_BOLD,
|
|
8
|
+
dim: ::Curses::A_DIM,
|
|
9
|
+
reverse: ::Curses::A_REVERSE,
|
|
10
|
+
underline: ::Curses::A_UNDERLINE,
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def pair_attr(role, fallback:)
|
|
14
|
+
palette = context.palette
|
|
15
|
+
spec = palette&.[](role)
|
|
16
|
+
return fallback unless spec && spec[:pair]
|
|
17
|
+
|
|
18
|
+
attr = ::Curses.color_pair(spec[:pair])
|
|
19
|
+
|
|
20
|
+
Array(spec[:attrs]).each do |a|
|
|
21
|
+
attr |= attr_flag(a)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def attr_flag(name)
|
|
28
|
+
ATTR_FLAGS.fetch(name.to_sym, 0)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/fatty/curses.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Curses
|
|
4
|
+
class Window
|
|
5
|
+
def origin
|
|
6
|
+
[begy, begx]
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative "curses/patch"
|
|
12
|
+
require_relative "curses/curses_coder"
|
|
13
|
+
require_relative "curses/key_decoder"
|
|
14
|
+
require_relative "curses/event_source"
|
|
15
|
+
require_relative "curses/context"
|
|
16
|
+
require_relative "curses/window_styling"
|
data/lib/fatty/env.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
module Env
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def detect
|
|
8
|
+
{
|
|
9
|
+
os: os,
|
|
10
|
+
arch: arch,
|
|
11
|
+
ruby_platform: RUBY_PLATFORM,
|
|
12
|
+
term: ENV["TERM"],
|
|
13
|
+
terminal: detect_terminal_program,
|
|
14
|
+
terminal_version: ENV["KONSOLE_VERSION"] || ENV["TERM_PROGRAM_VERSION"],
|
|
15
|
+
tmux: tmux?,
|
|
16
|
+
screen: screen?,
|
|
17
|
+
ssh: ssh?,
|
|
18
|
+
curses: curses_info,
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# -----------------------
|
|
23
|
+
# OS / platform
|
|
24
|
+
# -----------------------
|
|
25
|
+
|
|
26
|
+
def os
|
|
27
|
+
case RUBY_PLATFORM
|
|
28
|
+
when /linux/ then :linux
|
|
29
|
+
when /darwin/ then :darwin
|
|
30
|
+
when /bsd/ then :bsd
|
|
31
|
+
when /mswin|mingw|cygwin/ then :windows
|
|
32
|
+
else :unknown
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def arch
|
|
37
|
+
case RUBY_PLATFORM
|
|
38
|
+
when /x86_64/ then :x86_64
|
|
39
|
+
when /arm64|aarch64/ then :arm64
|
|
40
|
+
else :unknown
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# -----------------------
|
|
45
|
+
# Terminal identity
|
|
46
|
+
# -----------------------
|
|
47
|
+
|
|
48
|
+
def term_program
|
|
49
|
+
ENV["TERM_PROGRAM"]&.downcase ||
|
|
50
|
+
ENV["COLORTERM"]&.downcase
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def detect_terminal_program
|
|
54
|
+
return "tmux" if ENV.key?("TMUX")
|
|
55
|
+
return "screen" if ENV.key?("STY")
|
|
56
|
+
return "konsole" if ENV.key?("KONSOLE_VERSION")
|
|
57
|
+
return "kitty" if ENV.key?("KITTY_WINDOW_ID")
|
|
58
|
+
return "wezterm" if ENV["TERM_PROGRAM"]&.downcase == "wezterm"
|
|
59
|
+
return "iterm" if ENV["TERM_PROGRAM"] == "iTerm.app"
|
|
60
|
+
return "ghostty" if ENV["TERM_PROGRAM"]&.downcase == "ghostty"
|
|
61
|
+
return "alacritty" if ENV.key?("ALACRITTY_LOG")
|
|
62
|
+
return "terminator" if ENV.key?("TERMINATOR_UUID")
|
|
63
|
+
|
|
64
|
+
if ENV.key?("TERM_PROGRAM")
|
|
65
|
+
ENV["TERM_PROGRAM"]&.downcase
|
|
66
|
+
elsif ENV.key?("TERM")
|
|
67
|
+
ENV["TERM"]&.downcase
|
|
68
|
+
else
|
|
69
|
+
'unknown'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def tmux?
|
|
74
|
+
ENV.key?("TMUX")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def screen?
|
|
78
|
+
ENV.key?("STY")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def ssh?
|
|
82
|
+
ENV.key?("SSH_TTY") || ENV.key?("SSH_CONNECTION")
|
|
83
|
+
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
|
+
end
|
|
100
|
+
end
|