kward 0.71.0 → 0.73.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 +4 -4
- data/.github/workflows/ci.yml +30 -0
- data/CHANGELOG.md +93 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +415 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +123 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +75 -5
- data/doc/session-management.md +35 -1
- data/doc/shell.md +332 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +79 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/ansi.rb +62 -23
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +268 -4
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +217 -9
- data/lib/kward/cli/slash_commands.rb +628 -2
- data/lib/kward/cli/tabs.rb +725 -0
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +150 -26
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +145 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +559 -0
- data/lib/kward/image_attachments.rb +3 -1
- data/lib/kward/interactive_pty_runner.rb +151 -0
- data/lib/kward/local_command_runner.rb +155 -0
- data/lib/kward/local_pty_command_runner.rb +171 -0
- data/lib/kward/model/context_usage.rb +2 -2
- data/lib/kward/model/payloads.rb +2 -5
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +84 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1218 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +354 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +244 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1271 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +288 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +451 -57
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +99 -56
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +19 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +366 -222
- data/lib/kward/prompts/commands.rb +9 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +169 -83
- data/lib/kward/rpc/session_manager.rb +45 -121
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/rpc/tool_metadata.rb +11 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -39
- data/lib/kward/scratchpad_runner.rb +56 -0
- data/lib/kward/session_diff.rb +20 -3
- data/lib/kward/session_naming.rb +11 -0
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +204 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +93 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +10 -0
- data/lib/kward/workspace.rb +125 -87
- data/templates/default/fulldoc/html/css/kward.css +140 -36
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +23 -34
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +67 -1
|
@@ -10,35 +10,52 @@ module Kward
|
|
|
10
10
|
pending = @pending_keys.shift unless @pending_keys.empty?
|
|
11
11
|
return pending if pending
|
|
12
12
|
|
|
13
|
+
return nil if nonblock && @input_io.respond_to?(:wait_readable) && !@input_io.wait_readable(0)
|
|
14
|
+
|
|
13
15
|
@reader.read_keypress(echo: false, raw: true, nonblock: nonblock)
|
|
14
16
|
rescue TTY::Reader::InputInterrupt
|
|
15
|
-
|
|
17
|
+
TerminalKeys::CTRL_C
|
|
16
18
|
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
17
19
|
nil
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
def handle_editor_input_key(key)
|
|
23
|
+
tab_result = handle_tab_key_binding(key)
|
|
24
|
+
return tab_result unless tab_result == false
|
|
25
|
+
|
|
26
|
+
result = handle_editor_key(key)
|
|
27
|
+
result.is_a?(String) ? true : result
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
def handle_key(key)
|
|
21
31
|
return submit_input if key.nil?
|
|
32
|
+
return handle_interactive_key(key) if interactive_active_locked?
|
|
33
|
+
return handle_editor_input_key(key) if editor_active?
|
|
34
|
+
return handle_project_browser_key(key) if project_browser_visible?
|
|
35
|
+
return handle_history_search_key(key) if history_search_active?
|
|
36
|
+
return true if handle_mouse_reporting_key(key)
|
|
22
37
|
return if handle_bracketed_paste_key(key)
|
|
23
38
|
|
|
24
39
|
csi_result = handle_csi_u_key(key)
|
|
25
40
|
return csi_result unless csi_result == false
|
|
26
41
|
return if handle_shift_enter_key(key)
|
|
27
|
-
if key
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
return true if handle_bundled_key(key) { |token| handle_key(token) }
|
|
43
|
+
|
|
44
|
+
completion_result = handle_completion_provider_key(key)
|
|
45
|
+
return completion_result unless completion_result == false
|
|
46
|
+
|
|
47
|
+
reasoning_result = handle_reasoning_key_binding(key)
|
|
48
|
+
return reasoning_result unless reasoning_result == false
|
|
49
|
+
|
|
50
|
+
tab_result = handle_tab_key_binding(key)
|
|
51
|
+
return tab_result unless tab_result == false
|
|
34
52
|
|
|
35
53
|
binding_result = handle_composer_key_binding(key)
|
|
36
54
|
return binding_result unless binding_result == false
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
case key_name
|
|
56
|
+
case key_name_for(key)
|
|
40
57
|
when :return, :enter
|
|
41
|
-
submit_input
|
|
58
|
+
file_open_overlay_visible? ? open_selected_file_in_editor(fallback_to_typed_path: true) : submit_input
|
|
42
59
|
when :backspace
|
|
43
60
|
delete_before_cursor
|
|
44
61
|
when :delete
|
|
@@ -65,6 +82,8 @@ module Kward
|
|
|
65
82
|
yank_kill_buffer
|
|
66
83
|
when :ctrl_l
|
|
67
84
|
redraw_screen_locked
|
|
85
|
+
when :ctrl_r
|
|
86
|
+
start_history_search
|
|
68
87
|
when :left
|
|
69
88
|
move_cursor_left
|
|
70
89
|
when :right
|
|
@@ -74,21 +93,35 @@ module Kward
|
|
|
74
93
|
when :end
|
|
75
94
|
move_to_end_of_line
|
|
76
95
|
when :up
|
|
77
|
-
|
|
96
|
+
if file_overlay_visible?
|
|
97
|
+
select_previous_file_mention
|
|
98
|
+
elsif slash_overlay_visible?
|
|
99
|
+
select_previous_slash_command
|
|
100
|
+
else
|
|
101
|
+
recall_previous_history
|
|
102
|
+
end
|
|
78
103
|
when :down
|
|
79
|
-
|
|
104
|
+
if file_overlay_visible?
|
|
105
|
+
select_next_file_mention
|
|
106
|
+
elsif slash_overlay_visible?
|
|
107
|
+
select_next_slash_command
|
|
108
|
+
else
|
|
109
|
+
recall_next_history
|
|
110
|
+
end
|
|
80
111
|
else
|
|
81
112
|
case key
|
|
82
113
|
when "\n", "\r"
|
|
83
|
-
submit_input
|
|
114
|
+
file_open_overlay_visible? ? open_selected_file_in_editor(fallback_to_typed_path: true) : submit_input
|
|
84
115
|
when "\t"
|
|
85
|
-
|
|
116
|
+
handle_tab_completion_key
|
|
86
117
|
when "\b", "\x7F"
|
|
87
118
|
delete_before_cursor
|
|
88
|
-
when
|
|
119
|
+
when TerminalKeys::CTRL_D
|
|
89
120
|
delete_at_cursor_or_exit
|
|
90
|
-
when
|
|
121
|
+
when TerminalKeys::CTRL_C
|
|
91
122
|
cancel_input_or_interrupt
|
|
123
|
+
when TerminalKeys::CTRL_R
|
|
124
|
+
start_history_search
|
|
92
125
|
when "\e"
|
|
93
126
|
handle_escape_sequence
|
|
94
127
|
else
|
|
@@ -97,31 +130,114 @@ module Kward
|
|
|
97
130
|
end
|
|
98
131
|
end
|
|
99
132
|
|
|
133
|
+
def handle_history_search_key(key)
|
|
134
|
+
csi_result = handle_history_search_csi_u_key(key)
|
|
135
|
+
return csi_result unless csi_result == false
|
|
136
|
+
return true if handle_bundled_key(key) { |token| handle_history_search_key(token) }
|
|
137
|
+
|
|
138
|
+
case key_name_for(key)
|
|
139
|
+
when :return, :enter
|
|
140
|
+
accept_history_search
|
|
141
|
+
when :up
|
|
142
|
+
select_previous_history_search_match
|
|
143
|
+
when :down
|
|
144
|
+
select_next_history_search_match
|
|
145
|
+
when :backspace
|
|
146
|
+
update_history_search_query(composer_input[0...-1].to_s)
|
|
147
|
+
when :ctrl_c
|
|
148
|
+
cancel_history_search
|
|
149
|
+
else
|
|
150
|
+
case key
|
|
151
|
+
when "\n", "\r"
|
|
152
|
+
accept_history_search
|
|
153
|
+
when "\b", "\x7F"
|
|
154
|
+
update_history_search_query(composer_input[0...-1].to_s)
|
|
155
|
+
when TerminalKeys::CTRL_C, "\e"
|
|
156
|
+
cancel_history_search
|
|
157
|
+
else
|
|
158
|
+
append_history_search_key(key)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def handle_history_search_csi_u_key(key)
|
|
165
|
+
sequence = parse_csi_u_key(key)
|
|
166
|
+
return false unless sequence
|
|
167
|
+
|
|
168
|
+
code = sequence[:code]
|
|
169
|
+
modifier = sequence[:modifier]
|
|
170
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
171
|
+
|
|
172
|
+
if ctrl_modifier?(modifier) && ctrl_code_for(code) == 99
|
|
173
|
+
cancel_history_search
|
|
174
|
+
elsif code == 13
|
|
175
|
+
accept_history_search
|
|
176
|
+
elsif code == 27
|
|
177
|
+
cancel_history_search
|
|
178
|
+
elsif code == 8 || code == 127
|
|
179
|
+
update_history_search_query(composer_input[0...-1].to_s)
|
|
180
|
+
else
|
|
181
|
+
text = csi_u_printable_text(sequence)
|
|
182
|
+
update_history_search_query(composer_input + text) if text
|
|
183
|
+
end
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def append_history_search_key(key)
|
|
188
|
+
return unless key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
189
|
+
|
|
190
|
+
update_history_search_query(composer_input + key)
|
|
191
|
+
end
|
|
192
|
+
|
|
100
193
|
def cancel_input_or_interrupt
|
|
101
194
|
return CANCEL_INPUT if @busy
|
|
102
195
|
|
|
103
|
-
|
|
196
|
+
true
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def handle_tab_completion_key
|
|
200
|
+
open_selected_file_in_editor || complete_selected_file_mention || complete_selected_slash_command || insert_key("\t")
|
|
104
201
|
end
|
|
105
202
|
|
|
106
203
|
def handle_escape_sequence
|
|
107
204
|
pending_sequence = read_pending_escape_sequence
|
|
108
|
-
return true if pending_sequence.empty? && dismiss_slash_overlay
|
|
205
|
+
return true if pending_sequence.empty? && (dismiss_file_overlay || dismiss_slash_overlay)
|
|
109
206
|
|
|
110
207
|
full_sequence = "\e#{pending_sequence}"
|
|
208
|
+
return true if handle_mouse_reporting_key(full_sequence)
|
|
209
|
+
|
|
111
210
|
sequence = next_key_token(full_sequence)
|
|
112
211
|
queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
|
|
113
|
-
return true if sequence == "\e" && dismiss_slash_overlay
|
|
212
|
+
return true if sequence == "\e" && (dismiss_file_overlay || dismiss_slash_overlay)
|
|
114
213
|
return true if handle_shift_enter_key(sequence)
|
|
115
214
|
|
|
215
|
+
reasoning_result = handle_reasoning_key_binding(sequence)
|
|
216
|
+
return reasoning_result unless reasoning_result == false
|
|
217
|
+
|
|
218
|
+
tab_result = handle_tab_key_binding(sequence)
|
|
219
|
+
return tab_result unless tab_result == false
|
|
220
|
+
|
|
116
221
|
binding_result = handle_composer_key_binding(sequence)
|
|
117
222
|
return binding_result unless binding_result == false
|
|
118
223
|
|
|
119
|
-
|
|
120
|
-
case key_name
|
|
224
|
+
case key_name_for(sequence)
|
|
121
225
|
when :up
|
|
122
|
-
|
|
226
|
+
if file_overlay_visible?
|
|
227
|
+
select_previous_file_mention
|
|
228
|
+
elsif slash_overlay_visible?
|
|
229
|
+
select_previous_slash_command
|
|
230
|
+
else
|
|
231
|
+
recall_previous_history
|
|
232
|
+
end
|
|
123
233
|
when :down
|
|
124
|
-
|
|
234
|
+
if file_overlay_visible?
|
|
235
|
+
select_next_file_mention
|
|
236
|
+
elsif slash_overlay_visible?
|
|
237
|
+
select_next_slash_command
|
|
238
|
+
else
|
|
239
|
+
recall_next_history
|
|
240
|
+
end
|
|
125
241
|
when :left
|
|
126
242
|
move_cursor_left
|
|
127
243
|
when :right
|
|
@@ -137,14 +253,43 @@ module Kward
|
|
|
137
253
|
end
|
|
138
254
|
|
|
139
255
|
def handle_bracketed_paste_key(key)
|
|
256
|
+
handle_bracketed_paste(key) { |content| insert_paste(content) }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def handle_mouse_reporting_key(key)
|
|
260
|
+
event = parse_sgr_mouse_event(key)
|
|
261
|
+
return false unless event
|
|
262
|
+
|
|
263
|
+
queue_pending_keys(event[:remaining]) unless event[:remaining].empty?
|
|
264
|
+
true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def handle_bracketed_paste(key)
|
|
140
268
|
paste = read_bracketed_paste(key)
|
|
141
269
|
return false unless paste
|
|
142
270
|
|
|
143
|
-
|
|
271
|
+
yield normalize_paste(paste[:content])
|
|
144
272
|
queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
|
|
145
273
|
true
|
|
146
274
|
end
|
|
147
275
|
|
|
276
|
+
def parse_sgr_mouse_event(key)
|
|
277
|
+
match = key.to_s.match(/\A(?:\e)?\[<(\d+);(\d+);(\d+)([Mm])/)
|
|
278
|
+
return nil unless match
|
|
279
|
+
|
|
280
|
+
code = match[1].to_i
|
|
281
|
+
{
|
|
282
|
+
code: code,
|
|
283
|
+
button: code & 3,
|
|
284
|
+
column: match[2].to_i,
|
|
285
|
+
row: match[3].to_i,
|
|
286
|
+
action: match[4],
|
|
287
|
+
release: match[4] == "m",
|
|
288
|
+
drag: (code & 32).positive?,
|
|
289
|
+
remaining: key.to_s[match[0].length..].to_s
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
|
|
148
293
|
def read_bracketed_paste(key)
|
|
149
294
|
text = key.to_s
|
|
150
295
|
return nil unless text.start_with?(BRACKETED_PASTE_START)
|
|
@@ -174,39 +319,115 @@ module Kward
|
|
|
174
319
|
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
175
320
|
|
|
176
321
|
case code
|
|
322
|
+
when 9
|
|
323
|
+
if ctrl_modifier?(modifier)
|
|
324
|
+
shift_modifier?(modifier) ? { tab_action: :previous } : { tab_action: :next }
|
|
325
|
+
elsif shift_modifier?(modifier)
|
|
326
|
+
handle_reasoning_key_binding(key) || handle_tab_completion_key
|
|
327
|
+
else
|
|
328
|
+
completion_result = handle_completion_provider_key("\t")
|
|
329
|
+
completion_result == false ? handle_reasoning_key_binding("\t") || handle_tab_completion_key : completion_result
|
|
330
|
+
end
|
|
177
331
|
when 13
|
|
178
|
-
modifier == 2
|
|
332
|
+
if modifier == 2
|
|
333
|
+
insert_string("\n")
|
|
334
|
+
elsif file_open_overlay_visible?
|
|
335
|
+
open_selected_file_in_editor(fallback_to_typed_path: true)
|
|
336
|
+
else
|
|
337
|
+
submit_input
|
|
338
|
+
end
|
|
179
339
|
when 27
|
|
180
|
-
dismiss_slash_overlay || false
|
|
340
|
+
dismiss_file_overlay || dismiss_slash_overlay || false
|
|
181
341
|
when 8, 127
|
|
182
342
|
alt_modifier?(modifier) ? delete_word_before_cursor : delete_before_cursor
|
|
183
343
|
nil
|
|
184
344
|
when 4
|
|
185
345
|
delete_at_cursor_or_exit
|
|
186
346
|
else
|
|
187
|
-
handle_modified_csi_u_key(code, modifier)
|
|
347
|
+
handle_modified_csi_u_key(code, modifier) || insert_csi_u_text(sequence)
|
|
188
348
|
end
|
|
189
349
|
end
|
|
190
350
|
|
|
191
351
|
def parse_csi_u_key(key)
|
|
192
|
-
match = key.to_s.match(
|
|
352
|
+
match = key.to_s.match(TerminalKeys::CSI_U_PATTERN)
|
|
193
353
|
return nil unless match
|
|
194
354
|
|
|
195
|
-
|
|
355
|
+
fields = match[2].to_s.split(";", -1)[1..] || []
|
|
356
|
+
modifiers = fields[0].to_s
|
|
196
357
|
modifier = (modifiers.empty? ? "1" : modifiers).split(":", 2).first.to_i
|
|
197
358
|
{
|
|
198
359
|
sequence: match[0],
|
|
199
360
|
code: match[1].to_i,
|
|
200
361
|
modifiers: modifiers,
|
|
201
362
|
modifier: modifier,
|
|
363
|
+
text: fields[1].to_s,
|
|
202
364
|
remaining: key.to_s[match[0].length..]
|
|
203
365
|
}
|
|
204
366
|
end
|
|
205
367
|
|
|
368
|
+
def csi_u_key_event(sequence)
|
|
369
|
+
code = sequence[:code]
|
|
370
|
+
case code
|
|
371
|
+
when 9
|
|
372
|
+
{ type: :tab, modifier: sequence[:modifier] }
|
|
373
|
+
when 13
|
|
374
|
+
{ type: :enter, modifier: sequence[:modifier] }
|
|
375
|
+
when 27
|
|
376
|
+
{ type: :escape, modifier: sequence[:modifier] }
|
|
377
|
+
when 8, 127
|
|
378
|
+
{ type: :backspace, modifier: sequence[:modifier] }
|
|
379
|
+
when 4
|
|
380
|
+
{ type: :delete, modifier: sequence[:modifier] }
|
|
381
|
+
else
|
|
382
|
+
text = csi_u_printable_text(sequence)
|
|
383
|
+
return { type: :printable, text: text, modifier: sequence[:modifier] } if text
|
|
384
|
+
return { type: :text_field, modifier: sequence[:modifier] } if csi_u_text_field?(sequence)
|
|
385
|
+
|
|
386
|
+
{ type: :modified, code: code, modifier: sequence[:modifier] }
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def insert_csi_u_text(sequence)
|
|
391
|
+
event = csi_u_key_event(sequence)
|
|
392
|
+
return true if event[:type] == :text_field
|
|
393
|
+
return false unless event[:type] == :printable
|
|
394
|
+
|
|
395
|
+
insert_string(event[:text])
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def csi_u_text_field?(sequence)
|
|
399
|
+
!sequence[:text].to_s.empty?
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def csi_u_printable_text(sequence)
|
|
403
|
+
text = csi_u_text(sequence)
|
|
404
|
+
return text unless text.empty?
|
|
405
|
+
return nil if csi_u_text_field?(sequence)
|
|
406
|
+
return nil if ctrl_modifier?(sequence[:modifier]) || alt_modifier?(sequence[:modifier]) || super_modifier?(sequence[:modifier])
|
|
407
|
+
return nil unless sequence[:code].between?(32, 126)
|
|
408
|
+
|
|
409
|
+
sequence[:code].chr(Encoding::UTF_8)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def csi_u_text(sequence)
|
|
413
|
+
sequence[:text].to_s.split(":").map do |codepoint|
|
|
414
|
+
character = csi_u_codepoint_character(codepoint)
|
|
415
|
+
return "" unless character
|
|
416
|
+
|
|
417
|
+
character
|
|
418
|
+
end.join
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def csi_u_codepoint_character(codepoint)
|
|
422
|
+
codepoint.to_i.chr(Encoding::UTF_8)
|
|
423
|
+
rescue RangeError
|
|
424
|
+
nil
|
|
425
|
+
end
|
|
426
|
+
|
|
206
427
|
def handle_modified_csi_u_key(code, modifier)
|
|
207
428
|
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
208
429
|
|
|
209
|
-
normalized_code = code
|
|
430
|
+
normalized_code = ctrl_code_for(code)
|
|
210
431
|
if ctrl_modifier?(modifier)
|
|
211
432
|
case normalized_code
|
|
212
433
|
when 97
|
|
@@ -227,6 +448,8 @@ module Kward
|
|
|
227
448
|
kill_line_after_cursor
|
|
228
449
|
when 108
|
|
229
450
|
redraw_screen_locked
|
|
451
|
+
when 114
|
|
452
|
+
start_history_search
|
|
230
453
|
when 117
|
|
231
454
|
kill_line_before_cursor
|
|
232
455
|
when 119
|
|
@@ -252,6 +475,32 @@ module Kward
|
|
|
252
475
|
end
|
|
253
476
|
end
|
|
254
477
|
|
|
478
|
+
def ctrl_code_for(code)
|
|
479
|
+
code.to_i.chr.downcase.ord rescue code
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def key_name_for(key)
|
|
483
|
+
cursor_key_name(key) || @reader.console.keys[key]
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def cursor_key_name(key)
|
|
487
|
+
text = key.to_s
|
|
488
|
+
case text
|
|
489
|
+
when TerminalKeys::UP_PATTERN, *TerminalKeys::UP
|
|
490
|
+
:up
|
|
491
|
+
when TerminalKeys::DOWN_PATTERN, *TerminalKeys::DOWN
|
|
492
|
+
:down
|
|
493
|
+
when TerminalKeys::RIGHT_PATTERN, *TerminalKeys::RIGHT
|
|
494
|
+
:right
|
|
495
|
+
when TerminalKeys::LEFT_PATTERN, *TerminalKeys::LEFT
|
|
496
|
+
:left
|
|
497
|
+
when *TerminalKeys::PAGE_UP
|
|
498
|
+
:pageup
|
|
499
|
+
when *TerminalKeys::PAGE_DOWN
|
|
500
|
+
:pagedown
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
255
504
|
def ctrl_modifier?(modifier)
|
|
256
505
|
((modifier.to_i - 1) & 4).positive?
|
|
257
506
|
end
|
|
@@ -260,6 +509,14 @@ module Kward
|
|
|
260
509
|
((modifier.to_i - 1) & 2).positive?
|
|
261
510
|
end
|
|
262
511
|
|
|
512
|
+
def super_modifier?(modifier)
|
|
513
|
+
((modifier.to_i - 1) & 8).positive?
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def shift_modifier?(modifier)
|
|
517
|
+
((modifier.to_i - 1) & 1).positive?
|
|
518
|
+
end
|
|
519
|
+
|
|
263
520
|
def handle_shift_enter_key(key)
|
|
264
521
|
sequence = shift_enter_sequence_for(key)
|
|
265
522
|
return false unless sequence
|
|
@@ -278,17 +535,28 @@ module Kward
|
|
|
278
535
|
end
|
|
279
536
|
end
|
|
280
537
|
|
|
538
|
+
def handle_bundled_key(key)
|
|
539
|
+
return false unless key.is_a?(String) && key.length > 1
|
|
540
|
+
|
|
541
|
+
token = next_key_token(key)
|
|
542
|
+
return false unless token.length < key.length
|
|
543
|
+
|
|
544
|
+
queue_pending_keys(key[token.length..])
|
|
545
|
+
yield token
|
|
546
|
+
true
|
|
547
|
+
end
|
|
548
|
+
|
|
281
549
|
def next_key_token(keys)
|
|
282
550
|
text = keys.to_s
|
|
283
|
-
text.match(
|
|
284
|
-
text.match(
|
|
551
|
+
text.match(TerminalKeys::CSI_KEY_PATTERN)&.[](0) ||
|
|
552
|
+
text.match(TerminalKeys::SS3_KEY_PATTERN)&.[](0) ||
|
|
285
553
|
shift_enter_sequence_for(text) ||
|
|
286
554
|
(text.start_with?("\e") && text.length > 1 && alt_key_sequence?(text[1]) ? text[0, 2] : text[0, 1])
|
|
287
555
|
end
|
|
288
556
|
|
|
289
557
|
def alt_key_sequence?(char)
|
|
290
558
|
char = char.to_s
|
|
291
|
-
char.match?(/[[:
|
|
559
|
+
char.match?(/[[:alnum:]]/) || char == "\b" || char == "\x7F"
|
|
292
560
|
end
|
|
293
561
|
|
|
294
562
|
def shift_enter_sequence_for(key)
|
|
@@ -310,37 +578,154 @@ module Kward
|
|
|
310
578
|
sequence
|
|
311
579
|
end
|
|
312
580
|
|
|
581
|
+
CTRL_TAB_SEQUENCES = TerminalKeys::CTRL_TAB
|
|
582
|
+
CTRL_SHIFT_TAB_SEQUENCES = TerminalKeys::CTRL_SHIFT_TAB
|
|
583
|
+
SHIFT_TAB_SEQUENCES = TerminalKeys::SHIFT_TAB
|
|
584
|
+
|
|
585
|
+
def handle_completion_provider_key(key)
|
|
586
|
+
return false unless key == "\t" && @completion_provider
|
|
587
|
+
|
|
588
|
+
result = @completion_provider.call(composer_input, composer_cursor)
|
|
589
|
+
return true unless result
|
|
590
|
+
|
|
591
|
+
apply_completion_result(result)
|
|
592
|
+
true
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def apply_completion_result(result)
|
|
596
|
+
range = result[:range] || result["range"] || result.range
|
|
597
|
+
replacement = result[:replacement] || result["replacement"] || result.replacement
|
|
598
|
+
candidates = result[:candidates] || result["candidates"] || result.candidates
|
|
599
|
+
original = composer_input
|
|
600
|
+
before = original[0...range.begin].to_s
|
|
601
|
+
after = original[range.end..].to_s
|
|
602
|
+
self.composer_input = "#{before}#{replacement}#{after}"
|
|
603
|
+
self.composer_cursor = before.length + replacement.to_s.length
|
|
604
|
+
show_completion_candidates(candidates, replacement) if candidates.to_a.length > 1 && replacement.to_s == original[range]
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def show_completion_candidates(candidates, replacement)
|
|
608
|
+
lines = candidates.to_a.first(40)
|
|
609
|
+
text = ["completions:", *lines.map { |candidate| " #{candidate}" }].join("\n")
|
|
610
|
+
write_completion_transcript_locked(text)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def write_completion_transcript_locked(text)
|
|
614
|
+
with_synchronized_output_locked do
|
|
615
|
+
clear_prompt_for_output_locked
|
|
616
|
+
write_transcript_text_locked("\n#{text}\n")
|
|
617
|
+
render_prompt_after_output_locked
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def handle_reasoning_key_binding(key)
|
|
622
|
+
return false if @busy || @select_state || @question_state
|
|
623
|
+
return false if file_overlay_visible? || slash_overlay_visible?
|
|
624
|
+
return false if @slash_overlay_dismissed_input && @slash_overlay_dismissed_input == composer_input
|
|
625
|
+
mention_token = active_file_mention_token
|
|
626
|
+
open_token = active_file_open_token
|
|
627
|
+
return false if mention_token && @file_overlay_dismissed_token == mention_token
|
|
628
|
+
return false if open_token && @file_open_dismissed_token == open_token
|
|
629
|
+
|
|
630
|
+
case key
|
|
631
|
+
when "\t"
|
|
632
|
+
{ reasoning_action: :next }
|
|
633
|
+
when *SHIFT_TAB_SEQUENCES
|
|
634
|
+
{ reasoning_action: :previous }
|
|
635
|
+
else
|
|
636
|
+
false
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def handle_tab_key_binding(key)
|
|
641
|
+
return false if @select_state || @question_state || @tabs.empty?
|
|
642
|
+
|
|
643
|
+
navigation_result = handle_ctrl_tab_navigation_key_binding(key)
|
|
644
|
+
return navigation_result unless navigation_result == false
|
|
645
|
+
|
|
646
|
+
@tab_keybindings == "ctrl" ? handle_ctrl_tab_key_binding(key) : handle_alt_tab_key_binding(key)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def handle_ctrl_tab_navigation_key_binding(key)
|
|
650
|
+
case key
|
|
651
|
+
when *CTRL_TAB_SEQUENCES
|
|
652
|
+
{ tab_action: :next }
|
|
653
|
+
when *CTRL_SHIFT_TAB_SEQUENCES
|
|
654
|
+
{ tab_action: :previous }
|
|
655
|
+
else
|
|
656
|
+
false
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def handle_ctrl_tab_key_binding(key)
|
|
661
|
+
case key
|
|
662
|
+
when TerminalKeys::CTRL_T, TerminalKeys::CTRL_T_CSI_U
|
|
663
|
+
{ tab_action: :new }
|
|
664
|
+
when TerminalKeys::CTRL_W_CSI_U
|
|
665
|
+
{ tab_action: :close }
|
|
666
|
+
else
|
|
667
|
+
ctrl_number_tab_action(key)
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def ctrl_number_tab_action(key)
|
|
672
|
+
match = key.to_s.match(TerminalKeys::CTRL_NUMBER_TAB_PATTERN)
|
|
673
|
+
return false unless match
|
|
674
|
+
|
|
675
|
+
{ tab_action: :select, index: match[1].to_i - 49 }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def handle_alt_tab_key_binding(key)
|
|
679
|
+
case key
|
|
680
|
+
when "\et", "\eT"
|
|
681
|
+
{ tab_action: :new }
|
|
682
|
+
when *TerminalKeys::ALT_RIGHT
|
|
683
|
+
{ tab_action: :next }
|
|
684
|
+
when *TerminalKeys::ALT_LEFT
|
|
685
|
+
{ tab_action: :previous }
|
|
686
|
+
else
|
|
687
|
+
alt_number_tab_action(key)
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def alt_number_tab_action(key)
|
|
692
|
+
match = key.to_s.match(/\A\e([1-9])\z/)
|
|
693
|
+
return false unless match
|
|
694
|
+
|
|
695
|
+
{ tab_action: :select, index: match[1].to_i - 1 }
|
|
696
|
+
end
|
|
697
|
+
|
|
313
698
|
def handle_composer_key_binding(key)
|
|
314
699
|
case key
|
|
315
|
-
when
|
|
700
|
+
when TerminalKeys::CTRL_A
|
|
316
701
|
move_to_start_of_line
|
|
317
|
-
when
|
|
702
|
+
when TerminalKeys::CTRL_B
|
|
318
703
|
move_cursor_left
|
|
319
|
-
when
|
|
704
|
+
when TerminalKeys::CTRL_D
|
|
320
705
|
delete_at_cursor_or_exit
|
|
321
|
-
when
|
|
706
|
+
when TerminalKeys::CTRL_E
|
|
322
707
|
move_to_end_of_line
|
|
323
|
-
when
|
|
708
|
+
when TerminalKeys::CTRL_F
|
|
324
709
|
move_cursor_right
|
|
325
|
-
when
|
|
710
|
+
when TerminalKeys::CTRL_K
|
|
326
711
|
kill_line_after_cursor
|
|
327
|
-
when
|
|
712
|
+
when TerminalKeys::CTRL_L
|
|
328
713
|
redraw_screen_locked
|
|
329
|
-
when
|
|
714
|
+
when TerminalKeys::CTRL_U
|
|
330
715
|
kill_line_before_cursor
|
|
331
|
-
when
|
|
716
|
+
when TerminalKeys::CTRL_W
|
|
332
717
|
delete_word_before_cursor
|
|
333
|
-
when
|
|
718
|
+
when TerminalKeys::CTRL_Y
|
|
334
719
|
yank_kill_buffer
|
|
335
|
-
when
|
|
720
|
+
when *TerminalKeys::LEFT
|
|
336
721
|
move_cursor_left
|
|
337
|
-
when
|
|
722
|
+
when *TerminalKeys::RIGHT
|
|
338
723
|
move_cursor_right
|
|
339
|
-
when
|
|
724
|
+
when *TerminalKeys::HOME
|
|
340
725
|
move_to_start_of_line
|
|
341
|
-
when
|
|
726
|
+
when *TerminalKeys::END_KEY
|
|
342
727
|
move_to_end_of_line
|
|
343
|
-
when
|
|
728
|
+
when *TerminalKeys::DELETE
|
|
344
729
|
delete_at_cursor
|
|
345
730
|
when "\eb", "\eB"
|
|
346
731
|
move_to_previous_word
|
|
@@ -355,14 +740,23 @@ module Kward
|
|
|
355
740
|
end
|
|
356
741
|
end
|
|
357
742
|
|
|
743
|
+
def parse_modified_ansi_key(key)
|
|
744
|
+
if (match = key.to_s.match(TerminalKeys::MODIFIED_CURSOR_PATTERN))
|
|
745
|
+
{ type: :cursor, modifier: match[2].to_i, final: match[3] }
|
|
746
|
+
elsif (match = key.to_s.match(TerminalKeys::MODIFIED_DELETE_PATTERN))
|
|
747
|
+
{ type: :delete, modifier: match[1].to_i }
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
358
751
|
def handle_modified_ansi_key(key)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
modifier = match[2].to_i
|
|
362
|
-
final = match[3]
|
|
363
|
-
return false unless alt_modifier?(modifier)
|
|
752
|
+
sequence = parse_modified_ansi_key(key)
|
|
753
|
+
return false unless sequence
|
|
364
754
|
|
|
365
|
-
|
|
755
|
+
case sequence[:type]
|
|
756
|
+
when :cursor
|
|
757
|
+
return false unless alt_modifier?(sequence[:modifier])
|
|
758
|
+
|
|
759
|
+
case sequence[:final]
|
|
366
760
|
when "C"
|
|
367
761
|
move_to_next_word
|
|
368
762
|
when "D"
|
|
@@ -374,8 +768,8 @@ module Kward
|
|
|
374
768
|
else
|
|
375
769
|
false
|
|
376
770
|
end
|
|
377
|
-
|
|
378
|
-
alt_modifier?(
|
|
771
|
+
when :delete
|
|
772
|
+
alt_modifier?(sequence[:modifier]) ? delete_word_after_cursor : delete_at_cursor
|
|
379
773
|
else
|
|
380
774
|
false
|
|
381
775
|
end
|