kward 0.70.0 → 0.72.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/pages.yml +1 -1
- data/CHANGELOG.md +89 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +34 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +52 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +58 -23
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +102 -13
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +83 -0
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +74 -3
- data/doc/releasing.md +45 -8
- data/doc/rpc.md +77 -15
- data/doc/session-management.md +254 -0
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +60 -15
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +144 -0
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +41 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +111 -6
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +262 -13
- data/lib/kward/cli/settings.rb +216 -37
- data/lib/kward/cli/slash_commands.rb +439 -8
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +171 -26
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +125 -5
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +59 -22
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/model/client.rb +37 -50
- data/lib/kward/model/context_usage.rb +13 -6
- data/lib/kward/model/model_info.rb +92 -16
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +108 -1
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +124 -83
- data/lib/kward/prompt_interface/composer_renderer.rb +116 -14
- 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 +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -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 +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -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 +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -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 +299 -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 +416 -43
- data/lib/kward/prompt_interface/layout.rb +2 -2
- 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/prompt_renderer.rb +32 -13
- data/lib/kward/prompt_interface/question_prompt.rb +122 -82
- data/lib/kward/prompt_interface/runtime_state.rb +49 -1
- data/lib/kward/prompt_interface/screen.rb +17 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +511 -58
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +13 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +307 -35
- data/lib/kward/prompts/commands.rb +7 -1
- data/lib/kward/prompts.rb +4 -2
- data/lib/kward/rpc/server.rb +45 -11
- data/lib/kward/rpc/session_manager.rb +52 -53
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +67 -4
- 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/telemetry/logger.rb +5 -3
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +92 -15
- data/lib/kward/tools/retrieve_tool_output.rb +71 -0
- data/lib/kward/tools/search/web.rb +2 -2
- data/lib/kward/tools/summarize_file_structure.rb +29 -0
- data/lib/kward/tools/tool_call.rb +12 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -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 +7 -0
- data/lib/kward/workspace.rb +154 -12
- data/templates/default/fulldoc/html/css/kward.css +362 -42
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +161 -2
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +102 -0
- data/templates/default/layout/html/layout.erb +43 -10
- data/templates/default/layout/html/setup.rb +39 -38
- metadata +65 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
|
@@ -10,6 +10,8 @@ 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
|
"\x03"
|
|
@@ -17,28 +19,43 @@ module Kward
|
|
|
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
119
|
when "\x04"
|
|
89
120
|
delete_at_cursor_or_exit
|
|
90
121
|
when "\x03"
|
|
91
122
|
cancel_input_or_interrupt
|
|
123
|
+
when "\x12"
|
|
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 "\x03", "\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
|
raise Interrupt
|
|
104
197
|
end
|
|
105
198
|
|
|
199
|
+
def handle_tab_completion_key
|
|
200
|
+
open_selected_file_in_editor || complete_selected_file_mention || complete_selected_slash_command || insert_key("\t")
|
|
201
|
+
end
|
|
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,8 +253,26 @@ module Kward
|
|
|
137
253
|
end
|
|
138
254
|
|
|
139
255
|
def handle_bracketed_paste_key(key)
|
|
256
|
+
paste = read_bracketed_paste(key)
|
|
257
|
+
return false unless paste
|
|
258
|
+
|
|
259
|
+
insert_paste(normalize_paste(paste[:content]))
|
|
260
|
+
queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
|
|
261
|
+
true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def handle_mouse_reporting_key(key)
|
|
265
|
+
text = key.to_s
|
|
266
|
+
match = text.match(/\A(?:\e)?\[<\d+;\d+;\d+[Mm]/)
|
|
267
|
+
return false unless match
|
|
268
|
+
|
|
269
|
+
queue_pending_keys(text[match[0].length..]) if match[0].length < text.length
|
|
270
|
+
true
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def read_bracketed_paste(key)
|
|
140
274
|
text = key.to_s
|
|
141
|
-
return
|
|
275
|
+
return nil unless text.start_with?(BRACKETED_PASTE_START)
|
|
142
276
|
|
|
143
277
|
pasted = text[BRACKETED_PASTE_START.length..] || ""
|
|
144
278
|
until pasted.include?(BRACKETED_PASTE_END)
|
|
@@ -149,9 +283,7 @@ module Kward
|
|
|
149
283
|
end
|
|
150
284
|
|
|
151
285
|
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
152
|
-
|
|
153
|
-
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
154
|
-
true
|
|
286
|
+
{ content: content || "", remaining: remaining }
|
|
155
287
|
end
|
|
156
288
|
|
|
157
289
|
def normalize_paste(content)
|
|
@@ -159,33 +291,101 @@ module Kward
|
|
|
159
291
|
end
|
|
160
292
|
|
|
161
293
|
def handle_csi_u_key(key)
|
|
162
|
-
|
|
163
|
-
return false unless
|
|
294
|
+
sequence = parse_csi_u_key(key)
|
|
295
|
+
return false unless sequence
|
|
164
296
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
297
|
+
code = sequence[:code]
|
|
298
|
+
modifier = sequence[:modifier]
|
|
299
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
169
300
|
|
|
170
301
|
case code
|
|
302
|
+
when 9
|
|
303
|
+
if ctrl_modifier?(modifier)
|
|
304
|
+
shift_modifier?(modifier) ? { tab_action: :previous } : { tab_action: :next }
|
|
305
|
+
elsif shift_modifier?(modifier)
|
|
306
|
+
handle_reasoning_key_binding(key) || handle_tab_completion_key
|
|
307
|
+
else
|
|
308
|
+
completion_result = handle_completion_provider_key("\t")
|
|
309
|
+
completion_result == false ? handle_reasoning_key_binding("\t") || handle_tab_completion_key : completion_result
|
|
310
|
+
end
|
|
171
311
|
when 13
|
|
172
|
-
modifier == 2
|
|
312
|
+
if modifier == 2
|
|
313
|
+
insert_string("\n")
|
|
314
|
+
elsif file_open_overlay_visible?
|
|
315
|
+
open_selected_file_in_editor(fallback_to_typed_path: true)
|
|
316
|
+
else
|
|
317
|
+
submit_input
|
|
318
|
+
end
|
|
173
319
|
when 27
|
|
174
|
-
dismiss_slash_overlay || false
|
|
320
|
+
dismiss_file_overlay || dismiss_slash_overlay || false
|
|
175
321
|
when 8, 127
|
|
176
322
|
alt_modifier?(modifier) ? delete_word_before_cursor : delete_before_cursor
|
|
177
323
|
nil
|
|
178
324
|
when 4
|
|
179
325
|
delete_at_cursor_or_exit
|
|
180
326
|
else
|
|
181
|
-
handle_modified_csi_u_key(code, modifier)
|
|
327
|
+
handle_modified_csi_u_key(code, modifier) || insert_csi_u_text(sequence)
|
|
182
328
|
end
|
|
183
329
|
end
|
|
184
330
|
|
|
331
|
+
def parse_csi_u_key(key)
|
|
332
|
+
match = key.to_s.match(/\A\e\[(\d+)((?:;[\d:]*)*)u/)
|
|
333
|
+
return nil unless match
|
|
334
|
+
|
|
335
|
+
fields = match[2].to_s.split(";", -1)[1..] || []
|
|
336
|
+
modifiers = fields[0].to_s
|
|
337
|
+
modifier = (modifiers.empty? ? "1" : modifiers).split(":", 2).first.to_i
|
|
338
|
+
{
|
|
339
|
+
sequence: match[0],
|
|
340
|
+
code: match[1].to_i,
|
|
341
|
+
modifiers: modifiers,
|
|
342
|
+
modifier: modifier,
|
|
343
|
+
text: fields[1].to_s,
|
|
344
|
+
remaining: key.to_s[match[0].length..]
|
|
345
|
+
}
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def insert_csi_u_text(sequence)
|
|
349
|
+
text = csi_u_printable_text(sequence)
|
|
350
|
+
return true if text.nil? && csi_u_text_field?(sequence)
|
|
351
|
+
return false unless text
|
|
352
|
+
|
|
353
|
+
insert_string(text)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def csi_u_text_field?(sequence)
|
|
357
|
+
!sequence[:text].to_s.empty?
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def csi_u_printable_text(sequence)
|
|
361
|
+
text = csi_u_text(sequence)
|
|
362
|
+
return text unless text.empty?
|
|
363
|
+
return nil if csi_u_text_field?(sequence)
|
|
364
|
+
return nil if ctrl_modifier?(sequence[:modifier]) || alt_modifier?(sequence[:modifier]) || super_modifier?(sequence[:modifier])
|
|
365
|
+
return nil unless sequence[:code].between?(32, 126)
|
|
366
|
+
|
|
367
|
+
sequence[:code].chr(Encoding::UTF_8)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def csi_u_text(sequence)
|
|
371
|
+
sequence[:text].to_s.split(":").map do |codepoint|
|
|
372
|
+
character = csi_u_codepoint_character(codepoint)
|
|
373
|
+
return "" unless character
|
|
374
|
+
|
|
375
|
+
character
|
|
376
|
+
end.join
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def csi_u_codepoint_character(codepoint)
|
|
380
|
+
codepoint.to_i.chr(Encoding::UTF_8)
|
|
381
|
+
rescue RangeError
|
|
382
|
+
nil
|
|
383
|
+
end
|
|
384
|
+
|
|
185
385
|
def handle_modified_csi_u_key(code, modifier)
|
|
186
386
|
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
187
387
|
|
|
188
|
-
normalized_code = code
|
|
388
|
+
normalized_code = ctrl_code_for(code)
|
|
189
389
|
if ctrl_modifier?(modifier)
|
|
190
390
|
case normalized_code
|
|
191
391
|
when 97
|
|
@@ -206,6 +406,8 @@ module Kward
|
|
|
206
406
|
kill_line_after_cursor
|
|
207
407
|
when 108
|
|
208
408
|
redraw_screen_locked
|
|
409
|
+
when 114
|
|
410
|
+
start_history_search
|
|
209
411
|
when 117
|
|
210
412
|
kill_line_before_cursor
|
|
211
413
|
when 119
|
|
@@ -231,6 +433,32 @@ module Kward
|
|
|
231
433
|
end
|
|
232
434
|
end
|
|
233
435
|
|
|
436
|
+
def ctrl_code_for(code)
|
|
437
|
+
code.to_i.chr.downcase.ord rescue code
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def key_name_for(key)
|
|
441
|
+
cursor_key_name(key) || @reader.console.keys[key]
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def cursor_key_name(key)
|
|
445
|
+
text = key.to_s
|
|
446
|
+
case text
|
|
447
|
+
when /\A\e\[[0-9;:]*A\z/, "\eOA"
|
|
448
|
+
:up
|
|
449
|
+
when /\A\e\[[0-9;:]*B\z/, "\eOB"
|
|
450
|
+
:down
|
|
451
|
+
when /\A\e\[[0-9;:]*C\z/, "\eOC"
|
|
452
|
+
:right
|
|
453
|
+
when /\A\e\[[0-9;:]*D\z/, "\eOD"
|
|
454
|
+
:left
|
|
455
|
+
when "\e[5~"
|
|
456
|
+
:pageup
|
|
457
|
+
when "\e[6~"
|
|
458
|
+
:pagedown
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
234
462
|
def ctrl_modifier?(modifier)
|
|
235
463
|
((modifier.to_i - 1) & 4).positive?
|
|
236
464
|
end
|
|
@@ -239,6 +467,14 @@ module Kward
|
|
|
239
467
|
((modifier.to_i - 1) & 2).positive?
|
|
240
468
|
end
|
|
241
469
|
|
|
470
|
+
def super_modifier?(modifier)
|
|
471
|
+
((modifier.to_i - 1) & 8).positive?
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def shift_modifier?(modifier)
|
|
475
|
+
((modifier.to_i - 1) & 1).positive?
|
|
476
|
+
end
|
|
477
|
+
|
|
242
478
|
def handle_shift_enter_key(key)
|
|
243
479
|
sequence = shift_enter_sequence_for(key)
|
|
244
480
|
return false unless sequence
|
|
@@ -257,6 +493,17 @@ module Kward
|
|
|
257
493
|
end
|
|
258
494
|
end
|
|
259
495
|
|
|
496
|
+
def handle_bundled_key(key)
|
|
497
|
+
return false unless key.is_a?(String) && key.length > 1
|
|
498
|
+
|
|
499
|
+
token = next_key_token(key)
|
|
500
|
+
return false unless token.length < key.length
|
|
501
|
+
|
|
502
|
+
queue_pending_keys(key[token.length..])
|
|
503
|
+
yield token
|
|
504
|
+
true
|
|
505
|
+
end
|
|
506
|
+
|
|
260
507
|
def next_key_token(keys)
|
|
261
508
|
text = keys.to_s
|
|
262
509
|
text.match(/\A\e\[[0-9;:]*[A-Za-z~]/)&.[](0) ||
|
|
@@ -267,7 +514,7 @@ module Kward
|
|
|
267
514
|
|
|
268
515
|
def alt_key_sequence?(char)
|
|
269
516
|
char = char.to_s
|
|
270
|
-
char.match?(/[[:
|
|
517
|
+
char.match?(/[[:alnum:]]/) || char == "\b" || char == "\x7F"
|
|
271
518
|
end
|
|
272
519
|
|
|
273
520
|
def shift_enter_sequence_for(key)
|
|
@@ -289,6 +536,123 @@ module Kward
|
|
|
289
536
|
sequence
|
|
290
537
|
end
|
|
291
538
|
|
|
539
|
+
CTRL_TAB_SEQUENCES = ["\e[9;5u", "\e[27;5;9~", "\e[1;5I"].freeze
|
|
540
|
+
CTRL_SHIFT_TAB_SEQUENCES = ["\e[9;6u", "\e[27;6;9~", "\e[1;6I", "\e[1;6Z"].freeze
|
|
541
|
+
SHIFT_TAB_SEQUENCES = ["\e[Z", "\e[1;2Z", "\e[9;2u", "\e[27;2;9~", "\e[1;2I"].freeze
|
|
542
|
+
|
|
543
|
+
def handle_completion_provider_key(key)
|
|
544
|
+
return false unless key == "\t" && @completion_provider
|
|
545
|
+
|
|
546
|
+
result = @completion_provider.call(composer_input, composer_cursor)
|
|
547
|
+
return true unless result
|
|
548
|
+
|
|
549
|
+
apply_completion_result(result)
|
|
550
|
+
true
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def apply_completion_result(result)
|
|
554
|
+
range = result[:range] || result["range"] || result.range
|
|
555
|
+
replacement = result[:replacement] || result["replacement"] || result.replacement
|
|
556
|
+
candidates = result[:candidates] || result["candidates"] || result.candidates
|
|
557
|
+
original = composer_input
|
|
558
|
+
before = original[0...range.begin].to_s
|
|
559
|
+
after = original[range.end..].to_s
|
|
560
|
+
self.composer_input = "#{before}#{replacement}#{after}"
|
|
561
|
+
self.composer_cursor = before.length + replacement.to_s.length
|
|
562
|
+
show_completion_candidates(candidates, replacement) if candidates.to_a.length > 1 && replacement.to_s == original[range]
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def show_completion_candidates(candidates, replacement)
|
|
566
|
+
lines = candidates.to_a.first(40)
|
|
567
|
+
text = ["completions:", *lines.map { |candidate| " #{candidate}" }].join("\n")
|
|
568
|
+
write_completion_transcript_locked(text)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def write_completion_transcript_locked(text)
|
|
572
|
+
with_synchronized_output_locked do
|
|
573
|
+
clear_prompt_for_output_locked
|
|
574
|
+
write_transcript_text_locked("\n#{text}\n")
|
|
575
|
+
render_prompt_after_output_locked
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def handle_reasoning_key_binding(key)
|
|
580
|
+
return false if @busy || @select_state || @question_state
|
|
581
|
+
return false if file_overlay_visible? || slash_overlay_visible?
|
|
582
|
+
return false if @slash_overlay_dismissed_input && @slash_overlay_dismissed_input == composer_input
|
|
583
|
+
mention_token = active_file_mention_token
|
|
584
|
+
open_token = active_file_open_token
|
|
585
|
+
return false if mention_token && @file_overlay_dismissed_token == mention_token
|
|
586
|
+
return false if open_token && @file_open_dismissed_token == open_token
|
|
587
|
+
|
|
588
|
+
case key
|
|
589
|
+
when "\t"
|
|
590
|
+
{ reasoning_action: :next }
|
|
591
|
+
when *SHIFT_TAB_SEQUENCES
|
|
592
|
+
{ reasoning_action: :previous }
|
|
593
|
+
else
|
|
594
|
+
false
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def handle_tab_key_binding(key)
|
|
599
|
+
return false if @select_state || @question_state || @tabs.empty?
|
|
600
|
+
|
|
601
|
+
navigation_result = handle_ctrl_tab_navigation_key_binding(key)
|
|
602
|
+
return navigation_result unless navigation_result == false
|
|
603
|
+
|
|
604
|
+
@tab_keybindings == "ctrl" ? handle_ctrl_tab_key_binding(key) : handle_alt_tab_key_binding(key)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def handle_ctrl_tab_navigation_key_binding(key)
|
|
608
|
+
case key
|
|
609
|
+
when *CTRL_TAB_SEQUENCES
|
|
610
|
+
{ tab_action: :next }
|
|
611
|
+
when *CTRL_SHIFT_TAB_SEQUENCES
|
|
612
|
+
{ tab_action: :previous }
|
|
613
|
+
else
|
|
614
|
+
false
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def handle_ctrl_tab_key_binding(key)
|
|
619
|
+
case key
|
|
620
|
+
when "\x14", "\e[116;5u"
|
|
621
|
+
{ tab_action: :new }
|
|
622
|
+
when "\e[119;5u"
|
|
623
|
+
{ tab_action: :close }
|
|
624
|
+
else
|
|
625
|
+
ctrl_number_tab_action(key)
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def ctrl_number_tab_action(key)
|
|
630
|
+
match = key.to_s.match(/\A\e\[((?:49)|(?:5[0-7]));5u\z/)
|
|
631
|
+
return false unless match
|
|
632
|
+
|
|
633
|
+
{ tab_action: :select, index: match[1].to_i - 49 }
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def handle_alt_tab_key_binding(key)
|
|
637
|
+
case key
|
|
638
|
+
when "\et", "\eT"
|
|
639
|
+
{ tab_action: :new }
|
|
640
|
+
when "\e[1;3C", "\e[3C"
|
|
641
|
+
{ tab_action: :next }
|
|
642
|
+
when "\e[1;3D", "\e[3D"
|
|
643
|
+
{ tab_action: :previous }
|
|
644
|
+
else
|
|
645
|
+
alt_number_tab_action(key)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def alt_number_tab_action(key)
|
|
650
|
+
match = key.to_s.match(/\A\e([1-9])\z/)
|
|
651
|
+
return false unless match
|
|
652
|
+
|
|
653
|
+
{ tab_action: :select, index: match[1].to_i - 1 }
|
|
654
|
+
end
|
|
655
|
+
|
|
292
656
|
def handle_composer_key_binding(key)
|
|
293
657
|
case key
|
|
294
658
|
when "\x01"
|
|
@@ -334,14 +698,23 @@ module Kward
|
|
|
334
698
|
end
|
|
335
699
|
end
|
|
336
700
|
|
|
701
|
+
def parse_modified_ansi_key(key)
|
|
702
|
+
if (match = key.to_s.match(/\A\e\[(\d+);(\d+)([CDFH])\z/))
|
|
703
|
+
{ type: :cursor, modifier: match[2].to_i, final: match[3] }
|
|
704
|
+
elsif (match = key.to_s.match(/\A\e\[3;(\d+)~\z/))
|
|
705
|
+
{ type: :delete, modifier: match[1].to_i }
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
337
709
|
def handle_modified_ansi_key(key)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
710
|
+
sequence = parse_modified_ansi_key(key)
|
|
711
|
+
return false unless sequence
|
|
712
|
+
|
|
713
|
+
case sequence[:type]
|
|
714
|
+
when :cursor
|
|
715
|
+
return false unless alt_modifier?(sequence[:modifier])
|
|
343
716
|
|
|
344
|
-
case final
|
|
717
|
+
case sequence[:final]
|
|
345
718
|
when "C"
|
|
346
719
|
move_to_next_word
|
|
347
720
|
when "D"
|
|
@@ -353,8 +726,8 @@ module Kward
|
|
|
353
726
|
else
|
|
354
727
|
false
|
|
355
728
|
end
|
|
356
|
-
|
|
357
|
-
alt_modifier?(
|
|
729
|
+
when :delete
|
|
730
|
+
alt_modifier?(sequence[:modifier]) ? delete_word_after_cursor : delete_at_cursor
|
|
358
731
|
else
|
|
359
732
|
false
|
|
360
733
|
end
|
|
@@ -9,10 +9,29 @@ module Kward
|
|
|
9
9
|
def active_overlay_rows(width, height: screen_height)
|
|
10
10
|
return question_overlay_rows(width) if @question_state
|
|
11
11
|
return selection_overlay_rows(width, height: height) if @select_state
|
|
12
|
+
return git_overlay_rows(width, height: height) if @git_state
|
|
13
|
+
return project_browser_rows(width, height: height) if project_browser_visible?
|
|
14
|
+
return history_search_overlay_rows(width, height: height) if history_search_active?
|
|
15
|
+
return file_overlay_rows(width, height: height) if file_overlay_visible?
|
|
12
16
|
|
|
13
17
|
slash_overlay_rows(width, height: height)
|
|
14
18
|
end
|
|
15
19
|
|
|
20
|
+
def history_search_overlay_rows(width, height: screen_height)
|
|
21
|
+
matches = history_search_matches
|
|
22
|
+
if matches.empty?
|
|
23
|
+
return overlay_card_rows("History", [overlay_text_line("No matching history", :muted)], width)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
max_rows = max_overlay_list_rows(height)
|
|
27
|
+
selected = @composer.history_search_index
|
|
28
|
+
start = centered_list_window_start(selected, matches.length, max_rows)
|
|
29
|
+
rows = (matches[start, max_rows] || []).each_with_index.map do |value, offset|
|
|
30
|
+
overlay_choice_line(value, selected: start + offset == selected)
|
|
31
|
+
end
|
|
32
|
+
overlay_card_rows("History", rows, width)
|
|
33
|
+
end
|
|
34
|
+
|
|
16
35
|
def overlay_card_rows(title, content_rows, width)
|
|
17
36
|
card_width = overlay_card_width(width)
|
|
18
37
|
inner_width = [card_width - 4, 1].max
|
|
@@ -32,7 +51,7 @@ module Kward
|
|
|
32
51
|
def overlay_top_border(title, card_width)
|
|
33
52
|
title = visible_truncate(title.to_s, [card_width - 4, 1].max)
|
|
34
53
|
plain_length = ANSI.strip(title).length
|
|
35
|
-
colored("╭", :primary_green) + " #{colored(title, :
|
|
54
|
+
colored("╭", :primary_green) + " #{colored(title, :primary_green, :bold)} " + colored("─" * [card_width - plain_length - 4, 0].max, :primary_green) + colored("╮", :primary_green)
|
|
36
55
|
end
|
|
37
56
|
|
|
38
57
|
def overlay_bottom_border(card_width)
|
|
@@ -41,7 +60,7 @@ module Kward
|
|
|
41
60
|
|
|
42
61
|
def overlay_content_row(row, inner_width)
|
|
43
62
|
text = visible_truncate(row[:text], inner_width)
|
|
44
|
-
text = colored(text, :
|
|
63
|
+
text = colored(text, :primary_green, :bold) if row[:selected]
|
|
45
64
|
colored("│", :primary_green) + " " + visible_ljust(text, inner_width) + " " + colored("│", :primary_green)
|
|
46
65
|
end
|
|
47
66
|
|