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,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class SearchSession < Session
|
|
5
|
+
action_on :session
|
|
6
|
+
|
|
7
|
+
attr_reader :field, :direction, :regex
|
|
8
|
+
|
|
9
|
+
DEFAULT_SEARCH_HISTORY_FILE = File.expand_path("~/.fatty_search_history")
|
|
10
|
+
DEFAULT_SEARCH_HISTORY_MAX = 200
|
|
11
|
+
|
|
12
|
+
def initialize(direction: :backward, regex: false, history: nil, prefix: nil)
|
|
13
|
+
super(keymap: Keymaps.emacs, views: [])
|
|
14
|
+
|
|
15
|
+
@direction = direction.to_sym
|
|
16
|
+
@regex = !!regex
|
|
17
|
+
|
|
18
|
+
prompt = search_prompt(direction: @direction, regex: @regex)
|
|
19
|
+
|
|
20
|
+
@field = Fatty::InputField.new(
|
|
21
|
+
prompt: prompt,
|
|
22
|
+
history: history,
|
|
23
|
+
history_kind: -> { @regex ? :search_regex : :search_string },
|
|
24
|
+
)
|
|
25
|
+
text = prefix.to_s
|
|
26
|
+
@field.buffer.replace(text) unless text.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#########################################################################################
|
|
30
|
+
# Framework and Session Hooks
|
|
31
|
+
#########################################################################################
|
|
32
|
+
|
|
33
|
+
def keymap_contexts
|
|
34
|
+
[:search, :text, :terminal]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def view(screen:, renderer:)
|
|
38
|
+
row = screen.output_rect.rows - 1
|
|
39
|
+
|
|
40
|
+
::Curses.curs_set(1)
|
|
41
|
+
renderer.render_pager_field(
|
|
42
|
+
@field,
|
|
43
|
+
row: row,
|
|
44
|
+
role: :search_input,
|
|
45
|
+
)
|
|
46
|
+
renderer.restore_output_cursor(@field, row: row)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
############################################################################################
|
|
50
|
+
# Actions
|
|
51
|
+
############################################################################################
|
|
52
|
+
|
|
53
|
+
desc "Return the line so far as the prompt input"
|
|
54
|
+
action :search_accept do
|
|
55
|
+
search_accept
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "End the search session"
|
|
59
|
+
action :search_cancel do
|
|
60
|
+
cancel!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc "Toggle between string i-search and regex search"
|
|
64
|
+
action :search_toggle_regex do
|
|
65
|
+
toggle_regex
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "Move to the next search match"
|
|
69
|
+
action :search_step_forward do
|
|
70
|
+
step_search(:forward)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc "Move to the prior search match"
|
|
74
|
+
action :search_step_backward do
|
|
75
|
+
step_search(:backward)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_action(action, args, event:)
|
|
79
|
+
env = ActionEnvironment.new(
|
|
80
|
+
session: self,
|
|
81
|
+
counter: counter,
|
|
82
|
+
event: event,
|
|
83
|
+
buffer: @field.buffer,
|
|
84
|
+
field: @field,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if Fatty::Actions.lookup(action)&.fetch(:on) == :session
|
|
88
|
+
Fatty::Actions.call(action, env, *args)
|
|
89
|
+
else
|
|
90
|
+
@field.act_on(action, *args, env: env)
|
|
91
|
+
end
|
|
92
|
+
rescue Fatty::ActionError
|
|
93
|
+
[]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def toggle_regex
|
|
97
|
+
@regex = !@regex
|
|
98
|
+
@field.prompt = search_prompt(direction: @direction, regex: @regex)
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cancel!
|
|
103
|
+
[[:terminal, :pop_modal]]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def search_prompt(direction:, regex:)
|
|
107
|
+
label =
|
|
108
|
+
if regex
|
|
109
|
+
"Regex: "
|
|
110
|
+
else
|
|
111
|
+
"Search string: "
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# If you want a subtle direction hint right at the prompt:
|
|
115
|
+
dir = (direction == :backward ? "↑ " : "↓ ")
|
|
116
|
+
dir + label
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def search_accept
|
|
120
|
+
pattern = @field.accept_line.to_s
|
|
121
|
+
|
|
122
|
+
cmds = []
|
|
123
|
+
cmds << [
|
|
124
|
+
:terminal,
|
|
125
|
+
:send_modal_owner,
|
|
126
|
+
[:cmd, :pager_search_set, { pattern: pattern, direction: direction, regex: regex }]
|
|
127
|
+
]
|
|
128
|
+
cmds << [:terminal, :pop_modal]
|
|
129
|
+
cmds
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def step_search(dir)
|
|
133
|
+
[[:terminal, :send_modal_owner, [:cmd, :pager_search_step, { direction: dir }]]]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Fatty
|
|
6
|
+
class ShellSession < OutputSession
|
|
7
|
+
action_on :session
|
|
8
|
+
|
|
9
|
+
attr_reader :field, :history
|
|
10
|
+
|
|
11
|
+
def initialize(prompt: "sh> ", on_accept: nil, completion_proc: nil, history_ctx: nil, history_path: :default)
|
|
12
|
+
super(
|
|
13
|
+
keymap: Keymaps.emacs,
|
|
14
|
+
views: [
|
|
15
|
+
Fatty::OutputView.new(z: 0),
|
|
16
|
+
Fatty::InputView.new(z: 10),
|
|
17
|
+
Fatty::CursorView.new(z: 100),
|
|
18
|
+
]
|
|
19
|
+
)
|
|
20
|
+
@history = Fatty::History.for_path(history_path)
|
|
21
|
+
@field = Fatty::InputField.new(
|
|
22
|
+
prompt: prompt,
|
|
23
|
+
history: @history,
|
|
24
|
+
completion_proc: completion_proc,
|
|
25
|
+
history_kind: :command,
|
|
26
|
+
history_ctx: history_ctx,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@on_accept = on_accept
|
|
30
|
+
@completion_proc = completion_proc
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#########################################################################################
|
|
34
|
+
# Framework and Session Hooks
|
|
35
|
+
#########################################################################################
|
|
36
|
+
|
|
37
|
+
def init(terminal:)
|
|
38
|
+
super
|
|
39
|
+
resize_output!
|
|
40
|
+
[]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def keymap_contexts
|
|
44
|
+
pager_active? ? [:paging, :terminal] : [:input, :text, :terminal]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update_key(ev)
|
|
48
|
+
return [] unless ev.is_a?(Fatty::KeyEvent)
|
|
49
|
+
|
|
50
|
+
key_str = "key=#{ev} raw=#{ev.raw}"
|
|
51
|
+
Fatty.debug("ShellSession.update_key: #{key_str}", tag: :session)
|
|
52
|
+
case ev.key
|
|
53
|
+
when :resize
|
|
54
|
+
[[:terminal, :handle_resize]]
|
|
55
|
+
when :enter, :return
|
|
56
|
+
# safety: if somehow not bound, still accept
|
|
57
|
+
submit_line
|
|
58
|
+
else
|
|
59
|
+
[alert_cmd(:warn, "Unbound key: #{ev} (edit config in 'keybindings.yml' to bind)", ev: ev)]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def view(screen:, renderer:)
|
|
64
|
+
if pager_active?
|
|
65
|
+
::Curses.curs_set(0)
|
|
66
|
+
viewport = pager_status_viewport(screen)
|
|
67
|
+
highlights = pager.search_visible_highlights(viewport: viewport)
|
|
68
|
+
|
|
69
|
+
renderer.render_output(output, viewport: viewport, highlights: highlights)
|
|
70
|
+
renderer.render_pager_field(
|
|
71
|
+
pager_field,
|
|
72
|
+
row: screen.output_rect.rows - 1,
|
|
73
|
+
role: :pager_status,
|
|
74
|
+
)
|
|
75
|
+
else
|
|
76
|
+
::Curses.curs_set(1)
|
|
77
|
+
renderer.render_output(output, viewport: pager_viewport, highlights: nil)
|
|
78
|
+
renderer.render_input_field(field)
|
|
79
|
+
renderer.restore_cursor(field)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def pager_status_viewport(screen)
|
|
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
|
+
############################################################################################
|
|
112
|
+
# 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
|
|
125
|
+
|
|
126
|
+
desc "Pass the current shell input line to the proc"
|
|
127
|
+
action :submit_line do
|
|
128
|
+
submit_line
|
|
129
|
+
end
|
|
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?
|
|
136
|
+
|
|
137
|
+
Fatty.info("ShellSession: accept_line: #{line}")
|
|
138
|
+
|
|
139
|
+
case line
|
|
140
|
+
when "exit", "quit"
|
|
141
|
+
[[:terminal, :quit]]
|
|
142
|
+
when "clear"
|
|
143
|
+
reset_output!
|
|
144
|
+
[[:send, :alert, :clear, {}]]
|
|
145
|
+
else
|
|
146
|
+
reset_for_command!
|
|
147
|
+
anchor = output.lines.length
|
|
148
|
+
pager.begin_command!(anchor: anchor)
|
|
149
|
+
|
|
150
|
+
commands =
|
|
151
|
+
if @on_accept
|
|
152
|
+
@on_accept.call(line, accept_env)
|
|
153
|
+
else
|
|
154
|
+
run_default_command(line)
|
|
155
|
+
end
|
|
156
|
+
normalize_accept_commands(commands)
|
|
157
|
+
end
|
|
158
|
+
rescue Errno::ENOENT
|
|
159
|
+
[[:send, :alert, :show, { level: :error, message: "Command not found (#{line})" }]]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
desc "Accept the current shell input line and switch output to scrolling"
|
|
163
|
+
action :submit_and_scroll do
|
|
164
|
+
before = output.lines.length
|
|
165
|
+
cmds = submit_line
|
|
166
|
+
pager.paging_to_scrolling if output.lines.length > before
|
|
167
|
+
cmds
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
desc "Interrupt scrolling, otherwise quit Fatty"
|
|
171
|
+
action :interrupt do
|
|
172
|
+
if pager.mode == :scrolling
|
|
173
|
+
pager.quit_paging
|
|
174
|
+
[]
|
|
175
|
+
else
|
|
176
|
+
[[:terminal, :quit]]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
desc "Quit if input is empty, otherwise delete forward"
|
|
181
|
+
action :interrupt_if_empty do
|
|
182
|
+
if pager.mode == :scrolling
|
|
183
|
+
pager.quit_paging
|
|
184
|
+
[]
|
|
185
|
+
elsif @field.empty?
|
|
186
|
+
[[:terminal, :quit]]
|
|
187
|
+
else
|
|
188
|
+
@field.act_on(:delete_char_forward, env: action_env(event: nil))
|
|
189
|
+
[]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
desc "Clear shell output"
|
|
194
|
+
action :clear_output do
|
|
195
|
+
reset_output!
|
|
196
|
+
[]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
desc "Cycle to the next theme"
|
|
200
|
+
action :cycle_theme do
|
|
201
|
+
[[:terminal, :cycle_theme]]
|
|
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)
|
|
225
|
+
[]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
desc "Complete the current input prefix"
|
|
229
|
+
action :complete do
|
|
230
|
+
with_virtual_suffix_sync do
|
|
231
|
+
@field.cycle_completion!
|
|
232
|
+
[]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
desc "Open completion popup"
|
|
237
|
+
action :completion_popup do
|
|
238
|
+
with_virtual_suffix_sync do
|
|
239
|
+
candidates = @field.popup_completion_candidates
|
|
240
|
+
|
|
241
|
+
if candidates.empty?
|
|
242
|
+
[]
|
|
243
|
+
elsif candidates.length == 1
|
|
244
|
+
apply_completion(candidates.first, range: @field.popup_completion_range)
|
|
245
|
+
else
|
|
246
|
+
@completion_range = @field.popup_completion_range
|
|
247
|
+
# NOTE: we don't pass a matcher so that the default_matcher is used.
|
|
248
|
+
popup = Fatty::PopUpSession.new(
|
|
249
|
+
source: candidates,
|
|
250
|
+
kind: :completion,
|
|
251
|
+
title: "Completions",
|
|
252
|
+
prompt: "Complete: ",
|
|
253
|
+
order: :as_given,
|
|
254
|
+
selection: :top,
|
|
255
|
+
initial_query: @field.popup_completion_query.to_s,
|
|
256
|
+
)
|
|
257
|
+
[[:terminal, :push_modal, popup]]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
desc "Search pager forward"
|
|
263
|
+
action :pager_search_forward do
|
|
264
|
+
regex = consume_search_regex_flag
|
|
265
|
+
[[
|
|
266
|
+
:terminal,
|
|
267
|
+
:push_modal,
|
|
268
|
+
Fatty::SearchSession.new(direction: :forward, regex: regex, history: @history)
|
|
269
|
+
]]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
desc "Search pager backward"
|
|
273
|
+
action :pager_search_backward do
|
|
274
|
+
regex = consume_search_regex_flag
|
|
275
|
+
[[
|
|
276
|
+
:terminal,
|
|
277
|
+
:push_modal,
|
|
278
|
+
Fatty::SearchSession.new(direction: :backward, regex: regex, history: @history)
|
|
279
|
+
]]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
desc "Start incremental pager search forward"
|
|
283
|
+
action :pager_isearch_forward do
|
|
284
|
+
[[
|
|
285
|
+
:terminal,
|
|
286
|
+
:push_modal,
|
|
287
|
+
Fatty::ISearchSession.new(direction: :forward, history: @history, last_pattern: pager.search_pattern)
|
|
288
|
+
]]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
desc "Start incremental pager search backward"
|
|
292
|
+
action :pager_isearch_backward do
|
|
293
|
+
[[
|
|
294
|
+
:terminal,
|
|
295
|
+
:push_modal,
|
|
296
|
+
Fatty::ISearchSession.new(direction: :backward, history: @history, last_pattern: pager.search_pattern)
|
|
297
|
+
]]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
desc "Repeat pager search forward"
|
|
301
|
+
action :pager_search_next do
|
|
302
|
+
handle_search_result(pager.search_repeat_next!)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
desc "Repeat pager search backward"
|
|
306
|
+
action :pager_search_prev do
|
|
307
|
+
handle_search_result(pager.search_repeat_prev!)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
desc "Open command history search"
|
|
311
|
+
action :history_search do
|
|
312
|
+
src = ->(_q = nil) { @history.entries.select(&:command?).last(500).map(&:text) }
|
|
313
|
+
|
|
314
|
+
popup = Fatty::PopUpSession.new(
|
|
315
|
+
source: src,
|
|
316
|
+
kind: :history_search,
|
|
317
|
+
title: "History",
|
|
318
|
+
prompt: "I-search: ",
|
|
319
|
+
order: :as_given,
|
|
320
|
+
selection: :bottom,
|
|
321
|
+
initial_query: @field.buffer.text,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
[[:terminal, :push_modal, popup]]
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
#########################################################################################
|
|
328
|
+
# Completion
|
|
329
|
+
#########################################################################################
|
|
330
|
+
|
|
331
|
+
def completion_candidates
|
|
332
|
+
path_candidates = @field.path_completion_candidates
|
|
333
|
+
return path_candidates if path_candidates.any?
|
|
334
|
+
|
|
335
|
+
return [] unless @completion_proc
|
|
336
|
+
|
|
337
|
+
prefix = completion_prefix
|
|
338
|
+
Array(@completion_proc.call(@field.buffer))
|
|
339
|
+
.compact
|
|
340
|
+
.map(&:to_s)
|
|
341
|
+
.reject(&:empty?)
|
|
342
|
+
.select { |s| s.start_with?(prefix) }
|
|
343
|
+
.uniq
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def completion_prefix
|
|
347
|
+
@field.buffer.completion_prefix
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def completion_prefix_range
|
|
351
|
+
buffer = @field.buffer
|
|
352
|
+
prefix = completion_prefix
|
|
353
|
+
finish = buffer.cursor
|
|
354
|
+
start = finish - prefix.length
|
|
355
|
+
start...finish
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def longest_common_prefix(strings)
|
|
359
|
+
result = ""
|
|
360
|
+
if strings.any?
|
|
361
|
+
result = strings.first.to_s.dup
|
|
362
|
+
strings.drop(1).each do |s|
|
|
363
|
+
other = s.to_s
|
|
364
|
+
i = 0
|
|
365
|
+
max = [result.length, other.length].min
|
|
366
|
+
i += 1 while i < max && result[i] == other[i]
|
|
367
|
+
result = result[0...i]
|
|
368
|
+
break if result.empty?
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
result
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def apply_completion_prefix(prefix)
|
|
375
|
+
text = prefix.to_s
|
|
376
|
+
commands = []
|
|
377
|
+
unless text.empty?
|
|
378
|
+
buffer = @field.buffer
|
|
379
|
+
buffer.replace_range(completion_prefix_range, text)
|
|
380
|
+
end
|
|
381
|
+
commands
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def apply_completion(candidate, range: nil)
|
|
385
|
+
candidate = candidate.to_s
|
|
386
|
+
return [] if candidate.empty?
|
|
387
|
+
|
|
388
|
+
buffer = @field.buffer
|
|
389
|
+
target = range || buffer.completion_replace_range
|
|
390
|
+
old_text = buffer.text.dup
|
|
391
|
+
old_end = target.end
|
|
392
|
+
append_space =
|
|
393
|
+
!candidate.match?(/\s\z/) &&
|
|
394
|
+
(old_end >= old_text.length || old_text[old_end]&.match?(/\s/))
|
|
395
|
+
inserted = append_space ? "#{candidate} " : candidate
|
|
396
|
+
buffer.replace_range(target, inserted)
|
|
397
|
+
[]
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
private
|
|
401
|
+
|
|
402
|
+
def accept_env
|
|
403
|
+
Fatty::AcceptEnv.new(session: self)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def normalize_accept_commands(commands)
|
|
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
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def update_cmd(name, payload)
|
|
415
|
+
cmds = []
|
|
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
|
|
481
|
+
|
|
482
|
+
def handle_action(action, args, event:)
|
|
483
|
+
which =
|
|
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
|
|
493
|
+
|
|
494
|
+
# Centralized dispatch: actions declare their target (:buffer/:field/:pager)
|
|
495
|
+
# and Fatty::Actions routes through ActionEnvironment.
|
|
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
|
|
507
|
+
|
|
508
|
+
def run_default_command(line)
|
|
509
|
+
append_output("$ #{line}\n", follow: true)
|
|
510
|
+
out, status = Open3.capture2e(line)
|
|
511
|
+
# optional: include exit status line
|
|
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
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def handle_search_result(result)
|
|
528
|
+
return unless result.is_a?(Hash)
|
|
529
|
+
|
|
530
|
+
case result[:status]
|
|
531
|
+
when :boundary
|
|
532
|
+
msg = result[:message].to_s
|
|
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" }]]
|
|
537
|
+
else
|
|
538
|
+
[]
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def with_virtual_suffix_sync
|
|
543
|
+
@field.sync_virtual_suffix!
|
|
544
|
+
result = yield
|
|
545
|
+
@field.sync_virtual_suffix!
|
|
546
|
+
result
|
|
547
|
+
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
|
+
end
|
|
566
|
+
end
|