kward 0.70.0 → 0.71.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 +48 -2
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +30 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +43 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +39 -25
- data/doc/configuration.md +1 -15
- data/doc/context-tools.md +70 -0
- data/doc/plugins.md +2 -2
- data/doc/releasing.md +14 -5
- data/doc/rpc.md +3 -11
- data/doc/session-management.md +220 -0
- data/doc/usage.md +7 -8
- data/doc/workspace-tools.md +105 -0
- data/lib/kward/cli/commands.rb +8 -0
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/prompt_interface.rb +80 -6
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/sessions.rb +260 -11
- data/lib/kward/cli/settings.rb +0 -30
- data/lib/kward/cli/slash_commands.rb +24 -6
- data/lib/kward/cli.rb +13 -0
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +4 -6
- data/lib/kward/conversation.rb +49 -20
- 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 +47 -1
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +60 -87
- data/lib/kward/prompt_interface/composer_renderer.rb +7 -1
- data/lib/kward/prompt_interface/key_handler.rb +31 -10
- data/lib/kward/prompt_interface/layout.rb +2 -2
- data/lib/kward/prompt_interface/prompt_renderer.rb +32 -13
- data/lib/kward/prompt_interface/question_prompt.rb +34 -42
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +1 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +513 -54
- data/lib/kward/prompt_interface/transcript_buffer.rb +7 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +22 -28
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/prompts.rb +2 -2
- data/lib/kward/rpc/server.rb +3 -8
- data/lib/kward/rpc/session_manager.rb +17 -6
- data/lib/kward/session_store.rb +23 -4
- data/lib/kward/telemetry/logger.rb +5 -3
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/registry.rb +37 -6
- 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 +2 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +58 -2
- data/templates/default/fulldoc/html/css/kward.css +256 -7
- 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 +91 -0
- data/templates/default/layout/html/layout.erb +39 -8
- data/templates/default/layout/html/setup.rb +33 -38
- metadata +13 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
|
@@ -10,9 +10,6 @@ module Kward
|
|
|
10
10
|
return select_current_choice if key.nil?
|
|
11
11
|
return if handle_select_bracketed_paste_key(key)
|
|
12
12
|
|
|
13
|
-
csi_result = handle_select_csi_u_key(key)
|
|
14
|
-
return csi_result unless csi_result == false
|
|
15
|
-
|
|
16
13
|
if key.is_a?(String) && key.length > 1
|
|
17
14
|
token = next_key_token(key)
|
|
18
15
|
if token.length < key.length
|
|
@@ -21,22 +18,32 @@ module Kward
|
|
|
21
18
|
end
|
|
22
19
|
end
|
|
23
20
|
|
|
21
|
+
return handle_select_confirmation_key(key) if select_confirmation_active?
|
|
22
|
+
|
|
23
|
+
csi_result = handle_select_csi_u_key(key)
|
|
24
|
+
return csi_result unless csi_result == false
|
|
25
|
+
|
|
26
|
+
return handle_select_input_key(key) if select_input_active?
|
|
27
|
+
|
|
28
|
+
binding_result = handle_select_search_key_binding(key)
|
|
29
|
+
return binding_result unless binding_result == false
|
|
30
|
+
|
|
24
31
|
key_name = @reader.console.keys[key]
|
|
25
32
|
case key_name
|
|
26
33
|
when :return, :enter
|
|
27
34
|
select_current_choice
|
|
28
35
|
when :backspace
|
|
29
|
-
select_delete_before_cursor
|
|
36
|
+
select_delete_before_cursor if select_search_active?
|
|
30
37
|
when :delete
|
|
31
|
-
select_delete_at_cursor
|
|
38
|
+
select_delete_at_cursor if select_search_active?
|
|
32
39
|
when :left
|
|
33
|
-
|
|
40
|
+
select_move_cursor_left if select_search_active?
|
|
34
41
|
when :right
|
|
35
|
-
|
|
42
|
+
select_move_cursor_right if select_search_active?
|
|
36
43
|
when :home
|
|
37
|
-
self.composer_cursor = 0
|
|
44
|
+
self.composer_cursor = 0 if select_search_active?
|
|
38
45
|
when :end
|
|
39
|
-
self.composer_cursor = composer_input.length
|
|
46
|
+
self.composer_cursor = composer_input.length if select_search_active?
|
|
40
47
|
when :up
|
|
41
48
|
select_previous_choice
|
|
42
49
|
when :down
|
|
@@ -46,69 +53,111 @@ module Kward
|
|
|
46
53
|
when "\n", "\r"
|
|
47
54
|
select_current_choice
|
|
48
55
|
when "\b", "\x7F"
|
|
49
|
-
select_delete_before_cursor
|
|
56
|
+
select_delete_before_cursor if select_search_active?
|
|
50
57
|
when "\e"
|
|
51
58
|
handle_select_escape_sequence
|
|
52
59
|
else
|
|
53
|
-
|
|
60
|
+
select_typed_key(key)
|
|
54
61
|
end
|
|
55
62
|
end
|
|
56
63
|
end
|
|
57
64
|
|
|
65
|
+
def drain_pending_select_keys_locked(result)
|
|
66
|
+
until result.is_a?(String) || select_action_result?(result) || result == SELECT_CANCEL || @pending_keys.empty?
|
|
67
|
+
result = handle_select_key(@pending_keys.shift)
|
|
68
|
+
end
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
def handle_select_csi_u_key(key)
|
|
59
|
-
|
|
60
|
-
return false unless
|
|
73
|
+
sequence = parse_csi_u_key(key)
|
|
74
|
+
return false unless sequence
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
code = sequence[:code]
|
|
77
|
+
modifiers = sequence[:modifiers]
|
|
78
|
+
modifier = sequence[:modifier]
|
|
79
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
65
80
|
|
|
66
81
|
case code
|
|
67
82
|
when 13
|
|
68
|
-
select_current_choice
|
|
83
|
+
select_input_active? ? select_input_action_result : select_current_choice
|
|
69
84
|
when 27
|
|
70
|
-
|
|
85
|
+
if select_input_active?
|
|
86
|
+
clear_select_input
|
|
87
|
+
elsif select_search_active?
|
|
88
|
+
select_cancel_search
|
|
89
|
+
else
|
|
90
|
+
SELECT_CANCEL
|
|
91
|
+
end
|
|
71
92
|
when 8, 127
|
|
72
|
-
|
|
93
|
+
if select_editing_active?
|
|
94
|
+
alt_modifier?(modifier) ? select_delete_word_before_cursor : select_delete_before_cursor
|
|
95
|
+
end
|
|
96
|
+
nil
|
|
97
|
+
when 4
|
|
98
|
+
select_delete_at_cursor if select_editing_active?
|
|
73
99
|
nil
|
|
100
|
+
else
|
|
101
|
+
modified_result = handle_select_modified_csi_u_key(code, modifier)
|
|
102
|
+
return modified_result unless modified_result == false
|
|
103
|
+
|
|
104
|
+
handle_select_printable_csi_u_key(code, modifiers)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_select_modified_csi_u_key(code, modifier)
|
|
109
|
+
return false unless select_editing_active?
|
|
110
|
+
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
111
|
+
|
|
112
|
+
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
113
|
+
if ctrl_modifier?(modifier)
|
|
114
|
+
handle_select_ctrl_key(normalized_code)
|
|
115
|
+
elsif alt_modifier?(modifier)
|
|
116
|
+
handle_select_alt_key(normalized_code)
|
|
74
117
|
else
|
|
75
118
|
false
|
|
76
119
|
end
|
|
77
120
|
end
|
|
78
121
|
|
|
122
|
+
def handle_select_printable_csi_u_key(code, modifiers)
|
|
123
|
+
return false unless modifiers.empty? || modifiers == "1"
|
|
124
|
+
return false unless code.between?(32, 126)
|
|
125
|
+
|
|
126
|
+
key = code.chr(Encoding::UTF_8)
|
|
127
|
+
select_typed_key(key)
|
|
128
|
+
end
|
|
129
|
+
|
|
79
130
|
def handle_select_escape_sequence
|
|
80
|
-
|
|
81
|
-
return
|
|
131
|
+
pending_sequence = read_pending_escape_sequence
|
|
132
|
+
return clear_select_input if pending_sequence.empty? && select_input_active?
|
|
133
|
+
return select_cancel_search if pending_sequence.empty? && select_search_active?
|
|
134
|
+
return SELECT_CANCEL if pending_sequence.empty?
|
|
135
|
+
|
|
136
|
+
full_sequence = "\e#{pending_sequence}"
|
|
137
|
+
sequence = next_key_token(full_sequence)
|
|
138
|
+
queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
|
|
139
|
+
return SELECT_CANCEL if sequence == "\e"
|
|
82
140
|
|
|
83
|
-
key_name = @reader.console.keys[
|
|
141
|
+
key_name = @reader.console.keys[sequence]
|
|
84
142
|
case key_name
|
|
85
143
|
when :up
|
|
86
144
|
select_previous_choice
|
|
87
145
|
when :down
|
|
88
146
|
select_next_choice
|
|
89
147
|
when :left
|
|
90
|
-
|
|
148
|
+
select_move_cursor_left if select_editing_active?
|
|
91
149
|
when :right
|
|
92
|
-
|
|
150
|
+
select_move_cursor_right if select_editing_active?
|
|
93
151
|
end
|
|
94
152
|
true
|
|
95
153
|
end
|
|
96
154
|
|
|
97
155
|
def handle_select_bracketed_paste_key(key)
|
|
98
|
-
|
|
99
|
-
return false unless
|
|
100
|
-
|
|
101
|
-
pasted = text[BRACKETED_PASTE_START.length..] || ""
|
|
102
|
-
until pasted.include?(BRACKETED_PASTE_END)
|
|
103
|
-
chunk = @reader.read_keypress(echo: false, raw: true)
|
|
104
|
-
break if chunk.nil?
|
|
156
|
+
paste = read_bracketed_paste(key)
|
|
157
|
+
return false unless paste
|
|
105
158
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
110
|
-
select_insert_string(normalize_paste(content || ""))
|
|
111
|
-
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
159
|
+
select_insert_string(normalize_paste(paste[:content])) if select_editing_active?
|
|
160
|
+
queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
|
|
112
161
|
true
|
|
113
162
|
end
|
|
114
163
|
|
|
@@ -116,8 +165,233 @@ module Kward
|
|
|
116
165
|
selected_selection_choice || custom_selection_choice || SELECT_CANCEL
|
|
117
166
|
end
|
|
118
167
|
|
|
168
|
+
def handle_select_confirmation_key(key)
|
|
169
|
+
if key.to_s.start_with?("\e")
|
|
170
|
+
clear_select_confirmation
|
|
171
|
+
return true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
key == @select_state[:confirm_key] ? select_action_key(key) : true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def handle_select_input_key(key)
|
|
178
|
+
key_name = @reader.console.keys[key]
|
|
179
|
+
case key_name
|
|
180
|
+
when :return, :enter
|
|
181
|
+
select_input_action_result
|
|
182
|
+
when :backspace
|
|
183
|
+
select_delete_before_cursor
|
|
184
|
+
when :delete
|
|
185
|
+
select_delete_at_cursor
|
|
186
|
+
when :left
|
|
187
|
+
select_move_cursor_left
|
|
188
|
+
when :right
|
|
189
|
+
select_move_cursor_right
|
|
190
|
+
when :home
|
|
191
|
+
self.composer_cursor = 0
|
|
192
|
+
when :end
|
|
193
|
+
self.composer_cursor = composer_input.length
|
|
194
|
+
else
|
|
195
|
+
case key
|
|
196
|
+
when "\n", "\r"
|
|
197
|
+
select_input_action_result
|
|
198
|
+
when "\b", "\x7F"
|
|
199
|
+
select_delete_before_cursor
|
|
200
|
+
when "\e"
|
|
201
|
+
clear_select_input
|
|
202
|
+
true
|
|
203
|
+
else
|
|
204
|
+
handle_select_search_key_binding(key) || select_insert_key(key)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def select_action_key(key)
|
|
210
|
+
return nil unless key.is_a?(String) && key.length == 1
|
|
211
|
+
|
|
212
|
+
action_keys = @select_state ? @select_state[:action_keys].to_h : {}
|
|
213
|
+
action = action_keys[key]
|
|
214
|
+
choice = selected_selection_choice
|
|
215
|
+
return nil unless action && choice
|
|
216
|
+
|
|
217
|
+
if select_confirmation_active?
|
|
218
|
+
return nil unless key == @select_state[:confirm_key]
|
|
219
|
+
|
|
220
|
+
clear_select_confirmation
|
|
221
|
+
return action.merge(choice: choice).reject { |name, _value| name == :confirm || name == :confirm_title }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if action[:confirm]
|
|
225
|
+
@select_state[:confirm_key] = key
|
|
226
|
+
@select_state[:confirm_text] = action[:confirm].to_s
|
|
227
|
+
@select_state[:confirm_title] = action[:confirm_title].to_s
|
|
228
|
+
return true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if action[:input_prompt]
|
|
232
|
+
@select_state[:input_action] = action
|
|
233
|
+
@select_state[:input_choice] = choice
|
|
234
|
+
@select_state[:input_prompt_label] = @prompt_label
|
|
235
|
+
@prompt_label = action[:input_prompt].to_s
|
|
236
|
+
self.composer_input = ""
|
|
237
|
+
self.composer_cursor = 0
|
|
238
|
+
return true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
action.merge(choice: choice)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def select_confirmation_active?
|
|
245
|
+
@select_state && !@select_state[:confirm_key].to_s.empty?
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def select_input_active?
|
|
249
|
+
@select_state && @select_state[:input_action]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def select_input_action_result
|
|
253
|
+
return unless @select_state
|
|
254
|
+
|
|
255
|
+
action = @select_state[:input_action].dup
|
|
256
|
+
choice = @select_state[:input_choice]
|
|
257
|
+
input = composer_input.strip
|
|
258
|
+
clear_select_input
|
|
259
|
+
action.merge(choice: choice, input: input).reject { |name, _value| name == :input_prompt }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def clear_select_input
|
|
263
|
+
return unless @select_state
|
|
264
|
+
|
|
265
|
+
@prompt_label = @select_state[:input_prompt_label].to_s unless @select_state[:input_prompt_label].to_s.empty?
|
|
266
|
+
@select_state.delete(:input_action)
|
|
267
|
+
@select_state.delete(:input_choice)
|
|
268
|
+
@select_state.delete(:input_prompt_label)
|
|
269
|
+
self.composer_input = ""
|
|
270
|
+
self.composer_cursor = 0
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def clear_select_confirmation
|
|
274
|
+
return unless @select_state
|
|
275
|
+
|
|
276
|
+
@select_state.delete(:confirm_key)
|
|
277
|
+
@select_state.delete(:confirm_text)
|
|
278
|
+
@select_state.delete(:confirm_title)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def select_action_result?(result)
|
|
282
|
+
result.is_a?(Hash) && result.key?(:action) && result.key?(:choice)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def select_action_handler(result, action_handlers)
|
|
286
|
+
action_handlers.to_h[result[:action]] || action_handlers.to_h[result[:action].to_s]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def run_select_action(result, handler)
|
|
290
|
+
begin_select_action(result[:activity])
|
|
291
|
+
minimum_busy_until = result[:activity] ? monotonic_now + SELECT_ACTION_MINIMUM_BUSY_SECONDS : nil
|
|
292
|
+
action_result = nil
|
|
293
|
+
action_error = nil
|
|
294
|
+
worker = Thread.new do
|
|
295
|
+
action_result = if result.key?(:input) && handler.arity != 1
|
|
296
|
+
handler.call(result[:choice], result[:input])
|
|
297
|
+
else
|
|
298
|
+
handler.call(result[:choice])
|
|
299
|
+
end
|
|
300
|
+
rescue StandardError => e
|
|
301
|
+
action_error = e
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
while worker.alive? || (minimum_busy_until && monotonic_now < minimum_busy_until)
|
|
305
|
+
tick_select_action_locked
|
|
306
|
+
sleep 0.02
|
|
307
|
+
end
|
|
308
|
+
worker.join
|
|
309
|
+
raise action_error if action_error
|
|
310
|
+
|
|
311
|
+
apply_select_action_result(action_result)
|
|
312
|
+
ensure
|
|
313
|
+
finish_select_action
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def apply_select_action_result(result)
|
|
317
|
+
return SELECT_CONTINUE if result == SELECT_CONTINUE
|
|
318
|
+
return result unless select_continue_result?(result)
|
|
319
|
+
|
|
320
|
+
@mutex.synchronize do
|
|
321
|
+
if @select_state
|
|
322
|
+
@select_state[:choices] = Array(result[:choices]).map(&:to_s) if result.key?(:choices)
|
|
323
|
+
@select_state[:selection_index] = result[:selection_index].to_i if result.key?(:selection_index)
|
|
324
|
+
end
|
|
325
|
+
render_prompt_locked
|
|
326
|
+
@output_io.flush
|
|
327
|
+
end
|
|
328
|
+
SELECT_CONTINUE
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def select_continue_result?(result)
|
|
332
|
+
result.is_a?(Hash) && result[:select_continue]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def begin_select_action(activity)
|
|
336
|
+
return if activity.to_s.empty?
|
|
337
|
+
|
|
338
|
+
@mutex.synchronize do
|
|
339
|
+
@busy = true
|
|
340
|
+
@busy_activity = normalize_busy_activity(activity)
|
|
341
|
+
@asking = true
|
|
342
|
+
reset_spinner_locked
|
|
343
|
+
render_prompt_locked
|
|
344
|
+
@output_io.flush
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def finish_select_action
|
|
349
|
+
@mutex.synchronize do
|
|
350
|
+
@busy = false
|
|
351
|
+
@busy_activity = "streaming"
|
|
352
|
+
@select_state&.delete(:busy_activity)
|
|
353
|
+
render_prompt_locked if @asking
|
|
354
|
+
@output_io.flush
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def tick_select_action_locked
|
|
359
|
+
@mutex.synchronize do
|
|
360
|
+
resized = handle_resize_locked
|
|
361
|
+
spun = tick_spinner_locked
|
|
362
|
+
footer_refreshed = tick_footer_locked
|
|
363
|
+
render_prompt_locked if resized || spun || footer_refreshed
|
|
364
|
+
@output_io.flush if resized || spun || footer_refreshed
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def normalized_select_action_keys(action_keys)
|
|
369
|
+
action_keys.to_h.each_with_object({}) do |(key, action), normalized|
|
|
370
|
+
next unless key.to_s.length == 1
|
|
371
|
+
|
|
372
|
+
normalized_action = normalized_select_action(action)
|
|
373
|
+
normalized[key.to_s] = normalized_action if normalized_action
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def normalized_select_action(action)
|
|
378
|
+
if action.is_a?(Hash)
|
|
379
|
+
name = action[:action] || action["action"]
|
|
380
|
+
activity = action[:activity] || action["activity"]
|
|
381
|
+
confirm = action[:confirm] || action["confirm"]
|
|
382
|
+
confirm_title = action[:confirm_title] || action["confirm_title"]
|
|
383
|
+
input_prompt = action[:input_prompt] || action["input_prompt"]
|
|
384
|
+
defer_finish_render = action[:defer_finish_render] || action["defer_finish_render"]
|
|
385
|
+
else
|
|
386
|
+
name = action
|
|
387
|
+
end
|
|
388
|
+
return nil if name.to_s.empty?
|
|
389
|
+
|
|
390
|
+
{ action: name.to_sym, activity: activity.to_s, confirm: confirm.to_s, confirm_title: confirm_title.to_s, input_prompt: input_prompt.to_s, defer_finish_render: defer_finish_render }.delete_if { |_key, value| value.to_s.empty? }
|
|
391
|
+
end
|
|
392
|
+
|
|
119
393
|
def custom_selection_choice
|
|
120
|
-
return nil unless @select_state && @select_state[:custom]
|
|
394
|
+
return nil unless @select_state && @select_state[:custom] && select_search_active?
|
|
121
395
|
|
|
122
396
|
value = composer_input.strip
|
|
123
397
|
value.empty? ? nil : value
|
|
@@ -144,6 +418,168 @@ module Kward
|
|
|
144
418
|
@select_state[:selection_index] = next_list_selection_index(selection_index, matches.length)
|
|
145
419
|
end
|
|
146
420
|
|
|
421
|
+
def handle_select_search_key_binding(key)
|
|
422
|
+
return false unless select_editing_active?
|
|
423
|
+
|
|
424
|
+
case key
|
|
425
|
+
when "\x01"
|
|
426
|
+
select_move_to_start_of_line
|
|
427
|
+
when "\x02"
|
|
428
|
+
select_move_cursor_left
|
|
429
|
+
when "\x04"
|
|
430
|
+
select_delete_at_cursor
|
|
431
|
+
when "\x05"
|
|
432
|
+
select_move_to_end_of_line
|
|
433
|
+
when "\x06"
|
|
434
|
+
select_move_cursor_right
|
|
435
|
+
when "\x08"
|
|
436
|
+
select_delete_before_cursor
|
|
437
|
+
when "\x0B"
|
|
438
|
+
select_kill_line_after_cursor
|
|
439
|
+
when "\x0C"
|
|
440
|
+
redraw_screen_locked
|
|
441
|
+
when "\x15"
|
|
442
|
+
select_kill_line_before_cursor
|
|
443
|
+
when "\x17"
|
|
444
|
+
select_delete_word_before_cursor
|
|
445
|
+
when "\x19"
|
|
446
|
+
select_yank_kill_buffer
|
|
447
|
+
when "\eb"
|
|
448
|
+
select_move_to_previous_word
|
|
449
|
+
when "\ed"
|
|
450
|
+
select_delete_word_after_cursor
|
|
451
|
+
when "\ef"
|
|
452
|
+
select_move_to_next_word
|
|
453
|
+
when "\e\b", "\e\x7F"
|
|
454
|
+
select_delete_word_before_cursor
|
|
455
|
+
else
|
|
456
|
+
false
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def handle_select_ctrl_key(code)
|
|
461
|
+
case code
|
|
462
|
+
when 97
|
|
463
|
+
select_move_to_start_of_line
|
|
464
|
+
when 98
|
|
465
|
+
select_move_cursor_left
|
|
466
|
+
when 100
|
|
467
|
+
select_delete_at_cursor
|
|
468
|
+
when 101
|
|
469
|
+
select_move_to_end_of_line
|
|
470
|
+
when 102
|
|
471
|
+
select_move_cursor_right
|
|
472
|
+
when 104
|
|
473
|
+
select_delete_before_cursor
|
|
474
|
+
when 107
|
|
475
|
+
select_kill_line_after_cursor
|
|
476
|
+
when 108
|
|
477
|
+
redraw_screen_locked
|
|
478
|
+
when 117
|
|
479
|
+
select_kill_line_before_cursor
|
|
480
|
+
when 119
|
|
481
|
+
select_delete_word_before_cursor
|
|
482
|
+
when 121
|
|
483
|
+
select_yank_kill_buffer
|
|
484
|
+
else
|
|
485
|
+
false
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def handle_select_alt_key(code)
|
|
490
|
+
case code
|
|
491
|
+
when 98
|
|
492
|
+
select_move_to_previous_word
|
|
493
|
+
when 100
|
|
494
|
+
select_delete_word_after_cursor
|
|
495
|
+
when 102
|
|
496
|
+
select_move_to_next_word
|
|
497
|
+
else
|
|
498
|
+
false
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def select_typed_key(key)
|
|
503
|
+
return select_insert_key(key) if select_input_active?
|
|
504
|
+
return select_begin_search if key == "/" && !select_search_active?
|
|
505
|
+
return select_action_key(key) unless select_search_active?
|
|
506
|
+
|
|
507
|
+
select_insert_key(key)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def select_begin_search
|
|
511
|
+
return unless @select_state
|
|
512
|
+
|
|
513
|
+
@select_state[:search_active] = true
|
|
514
|
+
self.composer_input = ""
|
|
515
|
+
self.composer_cursor = 0
|
|
516
|
+
true
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def select_cancel_search
|
|
520
|
+
return unless @select_state
|
|
521
|
+
|
|
522
|
+
@select_state[:search_active] = false
|
|
523
|
+
self.composer_input = ""
|
|
524
|
+
self.composer_cursor = 0
|
|
525
|
+
@select_state[:selection_index] = 0
|
|
526
|
+
true
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def select_search_active?
|
|
530
|
+
@select_state && @select_state[:search_active]
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def select_editing_active?
|
|
534
|
+
select_search_active? || select_input_active?
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def select_move_cursor_left
|
|
538
|
+
@composer.move_cursor_left
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def select_move_cursor_right
|
|
542
|
+
@composer.move_cursor_right
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def select_move_to_start_of_line
|
|
546
|
+
@composer.move_to_start_of_line
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def select_move_to_end_of_line
|
|
550
|
+
@composer.move_to_end_of_line
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def select_move_to_previous_word
|
|
554
|
+
@composer.move_to_previous_word
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def select_move_to_next_word
|
|
558
|
+
@composer.move_to_next_word
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def select_delete_word_before_cursor
|
|
562
|
+
reset_select_filter if @composer.delete_word_before_cursor
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def select_delete_word_after_cursor
|
|
566
|
+
reset_select_filter if @composer.delete_word_after_cursor
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def select_kill_line_before_cursor
|
|
570
|
+
reset_select_filter if @composer.kill_line_before_cursor
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def select_kill_line_after_cursor
|
|
574
|
+
reset_select_filter if @composer.kill_line_after_cursor
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def select_yank_kill_buffer
|
|
578
|
+
before = composer_input
|
|
579
|
+
@composer.yank_kill_buffer
|
|
580
|
+
reset_select_filter unless composer_input == before
|
|
581
|
+
end
|
|
582
|
+
|
|
147
583
|
def select_insert_key(key)
|
|
148
584
|
return unless key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
149
585
|
|
|
@@ -153,29 +589,25 @@ module Kward
|
|
|
153
589
|
def select_insert_string(string)
|
|
154
590
|
return if string.empty?
|
|
155
591
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
592
|
+
@composer.insert_string(string)
|
|
593
|
+
reset_select_filter
|
|
159
594
|
end
|
|
160
595
|
|
|
161
596
|
def select_delete_before_cursor
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
self.composer_input = composer_input[0...(composer_cursor - 1)] + composer_input[composer_cursor..]
|
|
165
|
-
self.composer_cursor -= 1
|
|
166
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
597
|
+
reset_select_filter if @composer.delete_before_cursor
|
|
167
598
|
end
|
|
168
599
|
|
|
169
600
|
def select_delete_at_cursor
|
|
170
|
-
|
|
601
|
+
reset_select_filter if @composer.delete_at_cursor
|
|
602
|
+
end
|
|
171
603
|
|
|
172
|
-
|
|
173
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
604
|
+
def reset_select_filter
|
|
605
|
+
@select_state[:selection_index] = 0 if @select_state && !select_input_active?
|
|
174
606
|
end
|
|
175
607
|
|
|
176
608
|
def selection_matches
|
|
177
609
|
choices = @select_state ? @select_state[:choices] : []
|
|
178
|
-
filter = composer_input.downcase.strip
|
|
610
|
+
filter = select_search_active? ? composer_input.downcase.strip : ""
|
|
179
611
|
matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
|
|
180
612
|
clamp_selection_index(matches.length)
|
|
181
613
|
matches
|
|
@@ -192,22 +624,28 @@ module Kward
|
|
|
192
624
|
@select_state[:selection_index] = count - 1 if count.positive? && selection_index >= count
|
|
193
625
|
end
|
|
194
626
|
|
|
195
|
-
def finish_select_prompt
|
|
627
|
+
def finish_select_prompt(render: true)
|
|
196
628
|
@mutex.synchronize do
|
|
197
629
|
@select_state = nil
|
|
198
630
|
self.composer_input = ""
|
|
199
631
|
self.composer_cursor = 0
|
|
200
632
|
@asking = true
|
|
201
|
-
render_prompt_locked
|
|
633
|
+
render_prompt_locked if render
|
|
202
634
|
@output_io.flush
|
|
203
635
|
end
|
|
204
636
|
end
|
|
205
637
|
|
|
638
|
+
def select_deferred_finish_render?(result)
|
|
639
|
+
result.is_a?(Hash) && result[:defer_finish_render]
|
|
640
|
+
end
|
|
641
|
+
|
|
206
642
|
def selection_overlay_rows(width, height: screen_height)
|
|
643
|
+
return selection_confirmation_rows(width) if select_confirmation_active?
|
|
644
|
+
|
|
207
645
|
matches = selection_matches
|
|
208
|
-
lines = [overlay_text_line(
|
|
646
|
+
lines = [overlay_text_line(selection_overlay_help_text, :muted), overlay_blank_line]
|
|
209
647
|
if matches.empty?
|
|
210
|
-
if @select_state && @select_state[:custom] && !composer_input.strip.empty?
|
|
648
|
+
if @select_state && @select_state[:custom] && select_search_active? && !composer_input.strip.empty?
|
|
211
649
|
lines << overlay_choice_line("Use custom: #{composer_input.strip}", selected: true)
|
|
212
650
|
else
|
|
213
651
|
lines << overlay_text_line("No matches", :muted)
|
|
@@ -224,11 +662,32 @@ module Kward
|
|
|
224
662
|
overlay_card_rows(selection_overlay_title, lines, width)
|
|
225
663
|
end
|
|
226
664
|
|
|
665
|
+
def selection_overlay_help_text
|
|
666
|
+
return "Renaming · Enter save · Esc cancel" if select_input_active?
|
|
667
|
+
|
|
668
|
+
text = "↑/↓ select · Enter open"
|
|
669
|
+
text = "#{text} · / search" unless select_search_active?
|
|
670
|
+
action_keys = @select_state ? @select_state[:action_keys].to_h : {}
|
|
671
|
+
action_keys.each do |key, action|
|
|
672
|
+
text = "#{text} · #{key} #{action[:action]}"
|
|
673
|
+
end
|
|
674
|
+
"#{text} · Esc cancel"
|
|
675
|
+
end
|
|
676
|
+
|
|
227
677
|
def selection_overlay_title
|
|
228
678
|
title = @select_state && @select_state[:title].to_s
|
|
229
679
|
title && !title.empty? ? title : "Sessions"
|
|
230
680
|
end
|
|
231
681
|
|
|
682
|
+
def selection_confirmation_rows(width)
|
|
683
|
+
title = @select_state[:confirm_title].to_s
|
|
684
|
+
title = "Confirm" if title.empty?
|
|
685
|
+
text = @select_state[:confirm_text].to_s
|
|
686
|
+
text = "Press #{@select_state[:confirm_key]} again to confirm, Esc to cancel." if text.empty?
|
|
687
|
+
lines = [overlay_text_line(text, :muted)]
|
|
688
|
+
overlay_card_rows(title, lines, width)
|
|
689
|
+
end
|
|
690
|
+
|
|
232
691
|
def visible_selection_matches(matches, height: screen_height)
|
|
233
692
|
max_rows = max_overlay_list_rows(height)
|
|
234
693
|
start = centered_list_window_start(selection_index, matches.length, max_rows)
|