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
|
@@ -3,21 +3,19 @@
|
|
|
3
3
|
require "open3"
|
|
4
4
|
|
|
5
5
|
module Fatty
|
|
6
|
-
class ShellSession <
|
|
6
|
+
class ShellSession < Session
|
|
7
7
|
action_on :session
|
|
8
8
|
|
|
9
|
-
attr_reader :field, :history
|
|
9
|
+
attr_reader :field, :history, :output_session, :on_accept
|
|
10
10
|
|
|
11
|
-
def initialize(prompt: "sh> ", on_accept: nil, completion_proc: nil, history_ctx: nil, history_path: :default)
|
|
11
|
+
def initialize(id: nil, prompt: "sh> ", on_accept: nil, completion_proc: nil, history_ctx: nil, history_path: :default)
|
|
12
|
+
keymap = Keymaps.emacs
|
|
12
13
|
super(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Fatty::OutputView.new(z: 0),
|
|
16
|
-
Fatty::InputView.new(z: 10),
|
|
17
|
-
Fatty::CursorView.new(z: 100),
|
|
18
|
-
]
|
|
14
|
+
id: :shell,
|
|
15
|
+
keymap: keymap,
|
|
19
16
|
)
|
|
20
17
|
@history = Fatty::History.for_path(history_path)
|
|
18
|
+
@output_session = OutputSession.new(keymap: keymap, history: @history)
|
|
21
19
|
@field = Fatty::InputField.new(
|
|
22
20
|
prompt: prompt,
|
|
23
21
|
history: @history,
|
|
@@ -25,165 +23,170 @@ module Fatty
|
|
|
25
23
|
history_kind: :command,
|
|
26
24
|
history_ctx: history_ctx,
|
|
27
25
|
)
|
|
28
|
-
|
|
29
26
|
@on_accept = on_accept
|
|
30
27
|
@completion_proc = completion_proc
|
|
28
|
+
@completion_range = nil
|
|
29
|
+
@completion_path = nil
|
|
30
|
+
@completion_popup_prefix = nil
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
#########################################################################################
|
|
34
|
-
#
|
|
34
|
+
# Session Protocol
|
|
35
35
|
#########################################################################################
|
|
36
36
|
|
|
37
37
|
def init(terminal:)
|
|
38
38
|
super
|
|
39
|
-
|
|
40
|
-
[
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
output_session.init(terminal: terminal)
|
|
40
|
+
[
|
|
41
|
+
Command.terminal(:register_session, session: output_session),
|
|
42
|
+
Command.session(output_session.id, :resize),
|
|
43
|
+
]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def update(command)
|
|
47
|
+
log_update(command)
|
|
48
|
+
commands =
|
|
49
|
+
case command.action
|
|
50
|
+
when :terminal_paste
|
|
51
|
+
field.act_on(:paste, command.payload.fetch(:text, ""), env: action_env(event: nil))
|
|
52
|
+
[]
|
|
53
|
+
when :popup_result
|
|
54
|
+
apply_popup_result(command.payload)
|
|
55
|
+
when :paste
|
|
56
|
+
text = command.payload.fetch(:text, "").to_s
|
|
57
|
+
env = action_env(event: nil)
|
|
58
|
+
field.act_on(:paste, text, env: env)
|
|
59
|
+
[]
|
|
60
|
+
when :popup_tab
|
|
61
|
+
apply_completion_popup_tab(command.payload)
|
|
62
|
+
when :popup_backtab
|
|
63
|
+
apply_completion_popup_backtab(command.payload)
|
|
64
|
+
when :key
|
|
65
|
+
ev = command.payload.fetch(:event)
|
|
66
|
+
if output_session.pager_active?
|
|
67
|
+
Command.session(output_session.id, :key, event: ev)
|
|
68
|
+
elsif ev.is_a?(Fatty::KeyEvent)
|
|
69
|
+
action, args = resolve_action(ev)
|
|
70
|
+
if action
|
|
71
|
+
apply_action(action, args, event: ev)
|
|
72
|
+
else
|
|
73
|
+
case ev.key
|
|
74
|
+
when :enter, :return
|
|
75
|
+
apply_action(:submit_line, [], event: ev)
|
|
76
|
+
else
|
|
77
|
+
unrecognized_key_alert(ev)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
[]
|
|
85
|
+
end
|
|
86
|
+
Array(commands)
|
|
61
87
|
end
|
|
62
88
|
|
|
63
|
-
def view
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
row: screen.output_rect.rows - 1,
|
|
73
|
-
role: :pager_status,
|
|
74
|
-
)
|
|
89
|
+
def view
|
|
90
|
+
Fatty.debug(
|
|
91
|
+
"ShellSession#view output_session=" \
|
|
92
|
+
"#{output_session.class}:#{output_session.object_id}",
|
|
93
|
+
tag: :render,
|
|
94
|
+
)
|
|
95
|
+
output_session.view
|
|
96
|
+
if input_suppressed?
|
|
97
|
+
renderer.clear_input_field
|
|
75
98
|
else
|
|
76
|
-
::Curses.curs_set(1)
|
|
77
|
-
renderer.render_output(output, viewport: pager_viewport, highlights: nil)
|
|
78
99
|
renderer.render_input_field(field)
|
|
79
|
-
renderer.restore_cursor(field)
|
|
80
100
|
end
|
|
81
101
|
end
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
vp = pager_viewport.dup
|
|
85
|
-
vp.height = [screen.output_rect.rows - 1, 1].max
|
|
86
|
-
vp
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Save any state we want saved on quit, error, etc.
|
|
90
|
-
def persist!
|
|
91
|
-
return unless @history.respond_to?(:save!)
|
|
92
|
-
|
|
93
|
-
Fatty.debug("ShellSession#persist!: saving history", tag: :history)
|
|
94
|
-
@history.save!
|
|
95
|
-
rescue => e
|
|
96
|
-
Fatty.error("ShellSession#persist!: failed to save history: #{e.class}: #{e.message}", tag: :history)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Called by Terminal#go on every loop iteration.
|
|
100
|
-
# Returns true if any visible state changed (dirty).
|
|
101
|
-
def tick
|
|
102
|
-
dirty = false
|
|
103
|
-
# Animated autoscroll (e.g. after M-s in paging mode).
|
|
104
|
-
if pager.autoscroll?
|
|
105
|
-
step = [(viewport.height * 3) / 4, 1].max
|
|
106
|
-
dirty ||= pager.autoscroll_step?(max_lines: step)
|
|
107
|
-
end
|
|
108
|
-
dirty
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
############################################################################################
|
|
103
|
+
#########################################################################################
|
|
112
104
|
# Actions
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def action_env(event:)
|
|
116
|
-
Fatty::ActionEnvironment.new(
|
|
117
|
-
session: self,
|
|
118
|
-
counter: counter,
|
|
119
|
-
event: event,
|
|
120
|
-
buffer: @field.buffer,
|
|
121
|
-
field: @field,
|
|
122
|
-
pager: pager,
|
|
123
|
-
)
|
|
124
|
-
end
|
|
105
|
+
#########################################################################################
|
|
125
106
|
|
|
126
|
-
desc "Pass the current
|
|
107
|
+
desc "Pass the current input line to the on_accept proc or a shell"
|
|
127
108
|
action :submit_line do
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# Perform the on_accept action if defined, but catch a few special
|
|
132
|
-
# commands for quiting and clearing.
|
|
133
|
-
def submit_line
|
|
134
|
-
line = @field.accept_line.to_s.strip
|
|
135
|
-
return [] if line.empty?
|
|
109
|
+
line = @field.accept_line.to_s
|
|
110
|
+
return [] if line.blank? && on_accept.nil?
|
|
136
111
|
|
|
137
112
|
Fatty.info("ShellSession: accept_line: #{line}")
|
|
138
|
-
|
|
139
113
|
case line
|
|
140
114
|
when "exit", "quit"
|
|
141
|
-
|
|
115
|
+
Command.terminal(:quit)
|
|
142
116
|
when "clear"
|
|
143
|
-
|
|
144
|
-
|
|
117
|
+
[
|
|
118
|
+
Command.session(output_session.id, :clear),
|
|
119
|
+
Command.session(:alert, :clear),
|
|
120
|
+
]
|
|
145
121
|
else
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
122
|
+
commands = []
|
|
123
|
+
env = accept_env
|
|
124
|
+
terminal.apply_command(Command.session(output_session.id, :begin_command))
|
|
125
|
+
result = nil
|
|
126
|
+
interrupted = false
|
|
127
|
+
|
|
128
|
+
begin
|
|
129
|
+
result =
|
|
130
|
+
if @on_accept
|
|
131
|
+
@on_accept.call(line, env)
|
|
132
|
+
else
|
|
133
|
+
run_default_command(line)
|
|
134
|
+
end
|
|
135
|
+
rescue Fatty::Interrupt
|
|
136
|
+
interrupted = true
|
|
137
|
+
end
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
139
|
+
if interrupted
|
|
140
|
+
finish_interrupted_command(env)
|
|
141
|
+
else
|
|
142
|
+
commands.concat(env.commands)
|
|
143
|
+
|
|
144
|
+
if result.is_a?(String)
|
|
145
|
+
commands << Command.session(
|
|
146
|
+
output_session.id,
|
|
147
|
+
:append,
|
|
148
|
+
text: result,
|
|
149
|
+
follow: true,
|
|
150
|
+
)
|
|
151
|
+
elsif result.is_a?(Fatty::Command)
|
|
152
|
+
commands << result
|
|
153
|
+
elsif result.is_a?(Array)
|
|
154
|
+
commands.concat(result.grep(Fatty::Command))
|
|
155
|
+
elsif result
|
|
156
|
+
commands << Command.session(
|
|
157
|
+
output_session.id,
|
|
158
|
+
:append,
|
|
159
|
+
text: result.to_s,
|
|
160
|
+
follow: true,
|
|
161
|
+
)
|
|
155
162
|
end
|
|
156
|
-
|
|
163
|
+
|
|
164
|
+
commands << Command.session(output_session.id, :finish_command)
|
|
165
|
+
commands
|
|
166
|
+
end
|
|
157
167
|
end
|
|
158
|
-
rescue Errno::ENOENT
|
|
159
|
-
[[:send, :alert, :show, { level: :error, message: "Command not found (#{line})" }]]
|
|
160
168
|
end
|
|
161
169
|
|
|
162
170
|
desc "Accept the current shell input line and switch output to scrolling"
|
|
163
171
|
action :submit_and_scroll do
|
|
164
|
-
|
|
165
|
-
cmds = submit_line
|
|
166
|
-
pager.paging_to_scrolling if output.lines.length > before
|
|
167
|
-
cmds
|
|
172
|
+
submit_line << Command.session(output_session.id, :paging_to_scrolling)
|
|
168
173
|
end
|
|
169
174
|
|
|
170
175
|
desc "Interrupt scrolling, otherwise quit Fatty"
|
|
171
176
|
action :interrupt do
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
[]
|
|
177
|
+
if output_session.pager_active?
|
|
178
|
+
Command.session(output_session.id, :quit_paging)
|
|
175
179
|
else
|
|
176
|
-
|
|
180
|
+
Command.terminal(:quit)
|
|
177
181
|
end
|
|
178
182
|
end
|
|
179
183
|
|
|
180
184
|
desc "Quit if input is empty, otherwise delete forward"
|
|
181
185
|
action :interrupt_if_empty do
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
[]
|
|
186
|
+
if output_session.pager_active?
|
|
187
|
+
Command.session(output_session.id, :quit_paging)
|
|
185
188
|
elsif @field.empty?
|
|
186
|
-
|
|
189
|
+
Command.terminal(:quit)
|
|
187
190
|
else
|
|
188
191
|
@field.act_on(:delete_char_forward, env: action_env(event: nil))
|
|
189
192
|
[]
|
|
@@ -192,147 +195,248 @@ module Fatty
|
|
|
192
195
|
|
|
193
196
|
desc "Clear shell output"
|
|
194
197
|
action :clear_output do
|
|
195
|
-
|
|
196
|
-
[]
|
|
198
|
+
Command.session(output_session.id, :clear)
|
|
197
199
|
end
|
|
198
200
|
|
|
199
|
-
desc "
|
|
200
|
-
action :
|
|
201
|
-
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
desc "Choose a theme from a popup"
|
|
205
|
-
action :choose_theme do
|
|
206
|
-
current = Fatty::Themes::Manager.current
|
|
207
|
-
names = Fatty::Themes::Manager.theme_names
|
|
208
|
-
ordered = [current] + (names - [current])
|
|
209
|
-
@theme_popup_restore = current
|
|
210
|
-
|
|
211
|
-
popup = Fatty::PopUpSession.new(
|
|
212
|
-
source: ordered,
|
|
213
|
-
kind: :theme_chooser,
|
|
214
|
-
title: "Themes",
|
|
215
|
-
prompt: "Theme: ",
|
|
216
|
-
selection: :top,
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
[[:terminal, :push_modal, popup]]
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
desc "Add a digit to the current numeric prefix"
|
|
223
|
-
action :prefix_digit do |digit|
|
|
224
|
-
counter.push_digit(digit)
|
|
201
|
+
desc "Complete the current input prefix"
|
|
202
|
+
action :complete do
|
|
203
|
+
@field.complete!
|
|
225
204
|
[]
|
|
226
205
|
end
|
|
227
206
|
|
|
228
|
-
desc "Complete the
|
|
229
|
-
action :
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
[]
|
|
233
|
-
end
|
|
207
|
+
desc "Complete the previous input prefix"
|
|
208
|
+
action :complete_previous do
|
|
209
|
+
@field.complete!(direction: -1)
|
|
210
|
+
[]
|
|
234
211
|
end
|
|
235
212
|
|
|
236
213
|
desc "Open completion popup"
|
|
237
214
|
action :completion_popup do
|
|
238
215
|
with_virtual_suffix_sync do
|
|
239
216
|
candidates = @field.popup_completion_candidates
|
|
217
|
+
completion_path = @field.popup_completion_path?
|
|
240
218
|
|
|
241
219
|
if candidates.empty?
|
|
242
220
|
[]
|
|
243
221
|
elsif candidates.length == 1
|
|
244
|
-
apply_completion(
|
|
222
|
+
apply_completion(
|
|
223
|
+
candidates.first,
|
|
224
|
+
range: @field.popup_completion_range,
|
|
225
|
+
append_space: !completion_path,
|
|
226
|
+
)
|
|
245
227
|
else
|
|
246
|
-
@completion_range = @field.popup_completion_range
|
|
247
228
|
# NOTE: we don't pass a matcher so that the default_matcher is used.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
229
|
+
@completion_range = @field.popup_completion_range
|
|
230
|
+
@completion_path = completion_path
|
|
231
|
+
@completion_popup_prefix = completion_path ? @field.popup_completion_query.to_s : nil
|
|
232
|
+
|
|
233
|
+
completion =
|
|
234
|
+
Fatty::PopUpSession.new(
|
|
235
|
+
source: candidates,
|
|
236
|
+
kind: :completion,
|
|
237
|
+
title: completion_popup_title(@completion_popup_prefix),
|
|
238
|
+
prompt: "Complete: ",
|
|
239
|
+
order: :as_given,
|
|
240
|
+
current: :top,
|
|
241
|
+
initial_query: @field.popup_completion_query.to_s,
|
|
242
|
+
history: @history,
|
|
243
|
+
history_kind: :popup_filter,
|
|
244
|
+
history_ctx: {
|
|
245
|
+
kind: :popup_filter,
|
|
246
|
+
popup: :completion,
|
|
247
|
+
prompt: "Complete: ",
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
Command.terminal(:push_modal, session: completion)
|
|
258
252
|
end
|
|
259
253
|
end
|
|
260
254
|
end
|
|
261
255
|
|
|
262
|
-
desc "
|
|
263
|
-
action :
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
256
|
+
desc "Open command history search"
|
|
257
|
+
action :history_search do
|
|
258
|
+
src = ->(_q = nil) { @history.entries.select(&:command?).last(500).map(&:text) }
|
|
259
|
+
hist_searcher =
|
|
260
|
+
Fatty::PopUpSession.new(
|
|
261
|
+
source: src,
|
|
262
|
+
kind: :history_search,
|
|
263
|
+
title: "History",
|
|
264
|
+
prompt: "Narrow: ",
|
|
265
|
+
order: :as_given,
|
|
266
|
+
current: :bottom,
|
|
267
|
+
initial_query: @field.buffer.text,
|
|
268
|
+
history: @history,
|
|
269
|
+
history_kind: :popup_filter,
|
|
270
|
+
history_ctx: {
|
|
271
|
+
kind: :popup_filter,
|
|
272
|
+
popup: :history_search,
|
|
273
|
+
prompt: "Narrow: ",
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
Command.terminal(:push_modal, session: hist_searcher)
|
|
270
277
|
end
|
|
271
278
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
#########################################################################################
|
|
280
|
+
# Other public API methods
|
|
281
|
+
#########################################################################################
|
|
282
|
+
|
|
283
|
+
# Save any state we want saved on quit, error, etc.
|
|
284
|
+
def persist!
|
|
285
|
+
return unless @history.respond_to?(:save!)
|
|
286
|
+
|
|
287
|
+
Fatty.debug("ShellSession#persist!: saving history", tag: :history)
|
|
288
|
+
@history.save!
|
|
289
|
+
rescue => e
|
|
290
|
+
Fatty.error("ShellSession#persist!: failed to save history: #{e.class}: #{e.message}", tag: :history)
|
|
280
291
|
end
|
|
281
292
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
:push_modal,
|
|
287
|
-
Fatty::ISearchSession.new(direction: :forward, history: @history, last_pattern: pager.search_pattern)
|
|
288
|
-
]]
|
|
293
|
+
# Called by Terminal#go on every loop iteration. Returns true if any
|
|
294
|
+
# visible state changed (dirty).
|
|
295
|
+
def tick
|
|
296
|
+
output_session.tick
|
|
289
297
|
end
|
|
290
298
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
[[
|
|
294
|
-
:terminal,
|
|
295
|
-
:push_modal,
|
|
296
|
-
Fatty::ISearchSession.new(direction: :backward, history: @history, last_pattern: pager.search_pattern)
|
|
297
|
-
]]
|
|
299
|
+
def pager_active?
|
|
300
|
+
output_session.pager_active?
|
|
298
301
|
end
|
|
299
302
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
handle_search_result(pager.search_repeat_next!)
|
|
303
|
+
def cursor_visible?
|
|
304
|
+
!input_suppressed?
|
|
303
305
|
end
|
|
304
306
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
307
|
+
private
|
|
308
|
+
|
|
309
|
+
# simplecov:disable
|
|
310
|
+
|
|
311
|
+
def input_suppressed?
|
|
312
|
+
terminal.modal_active? || output_session.pager_active?
|
|
308
313
|
end
|
|
309
314
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
315
|
+
def keymap_contexts
|
|
316
|
+
[:input, :text, :terminal]
|
|
317
|
+
end
|
|
313
318
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
def apply_action(action, args, event:)
|
|
320
|
+
which =
|
|
321
|
+
if event&.respond_to?(:key)
|
|
322
|
+
event.key.inspect
|
|
323
|
+
elsif event&.respond_to?(:mouse)
|
|
324
|
+
event.mouse.inspect
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
Fatty.debug("ShellSession#apply_action: #{which}", tag: :keymap)
|
|
328
|
+
|
|
329
|
+
unless completion_action?(action)
|
|
330
|
+
@field.reset_completion_state!
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
env = action_env(event: event)
|
|
334
|
+
defn = Fatty::Actions.lookup(action)
|
|
335
|
+
result = Fatty::Actions.call(action, env, *args)
|
|
336
|
+
|
|
337
|
+
@field.sync_virtual_suffix! if defn && [:field, :buffer].include?(defn[:on])
|
|
338
|
+
normalize_action_result(result)
|
|
339
|
+
rescue Fatty::ActionError => e
|
|
340
|
+
[alert_cmd(:error, e.message, ev: event)]
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def apply_popup_result(payload)
|
|
344
|
+
commands = []
|
|
345
|
+
case payload[:kind]&.to_sym
|
|
346
|
+
when :history_search
|
|
347
|
+
item = payload.fetch(:item, "").to_s
|
|
348
|
+
env = action_env(event: nil)
|
|
349
|
+
with_virtual_suffix_sync do
|
|
350
|
+
Fatty::Actions.call(:replace, env, item)
|
|
351
|
+
end
|
|
352
|
+
when :completion
|
|
353
|
+
apply_completion(
|
|
354
|
+
payload.fetch(:item, "").to_s,
|
|
355
|
+
range: @completion_range,
|
|
356
|
+
append_space: !@completion_path,
|
|
357
|
+
)
|
|
358
|
+
@completion_range = nil
|
|
359
|
+
@completion_path = nil
|
|
360
|
+
@completion_popup_prefix = nil
|
|
361
|
+
end
|
|
362
|
+
commands
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def action_env(event:)
|
|
366
|
+
Fatty::ActionEnvironment.new(
|
|
367
|
+
session: self,
|
|
368
|
+
counter: counter,
|
|
369
|
+
event: event,
|
|
370
|
+
buffer: @field.buffer,
|
|
371
|
+
field: @field,
|
|
372
|
+
pager: pager,
|
|
322
373
|
)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
#########################################################################################
|
|
377
|
+
# Utilities
|
|
378
|
+
#########################################################################################
|
|
379
|
+
|
|
380
|
+
def pager
|
|
381
|
+
output_session.pager
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def accept_env
|
|
385
|
+
CallbackEnvironment.new(terminal: terminal, output_id: output_session.id)
|
|
386
|
+
end
|
|
323
387
|
|
|
324
|
-
|
|
388
|
+
def run_default_command(line)
|
|
389
|
+
commands = [Command.session(output_session.id, :append, text: "$ #{line}\n", follow: true)]
|
|
390
|
+
out, status = Open3.capture2e(line)
|
|
391
|
+
if status&.exitstatus && status.exitstatus != 0
|
|
392
|
+
out << "\n[exit #{status.exitstatus}]\n"
|
|
393
|
+
end
|
|
394
|
+
commands << Command.session(output_session.id, :append, text: out, follow: true)
|
|
395
|
+
rescue Errno::ENOENT => ex
|
|
396
|
+
commands << Command.session(output_session.id, :append, text: ex.to_s, follow: true)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def unrecognized_key_alert(ev)
|
|
400
|
+
if ev.uncoded?
|
|
401
|
+
alert_cmd(
|
|
402
|
+
:error,
|
|
403
|
+
"Undefined key: #{ev.key} " \
|
|
404
|
+
"(edit config in 'keydefs.yml' to name)",
|
|
405
|
+
ev: ev,
|
|
406
|
+
)
|
|
407
|
+
else
|
|
408
|
+
alert_cmd(
|
|
409
|
+
:warn,
|
|
410
|
+
"Unbound key: #{ev} " \
|
|
411
|
+
"(edit config in 'keybindings.yml' to bind)",
|
|
412
|
+
ev: ev,
|
|
413
|
+
)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def finish_interrupted_command(env)
|
|
418
|
+
env.commands.each do |command|
|
|
419
|
+
terminal.apply_command(command)
|
|
420
|
+
end
|
|
421
|
+
terminal.apply_command(Command.session(output_session.id, :finish_command))
|
|
422
|
+
terminal.apply_command(Command.session(output_session.id, :quit_paging))
|
|
423
|
+
terminal.render_frame
|
|
424
|
+
[]
|
|
325
425
|
end
|
|
326
426
|
|
|
327
427
|
#########################################################################################
|
|
328
428
|
# Completion
|
|
329
429
|
#########################################################################################
|
|
330
430
|
|
|
431
|
+
def completion_action?(action)
|
|
432
|
+
[:complete, :complete_previous].include?(action.to_sym)
|
|
433
|
+
end
|
|
434
|
+
|
|
331
435
|
def completion_candidates
|
|
332
436
|
path_candidates = @field.path_completion_candidates
|
|
333
437
|
return path_candidates if path_candidates.any?
|
|
334
438
|
|
|
335
|
-
return [] unless @
|
|
439
|
+
return [] unless @completion_pr
|
|
336
440
|
|
|
337
441
|
prefix = completion_prefix
|
|
338
442
|
Array(@completion_proc.call(@field.buffer))
|
|
@@ -381,7 +485,25 @@ module Fatty
|
|
|
381
485
|
commands
|
|
382
486
|
end
|
|
383
487
|
|
|
384
|
-
def
|
|
488
|
+
def completion_path_item?(item)
|
|
489
|
+
item.to_s.end_with?("/")
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def parent_completion_path(path)
|
|
493
|
+
s = path.to_s
|
|
494
|
+
return if s.empty?
|
|
495
|
+
|
|
496
|
+
s = s[0...-1] if s.end_with?("/") && s.length > 1
|
|
497
|
+
slash = s.rindex("/")
|
|
498
|
+
return unless slash
|
|
499
|
+
|
|
500
|
+
parent = s[0..slash]
|
|
501
|
+
return if parent == path
|
|
502
|
+
|
|
503
|
+
parent
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def apply_completion(candidate, range: nil, append_space: true)
|
|
385
507
|
candidate = candidate.to_s
|
|
386
508
|
return [] if candidate.empty?
|
|
387
509
|
|
|
@@ -389,153 +511,63 @@ module Fatty
|
|
|
389
511
|
target = range || buffer.completion_replace_range
|
|
390
512
|
old_text = buffer.text.dup
|
|
391
513
|
old_end = target.end
|
|
392
|
-
|
|
514
|
+
|
|
515
|
+
should_append_space =
|
|
516
|
+
append_space &&
|
|
393
517
|
!candidate.match?(/\s\z/) &&
|
|
394
518
|
(old_end >= old_text.length || old_text[old_end]&.match?(/\s/))
|
|
395
|
-
|
|
519
|
+
|
|
520
|
+
inserted = should_append_space ? "#{candidate} " : candidate
|
|
396
521
|
buffer.replace_range(target, inserted)
|
|
522
|
+
buffer.virtual_suffix = ""
|
|
397
523
|
[]
|
|
398
524
|
end
|
|
399
525
|
|
|
400
|
-
|
|
526
|
+
def apply_completion_popup_tab(payload)
|
|
527
|
+
return [] unless @completion_path
|
|
401
528
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
529
|
+
popup = payload[:session]
|
|
530
|
+
item = payload[:item].to_s
|
|
531
|
+
return [] unless popup
|
|
532
|
+
return [] unless completion_path_item?(item)
|
|
405
533
|
|
|
406
|
-
|
|
407
|
-
if commands.is_a?(Array) && commands.first.is_a?(Array) && commands.first.first.is_a?(Symbol)
|
|
408
|
-
commands
|
|
409
|
-
else
|
|
410
|
-
[[:send, :alert, :clear, {}]]
|
|
411
|
-
end
|
|
534
|
+
replace_completion_popup_path(popup, item)
|
|
412
535
|
end
|
|
413
536
|
|
|
414
|
-
def
|
|
415
|
-
|
|
416
|
-
case name
|
|
417
|
-
when :popup_result
|
|
418
|
-
case payload[:kind]&.to_sym
|
|
419
|
-
when :history_search
|
|
420
|
-
item = payload.fetch(:item, "").to_s
|
|
421
|
-
env = action_env(event: nil)
|
|
422
|
-
with_virtual_suffix_sync do
|
|
423
|
-
Fatty::Actions.call(:replace, env, item)
|
|
424
|
-
end
|
|
425
|
-
when :theme_chooser
|
|
426
|
-
theme = payload.fetch(:item).to_sym
|
|
427
|
-
@theme_popup_restore = nil
|
|
428
|
-
cmds << [:terminal, :set_theme, theme]
|
|
429
|
-
cmds << [:send, :alert, :show, { level: :info, message: "Theme: #{theme}" }]
|
|
430
|
-
when :completion
|
|
431
|
-
apply_completion(payload.fetch(:item, "").to_s, range: @completion_range)
|
|
432
|
-
@completion_range = nil
|
|
433
|
-
end
|
|
434
|
-
when :popup_changed
|
|
435
|
-
if payload[:kind]&.to_sym == :theme_chooser
|
|
436
|
-
theme = payload.fetch(:item).to_sym
|
|
437
|
-
cmds << [:terminal, :set_theme, theme]
|
|
438
|
-
end
|
|
439
|
-
when :popup_cancelled
|
|
440
|
-
if payload[:kind]&.to_sym == :theme_chooser && @theme_popup_restore
|
|
441
|
-
cmds << [:terminal, :set_theme, @theme_popup_restore]
|
|
442
|
-
@theme_popup_restore = nil
|
|
443
|
-
end
|
|
444
|
-
when :pager_search_set
|
|
445
|
-
pattern = payload.fetch(:pattern, "").to_s
|
|
446
|
-
regex = !!payload[:regex]
|
|
447
|
-
direction = (payload[:direction] || :forward).to_sym
|
|
448
|
-
result = pager.search_set!(pattern: pattern, regex: regex, direction: direction)
|
|
449
|
-
cmds.concat(handle_search_result(result))
|
|
450
|
-
when :pager_search_step
|
|
451
|
-
direction = (payload[:direction] || :forward).to_sym
|
|
452
|
-
result = pager.search_step!(direction: direction)
|
|
453
|
-
cmds.concat(handle_search_result(result))
|
|
454
|
-
when :pager_isearch_update
|
|
455
|
-
pattern = payload.fetch(:pattern, "").to_s
|
|
456
|
-
direction = (payload[:direction] || :forward).to_sym
|
|
457
|
-
result = pager.isearch_update!(pattern: pattern, direction: direction)
|
|
458
|
-
cmds.concat(handle_search_result(result))
|
|
459
|
-
cmds << [:send, :isearch, :isearch_set_failed, { failed: result[:status] != :moved }]
|
|
460
|
-
when :pager_isearch_step
|
|
461
|
-
direction = (payload[:direction] || :forward).to_sym
|
|
462
|
-
result = pager.isearch_step!(direction: direction)
|
|
463
|
-
cmds.concat(handle_search_result(result))
|
|
464
|
-
cmds << [:send, :isearch, :isearch_set_failed, { failed: result[:status] != :moved }]
|
|
465
|
-
when :pager_isearch_cancel
|
|
466
|
-
pager.isearch_cancel!
|
|
467
|
-
cmds << [:send, :isearch, :isearch_set_failed, { failed: false }]
|
|
468
|
-
when :pager_isearch_commit
|
|
469
|
-
pattern = payload.fetch(:pattern, "").to_s
|
|
470
|
-
direction = (payload[:direction] || :forward).to_sym
|
|
471
|
-
result = pager.isearch_commit!(pattern: pattern, direction: direction)
|
|
472
|
-
cmds.concat(handle_search_result(result))
|
|
473
|
-
cmds << [:send, :isearch, :isearch_set_failed, { failed: false }]
|
|
474
|
-
when :paste
|
|
475
|
-
text = payload.fetch(:text, "").to_s
|
|
476
|
-
env = action_env(event: nil)
|
|
477
|
-
field.act_on(:paste, text, env: env)
|
|
478
|
-
end
|
|
479
|
-
cmds
|
|
480
|
-
end
|
|
537
|
+
def apply_completion_popup_backtab(payload)
|
|
538
|
+
return [] unless @completion_path
|
|
481
539
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if event&.respond_to?(:key)
|
|
485
|
-
event.key.inspect
|
|
486
|
-
elsif event&.respond_to?(:mouse)
|
|
487
|
-
event.mouse.inspect
|
|
488
|
-
end
|
|
489
|
-
Fatty.debug("ShellSession#handle_action: #{which}", tag: :keymap)
|
|
490
|
-
env = action_env(event: event)
|
|
491
|
-
apply_action(action, args, event, env: env)
|
|
492
|
-
end
|
|
540
|
+
popup = payload[:session]
|
|
541
|
+
return [] unless popup
|
|
493
542
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
def apply_action(action, args, ev, env:)
|
|
497
|
-
@field.reset_completion_cycle!
|
|
498
|
-
defn = Fatty::Actions.lookup(action)
|
|
499
|
-
result = Fatty::Actions.call(action, env, *args)
|
|
500
|
-
if defn && [:field, :buffer].include?(defn[:on])
|
|
501
|
-
@field.sync_virtual_suffix!
|
|
502
|
-
end
|
|
503
|
-
result.is_a?(Array) ? result : []
|
|
504
|
-
rescue Fatty::ActionError => e
|
|
505
|
-
[alert_cmd(:error, e.message, ev: ev)]
|
|
506
|
-
end
|
|
543
|
+
path = @completion_popup_prefix || payload[:query].to_s
|
|
544
|
+
path = payload[:item].to_s if path.empty?
|
|
507
545
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
out << "\n[exit #{status.exitstatus}]\n" if status&.exitstatus && status.exitstatus != 0
|
|
513
|
-
out
|
|
514
|
-
rescue Errno::ENOENT
|
|
515
|
-
"Command not found: #{line}\n"
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
def consume_search_regex_flag
|
|
519
|
-
regex = false
|
|
520
|
-
if counter.digits.to_s == "4"
|
|
521
|
-
regex = true
|
|
522
|
-
counter.clear!
|
|
523
|
-
end
|
|
524
|
-
regex
|
|
546
|
+
parent = parent_completion_path(path)
|
|
547
|
+
return [] unless parent
|
|
548
|
+
|
|
549
|
+
replace_completion_popup_path(popup, parent)
|
|
525
550
|
end
|
|
526
551
|
|
|
527
|
-
def
|
|
528
|
-
|
|
552
|
+
def replace_completion_popup_path(popup, prefix)
|
|
553
|
+
@completion_popup_prefix = prefix.to_s
|
|
554
|
+
candidates = @field.path_completion_candidates_for(@completion_popup_prefix)
|
|
555
|
+
|
|
556
|
+
popup.replace_source(
|
|
557
|
+
source: candidates,
|
|
558
|
+
title: completion_popup_title(@completion_popup_prefix),
|
|
559
|
+
query: "",
|
|
560
|
+
current: :top,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
[]
|
|
564
|
+
end
|
|
529
565
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
msg = "Search reached boundary" if msg.empty?
|
|
534
|
-
[[:send, :alert, :show, { level: :info, message: msg }]]
|
|
535
|
-
when :not_found
|
|
536
|
-
[[:send, :alert, :show, { level: :info, message: "No matches" }]]
|
|
566
|
+
def completion_popup_title(prefix = nil)
|
|
567
|
+
if prefix && !prefix.empty?
|
|
568
|
+
"Completions: #{prefix}"
|
|
537
569
|
else
|
|
538
|
-
|
|
570
|
+
"Completions"
|
|
539
571
|
end
|
|
540
572
|
end
|
|
541
573
|
|
|
@@ -545,22 +577,5 @@ module Fatty
|
|
|
545
577
|
@field.sync_virtual_suffix!
|
|
546
578
|
result
|
|
547
579
|
end
|
|
548
|
-
|
|
549
|
-
def alert_cmd(level, message, ev: nil)
|
|
550
|
-
details =
|
|
551
|
-
if ev
|
|
552
|
-
{
|
|
553
|
-
key: ev.key,
|
|
554
|
-
ctrl: ev.respond_to?(:ctrl?) ? ev.ctrl? : ev.ctrl,
|
|
555
|
-
meta: ev.respond_to?(:meta?) ? ev.meta? : ev.meta,
|
|
556
|
-
shift: ev.respond_to?(:shift?) ? ev.shift? : ev.shift,
|
|
557
|
-
text: ev.text
|
|
558
|
-
}
|
|
559
|
-
else
|
|
560
|
-
{}
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
[:send, :alert, :show, { level: level, message: message, details: details }]
|
|
564
|
-
end
|
|
565
580
|
end
|
|
566
581
|
end
|