kward 0.80.1 → 0.82.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/CHANGELOG.md +62 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/Rakefile +44 -4
- data/doc/composer.md +6 -9
- data/doc/configuration.md +13 -3
- data/doc/files.md +8 -2
- data/doc/permissions.md +1 -1
- data/doc/releasing.md +71 -38
- data/doc/rpc.md +2 -1
- data/doc/sandboxing.md +4 -4
- data/doc/security.md +6 -9
- data/doc/shell.md +161 -196
- data/doc/skills.md +17 -5
- data/doc/tabs.md +1 -1
- data/doc/usage.md +6 -5
- data/kward.gemspec +1 -1
- data/lib/kward/adaptive_pty_output_sink.rb +183 -0
- data/lib/kward/ansi.rb +9 -1
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/git.rb +5 -1
- data/lib/kward/cli/interactive_turn.rb +1 -1
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/project_skills.rb +99 -0
- data/lib/kward/cli/project_skills_commands.rb +87 -0
- data/lib/kward/cli/prompt_interface.rb +18 -4
- data/lib/kward/cli/rendering.rb +41 -3
- data/lib/kward/cli/runtime_helpers.rb +222 -21
- data/lib/kward/cli/sessions.rb +6 -4
- data/lib/kward/cli/settings.rb +5 -13
- data/lib/kward/cli/slash_commands.rb +6 -0
- data/lib/kward/cli/tabs.rb +62 -5
- data/lib/kward/cli.rb +21 -1
- data/lib/kward/config_files.rb +9 -4
- data/lib/kward/conversation.rb +8 -5
- data/lib/kward/ekwsh.rb +37 -16
- data/lib/kward/image_attachments.rb +98 -15
- data/lib/kward/interactive_pty_runner.rb +102 -30
- data/lib/kward/prompt_interface/composer_controller.rb +15 -3
- data/lib/kward/prompt_interface/composer_renderer.rb +27 -0
- data/lib/kward/prompt_interface/editor/controller.rb +10 -6
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +2 -2
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +1 -1
- data/lib/kward/prompt_interface/key_handler.rb +100 -43
- data/lib/kward/prompt_interface/overlay_renderer.rb +13 -0
- data/lib/kward/prompt_interface/project_browser.rb +162 -3
- data/lib/kward/prompt_interface/prompt_renderer.rb +15 -6
- data/lib/kward/prompt_interface/question_prompt.rb +1 -1
- data/lib/kward/prompt_interface/screen.rb +40 -15
- data/lib/kward/prompt_interface/selection_prompt.rb +5 -5
- data/lib/kward/prompt_interface/transcript_renderer.rb +4 -4
- data/lib/kward/prompt_interface.rb +167 -44
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts.rb +6 -6
- data/lib/kward/pty_output_sink.rb +45 -0
- data/lib/kward/pty_transcript_normalizer.rb +93 -0
- data/lib/kward/rpc/server.rb +1 -0
- data/lib/kward/session_catalog.rb +87 -0
- data/lib/kward/session_store.rb +97 -7
- data/lib/kward/skills/registry.rb +52 -2
- data/lib/kward/skills/trust_coordinator.rb +45 -0
- data/lib/kward/skills/trust_store.rb +107 -0
- data/lib/kward/terminal_image_support.rb +116 -0
- data/lib/kward/terminal_sequences.rb +43 -0
- data/lib/kward/version.rb +1 -1
- metadata +11 -4
- data/.github/workflows/ci.yml +0 -48
- data/.github/workflows/pages.yml +0 -48
|
@@ -27,15 +27,40 @@ module Kward
|
|
|
27
27
|
result.is_a?(String) ? true : result
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def handle_image_viewer_key(key)
|
|
31
|
+
return true if handle_bundled_key(key) { |token| handle_image_viewer_key(token) }
|
|
32
|
+
|
|
33
|
+
sequence = parse_csi_u_key(key)
|
|
34
|
+
if sequence
|
|
35
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
36
|
+
return close_image_viewer if sequence[:code] == 27
|
|
37
|
+
|
|
38
|
+
text = csi_u_printable_text(sequence)
|
|
39
|
+
return zoom_image_viewer(:in) if text == "+"
|
|
40
|
+
return zoom_image_viewer(:out) if text == "-"
|
|
41
|
+
return true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return zoom_image_viewer(:in) if key == "+"
|
|
45
|
+
return zoom_image_viewer(:out) if key == "-"
|
|
46
|
+
|
|
47
|
+
close_image_viewer if key == "\e" || key == "q" || key == "Q"
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
|
|
30
51
|
def handle_key(key)
|
|
31
52
|
return submit_input if key.nil?
|
|
32
53
|
return handle_interactive_key(key) if interactive_active_locked?
|
|
54
|
+
return handle_image_viewer_key(key) if image_viewer_active?
|
|
33
55
|
return handle_editor_input_key(key) if editor_active?
|
|
34
56
|
return handle_project_browser_key(key) if project_browser_visible?
|
|
35
57
|
return handle_history_search_key(key) if history_search_active?
|
|
36
58
|
return true if handle_mouse_reporting_key(key)
|
|
37
59
|
return if handle_bracketed_paste_key(key)
|
|
38
60
|
|
|
61
|
+
completion_overlay_result = handle_completion_overlay_key(key)
|
|
62
|
+
return completion_overlay_result unless completion_overlay_result == false
|
|
63
|
+
|
|
39
64
|
csi_result = handle_csi_u_key(key)
|
|
40
65
|
return csi_result unless csi_result == false
|
|
41
66
|
return if handle_shift_enter_key(key)
|
|
@@ -44,9 +69,6 @@ module Kward
|
|
|
44
69
|
completion_result = handle_completion_provider_key(key)
|
|
45
70
|
return completion_result unless completion_result == false
|
|
46
71
|
|
|
47
|
-
reasoning_result = handle_reasoning_key_binding(key)
|
|
48
|
-
return reasoning_result unless reasoning_result == false
|
|
49
|
-
|
|
50
72
|
tab_result = handle_tab_key_binding(key)
|
|
51
73
|
return tab_result unless tab_result == false
|
|
52
74
|
|
|
@@ -81,7 +103,7 @@ module Kward
|
|
|
81
103
|
when :ctrl_y
|
|
82
104
|
yank_kill_buffer
|
|
83
105
|
when :ctrl_l
|
|
84
|
-
|
|
106
|
+
request_transcript_redraw
|
|
85
107
|
when :ctrl_r
|
|
86
108
|
start_history_search
|
|
87
109
|
when :left
|
|
@@ -212,9 +234,6 @@ module Kward
|
|
|
212
234
|
return true if sequence == "\e" && (dismiss_file_overlay || dismiss_slash_overlay)
|
|
213
235
|
return true if handle_shift_enter_key(sequence)
|
|
214
236
|
|
|
215
|
-
reasoning_result = handle_reasoning_key_binding(sequence)
|
|
216
|
-
return reasoning_result unless reasoning_result == false
|
|
217
|
-
|
|
218
237
|
tab_result = handle_tab_key_binding(sequence)
|
|
219
238
|
return tab_result unless tab_result == false
|
|
220
239
|
|
|
@@ -323,10 +342,10 @@ module Kward
|
|
|
323
342
|
if ctrl_modifier?(modifier)
|
|
324
343
|
shift_modifier?(modifier) ? { tab_action: :previous } : { tab_action: :next }
|
|
325
344
|
elsif shift_modifier?(modifier)
|
|
326
|
-
|
|
345
|
+
handle_tab_completion_key
|
|
327
346
|
else
|
|
328
347
|
completion_result = handle_completion_provider_key("\t")
|
|
329
|
-
completion_result == false ?
|
|
348
|
+
completion_result == false ? handle_tab_completion_key : completion_result
|
|
330
349
|
end
|
|
331
350
|
when 13
|
|
332
351
|
if modifier == 2
|
|
@@ -447,7 +466,7 @@ module Kward
|
|
|
447
466
|
when 107
|
|
448
467
|
kill_line_after_cursor
|
|
449
468
|
when 108
|
|
450
|
-
|
|
469
|
+
request_transcript_redraw
|
|
451
470
|
when 114
|
|
452
471
|
start_history_search
|
|
453
472
|
when 117
|
|
@@ -580,61 +599,99 @@ module Kward
|
|
|
580
599
|
|
|
581
600
|
CTRL_TAB_SEQUENCES = TerminalKeys::CTRL_TAB
|
|
582
601
|
CTRL_SHIFT_TAB_SEQUENCES = TerminalKeys::CTRL_SHIFT_TAB
|
|
583
|
-
|
|
602
|
+
|
|
603
|
+
def handle_completion_overlay_key(key)
|
|
604
|
+
return false unless @completion_overlay
|
|
605
|
+
|
|
606
|
+
return handle_completion_provider_key("\t") if completion_tab_key?(key)
|
|
607
|
+
|
|
608
|
+
dismiss_completion_overlay
|
|
609
|
+
false
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def completion_tab_key?(key)
|
|
613
|
+
return true if key == "\t"
|
|
614
|
+
|
|
615
|
+
sequence = parse_csi_u_key(key)
|
|
616
|
+
return false unless sequence && sequence[:code] == 9
|
|
617
|
+
|
|
618
|
+
completion = !ctrl_modifier?(sequence[:modifier]) && !shift_modifier?(sequence[:modifier])
|
|
619
|
+
queue_pending_keys(sequence[:remaining]) if completion && sequence[:remaining] && !sequence[:remaining].empty?
|
|
620
|
+
completion
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def dismiss_completion_overlay
|
|
624
|
+
@completion_overlay = nil
|
|
625
|
+
@completion_cycle = nil
|
|
626
|
+
end
|
|
584
627
|
|
|
585
628
|
def handle_completion_provider_key(key)
|
|
586
629
|
return false unless key == "\t" && @completion_provider
|
|
587
630
|
|
|
631
|
+
if (cycle = matching_completion_cycle)
|
|
632
|
+
cycle_completion(cycle)
|
|
633
|
+
return true
|
|
634
|
+
end
|
|
635
|
+
|
|
588
636
|
result = @completion_provider.call(composer_input, composer_cursor)
|
|
637
|
+
return false if result == false
|
|
589
638
|
return true unless result
|
|
590
639
|
|
|
591
640
|
apply_completion_result(result)
|
|
592
641
|
true
|
|
593
642
|
end
|
|
594
643
|
|
|
644
|
+
def matching_completion_cycle
|
|
645
|
+
cycle = @completion_cycle
|
|
646
|
+
return nil unless cycle
|
|
647
|
+
return cycle if cycle[:input] == composer_input && cycle[:cursor] == composer_cursor
|
|
648
|
+
|
|
649
|
+
@completion_cycle = nil
|
|
650
|
+
@completion_overlay = nil
|
|
651
|
+
nil
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def cycle_completion(cycle)
|
|
655
|
+
cycle[:index] = (cycle[:index] + 1) % cycle[:candidates].length
|
|
656
|
+
apply_completion_cycle_candidate(cycle)
|
|
657
|
+
end
|
|
658
|
+
|
|
595
659
|
def apply_completion_result(result)
|
|
596
660
|
range = result[:range] || result["range"] || result.range
|
|
597
661
|
replacement = result[:replacement] || result["replacement"] || result.replacement
|
|
598
|
-
candidates = result[:candidates] || result["candidates"] || result.candidates
|
|
662
|
+
candidates = (result[:candidates] || result["candidates"] || result.candidates).to_a
|
|
599
663
|
original = composer_input
|
|
600
664
|
before = original[0...range.begin].to_s
|
|
601
665
|
after = original[range.end..].to_s
|
|
666
|
+
|
|
667
|
+
if candidates.length > 1
|
|
668
|
+
@completion_cycle = {
|
|
669
|
+
candidates: candidates.map(&:to_s),
|
|
670
|
+
index: 0,
|
|
671
|
+
before: before,
|
|
672
|
+
after: after
|
|
673
|
+
}
|
|
674
|
+
apply_completion_cycle_candidate(@completion_cycle)
|
|
675
|
+
show_completion_overlay(candidates)
|
|
676
|
+
return
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
@completion_cycle = nil
|
|
602
680
|
self.composer_input = "#{before}#{replacement}#{after}"
|
|
603
681
|
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
682
|
end
|
|
606
683
|
|
|
607
|
-
def
|
|
608
|
-
|
|
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
|
|
684
|
+
def show_completion_overlay(candidates)
|
|
685
|
+
@completion_overlay = { candidates: candidates.map(&:to_s), index: 0 }
|
|
619
686
|
end
|
|
620
687
|
|
|
621
|
-
def
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
|
688
|
+
def apply_completion_cycle_candidate(cycle)
|
|
689
|
+
replacement = cycle[:candidates][cycle[:index]]
|
|
690
|
+
self.composer_input = "#{cycle[:before]}#{replacement}#{cycle[:after]}"
|
|
691
|
+
self.composer_cursor = cycle[:before].length + replacement.length
|
|
692
|
+
@completion_overlay[:index] = cycle[:index] if @completion_overlay
|
|
693
|
+
cycle[:input] = composer_input
|
|
694
|
+
cycle[:cursor] = composer_cursor
|
|
638
695
|
end
|
|
639
696
|
|
|
640
697
|
def handle_tab_key_binding(key)
|
|
@@ -710,7 +767,7 @@ module Kward
|
|
|
710
767
|
when TerminalKeys::CTRL_K
|
|
711
768
|
kill_line_after_cursor
|
|
712
769
|
when TerminalKeys::CTRL_L
|
|
713
|
-
|
|
770
|
+
request_transcript_redraw
|
|
714
771
|
when TerminalKeys::CTRL_U
|
|
715
772
|
kill_line_before_cursor
|
|
716
773
|
when TerminalKeys::CTRL_W
|
|
@@ -10,13 +10,26 @@ module Kward
|
|
|
10
10
|
return question_overlay_rows(width) if @question_state
|
|
11
11
|
return selection_overlay_rows(width, height: height) if @select_state
|
|
12
12
|
return git_overlay_rows(width, height: height) if @git_state
|
|
13
|
+
return image_viewer_rows(width, height: height) if image_viewer_active?
|
|
13
14
|
return project_browser_rows(width, height: height) if project_browser_visible?
|
|
15
|
+
return completion_overlay_rows(width, height: height) if @completion_overlay
|
|
14
16
|
return history_search_overlay_rows(width, height: height) if history_search_active?
|
|
15
17
|
return file_overlay_rows(width, height: height) if file_overlay_visible?
|
|
16
18
|
|
|
17
19
|
slash_overlay_rows(width, height: height)
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
def completion_overlay_rows(width, height: screen_height)
|
|
23
|
+
candidates = @completion_overlay[:candidates]
|
|
24
|
+
max_rows = max_overlay_list_rows(height)
|
|
25
|
+
selected = @completion_overlay[:index].to_i
|
|
26
|
+
start = centered_list_window_start(selected, candidates.length, max_rows)
|
|
27
|
+
rows = (candidates[start, max_rows] || []).each_with_index.map do |candidate, offset|
|
|
28
|
+
overlay_choice_line(candidate, selected: start + offset == selected)
|
|
29
|
+
end
|
|
30
|
+
overlay_card_rows("Completions", rows, width)
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
def history_search_overlay_rows(width, height: screen_height)
|
|
21
34
|
matches = history_search_matches
|
|
22
35
|
if matches.empty?
|
|
@@ -34,6 +34,10 @@ module Kward
|
|
|
34
34
|
}.freeze
|
|
35
35
|
PROJECT_BROWSER_DIRECTORY_ICON = ""
|
|
36
36
|
PROJECT_BROWSER_FILE_ICON = ""
|
|
37
|
+
IMAGE_VIEWER_CELL_HEIGHT_TO_WIDTH = 2.0
|
|
38
|
+
IMAGE_VIEWER_ZOOM_STEP = 0.25
|
|
39
|
+
IMAGE_VIEWER_MIN_ZOOM = 0.25
|
|
40
|
+
IMAGE_VIEWER_MAX_ZOOM = 4.0
|
|
37
41
|
|
|
38
42
|
def open_project_browser
|
|
39
43
|
@mutex.synchronize do
|
|
@@ -51,7 +55,8 @@ module Kward
|
|
|
51
55
|
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
52
56
|
selection_index: 0,
|
|
53
57
|
search_active: false,
|
|
54
|
-
query: ""
|
|
58
|
+
query: "",
|
|
59
|
+
status: nil
|
|
55
60
|
}
|
|
56
61
|
restore_project_browser_selection(saved_state["selected_path"])
|
|
57
62
|
self.composer_input = ""
|
|
@@ -63,7 +68,11 @@ module Kward
|
|
|
63
68
|
private
|
|
64
69
|
|
|
65
70
|
def project_browser_visible?
|
|
66
|
-
!@project_browser_state.nil? && !editor_active?
|
|
71
|
+
!@project_browser_state.nil? && !editor_active? && !image_viewer_active?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def image_viewer_active?
|
|
75
|
+
!@image_viewer_state.nil?
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
def dismiss_project_browser
|
|
@@ -243,6 +252,8 @@ module Kward
|
|
|
243
252
|
if row[:directory]
|
|
244
253
|
toggle_project_browser_directory(row[:path])
|
|
245
254
|
true
|
|
255
|
+
elsif Kward::ImageAttachments.image_extension?(row[:path])
|
|
256
|
+
open_project_browser_image(row[:path])
|
|
246
257
|
else
|
|
247
258
|
persist_project_browser_state unless project_browser_search_active?
|
|
248
259
|
@project_browser_restore_after_editor = true if open_editor(row[:path])
|
|
@@ -250,6 +261,152 @@ module Kward
|
|
|
250
261
|
end
|
|
251
262
|
end
|
|
252
263
|
|
|
264
|
+
def open_project_browser_image(path)
|
|
265
|
+
full_path = File.expand_path(path.to_s, prompt_workspace_root)
|
|
266
|
+
part = Kward::ImageAttachments.image_part_from_path(full_path)
|
|
267
|
+
unless part
|
|
268
|
+
@project_browser_state[:status] = "Cannot preview #{path}: unreadable or larger than 20 MB"
|
|
269
|
+
return false
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
protocol = terminal_image_protocol
|
|
273
|
+
unless protocol
|
|
274
|
+
@project_browser_state[:status] = "Cannot preview #{path}: terminal does not support inline images"
|
|
275
|
+
return false
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
part = Kward::ImageAttachments.terminal_image_part(part, protocol)
|
|
279
|
+
unless part
|
|
280
|
+
@project_browser_state[:status] = "Cannot preview #{path}: terminal cannot render this image format"
|
|
281
|
+
return false
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
image_id = next_terminal_image_id
|
|
285
|
+
columns = image_viewer_columns(screen_width)
|
|
286
|
+
rows = image_viewer_preview_rows(screen_height, width: screen_width)
|
|
287
|
+
terminal_columns, terminal_rows = image_viewer_terminal_size(part, protocol, columns, rows)
|
|
288
|
+
sequence = Kward::ImageAttachments.terminal_image_sequence(
|
|
289
|
+
part,
|
|
290
|
+
width: terminal_columns,
|
|
291
|
+
height: terminal_rows,
|
|
292
|
+
protocol: protocol,
|
|
293
|
+
image_id: image_id,
|
|
294
|
+
move_cursor: false,
|
|
295
|
+
env: ENV
|
|
296
|
+
)
|
|
297
|
+
unless sequence
|
|
298
|
+
@project_browser_state[:status] = "Cannot preview #{path}: terminal cannot render this image format"
|
|
299
|
+
return false
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
@image_viewer_state = {
|
|
303
|
+
path: full_path,
|
|
304
|
+
display_path: path.to_s,
|
|
305
|
+
protocol: protocol,
|
|
306
|
+
image_id: image_id,
|
|
307
|
+
part: part,
|
|
308
|
+
zoom: 1.0,
|
|
309
|
+
columns: columns,
|
|
310
|
+
rows: rows,
|
|
311
|
+
sequence: sequence
|
|
312
|
+
}
|
|
313
|
+
@pending_keys.clear
|
|
314
|
+
@asking = true
|
|
315
|
+
true
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def close_image_viewer
|
|
319
|
+
return false unless image_viewer_active?
|
|
320
|
+
|
|
321
|
+
clear_image_viewer_output_locked
|
|
322
|
+
@image_viewer_state = nil
|
|
323
|
+
sync_project_browser_query_input
|
|
324
|
+
@asking = true
|
|
325
|
+
true
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def terminal_image_protocol
|
|
329
|
+
return @terminal_image_protocol if @terminal_image_protocol
|
|
330
|
+
|
|
331
|
+
detected = TerminalImageSupport.detect(input: @input_io, output: @output_io)
|
|
332
|
+
@terminal_image_protocol = detected if detected
|
|
333
|
+
detected
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def next_terminal_image_id
|
|
337
|
+
@next_terminal_image_id ||= 0
|
|
338
|
+
@next_terminal_image_id += 1
|
|
339
|
+
@next_terminal_image_id = 1 if @next_terminal_image_id > 4_294_967_295
|
|
340
|
+
@next_terminal_image_id
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def image_viewer_zoom
|
|
344
|
+
@image_viewer_state&.fetch(:zoom, 1.0) || 1.0
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def zoom_image_viewer(direction)
|
|
348
|
+
return false unless image_viewer_active?
|
|
349
|
+
|
|
350
|
+
delta = direction == :in ? IMAGE_VIEWER_ZOOM_STEP : -IMAGE_VIEWER_ZOOM_STEP
|
|
351
|
+
zoom = [[image_viewer_zoom + delta, IMAGE_VIEWER_MIN_ZOOM].max, IMAGE_VIEWER_MAX_ZOOM].min
|
|
352
|
+
@image_viewer_state[:zoom] = zoom.round(2)
|
|
353
|
+
@asking = true
|
|
354
|
+
true
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def image_viewer_columns(width = screen_width)
|
|
358
|
+
available_columns = [width - 4, 1].max
|
|
359
|
+
desired_columns = [(ImageAttachments::DEFAULT_TERMINAL_IMAGE_WIDTH.to_i * image_viewer_zoom).round, 1].max
|
|
360
|
+
[desired_columns, available_columns].min
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def image_viewer_sequence(width, height)
|
|
364
|
+
columns = image_viewer_columns(width)
|
|
365
|
+
rows = image_viewer_preview_rows(height, width: width)
|
|
366
|
+
state = @image_viewer_state
|
|
367
|
+
return state[:sequence] if state[:columns] == columns && state[:rows] == rows
|
|
368
|
+
|
|
369
|
+
terminal_columns, terminal_rows = image_viewer_terminal_size(state[:part], state[:protocol], columns, rows)
|
|
370
|
+
state[:columns] = columns
|
|
371
|
+
state[:rows] = rows
|
|
372
|
+
sequence = Kward::ImageAttachments.terminal_image_sequence(
|
|
373
|
+
state[:part],
|
|
374
|
+
width: terminal_columns,
|
|
375
|
+
height: terminal_rows,
|
|
376
|
+
protocol: state[:protocol],
|
|
377
|
+
image_id: state[:image_id],
|
|
378
|
+
move_cursor: false,
|
|
379
|
+
env: ENV
|
|
380
|
+
)
|
|
381
|
+
delete_sequence = image_viewer_delete_sequence(state[:image_id]) if state[:protocol] == TerminalImageSupport::KITTY
|
|
382
|
+
state[:sequence] = "#{delete_sequence}#{sequence}"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def image_viewer_terminal_size(part, protocol, columns, rows)
|
|
386
|
+
return [columns, rows] unless protocol == TerminalImageSupport::KITTY
|
|
387
|
+
|
|
388
|
+
dimensions = Kward::ImageAttachments.png_dimensions(part)
|
|
389
|
+
return [nil, rows] unless dimensions
|
|
390
|
+
|
|
391
|
+
image_width, image_height = dimensions
|
|
392
|
+
image_aspect_ratio = image_width.to_f / image_height
|
|
393
|
+
available_aspect_ratio = columns.to_f / (rows * IMAGE_VIEWER_CELL_HEIGHT_TO_WIDTH)
|
|
394
|
+
image_aspect_ratio > available_aspect_ratio ? [columns, nil] : [nil, rows]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def image_viewer_delete_sequence(image_id)
|
|
398
|
+
sequence = TerminalSequences.kitty_delete(image_id)
|
|
399
|
+
ENV["TMUX"].to_s.empty? ? sequence : TerminalSequences.tmux_passthrough(sequence)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def clear_image_viewer_output_locked
|
|
403
|
+
return unless image_viewer_active?
|
|
404
|
+
return unless @image_viewer_state[:protocol] == TerminalImageSupport::KITTY
|
|
405
|
+
return unless @started
|
|
406
|
+
|
|
407
|
+
print_output_locked(image_viewer_delete_sequence(@image_viewer_state[:image_id]))
|
|
408
|
+
end
|
|
409
|
+
|
|
253
410
|
def expand_selected_project_browser_row
|
|
254
411
|
row = selected_project_browser_row
|
|
255
412
|
return false unless row&.fetch(:directory, false)
|
|
@@ -318,7 +475,8 @@ module Kward
|
|
|
318
475
|
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
319
476
|
selection_index: 0,
|
|
320
477
|
search_active: false,
|
|
321
|
-
query: ""
|
|
478
|
+
query: "",
|
|
479
|
+
status: nil
|
|
322
480
|
}
|
|
323
481
|
restore_project_browser_selection(saved_state["selected_path"])
|
|
324
482
|
end
|
|
@@ -341,6 +499,7 @@ module Kward
|
|
|
341
499
|
lines << overlay_choice_line(project_browser_row_text(row), selected: index == @project_browser_state[:selection_index])
|
|
342
500
|
end
|
|
343
501
|
end
|
|
502
|
+
lines << overlay_text_line(@project_browser_state[:status], :muted) if @project_browser_state[:status]
|
|
344
503
|
lines << overlay_blank_line
|
|
345
504
|
lines << overlay_text_line(project_browser_help_text, :muted)
|
|
346
505
|
overlay_card_rows(title, lines, width)
|
|
@@ -7,12 +7,12 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def render_prompt_locked(synchronized: false)
|
|
10
|
-
return unless @started && @asking
|
|
10
|
+
return unless @started && @asking && !terminal_owned_by_child_locked?
|
|
11
11
|
|
|
12
12
|
width, height = screen_size
|
|
13
13
|
if width != @last_width || height != @last_height
|
|
14
14
|
with_synchronized_output_locked { render_prompt_body_locked }
|
|
15
|
-
|
|
15
|
+
flush_output_locked
|
|
16
16
|
return
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -23,10 +23,12 @@ module Kward
|
|
|
23
23
|
else
|
|
24
24
|
render_prompt_layout_locked(rows, cursor_row, cursor_col, width, height)
|
|
25
25
|
end
|
|
26
|
-
|
|
26
|
+
flush_output_locked
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def render_prompt_body_locked
|
|
30
|
+
return if terminal_owned_by_child_locked?
|
|
31
|
+
|
|
30
32
|
handle_resize_locked
|
|
31
33
|
width, height = screen_size
|
|
32
34
|
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
@@ -49,6 +51,8 @@ module Kward
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def clear_prompt_locked
|
|
54
|
+
return if terminal_owned_by_child_locked?
|
|
55
|
+
|
|
52
56
|
handle_resize_locked
|
|
53
57
|
width, height = screen_size
|
|
54
58
|
clear_composer_region_locked(height: height)
|
|
@@ -58,6 +62,8 @@ module Kward
|
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def clear_prompt_for_output_locked
|
|
65
|
+
return if terminal_owned_by_child_locked?
|
|
66
|
+
|
|
61
67
|
handle_resize_locked
|
|
62
68
|
width, height = screen_size
|
|
63
69
|
reserve_composer_region_locked(width: width, height: height) if @started && @asking
|
|
@@ -68,6 +74,8 @@ module Kward
|
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def prepare_transcript_output_locked
|
|
77
|
+
return if terminal_owned_by_child_locked?
|
|
78
|
+
|
|
71
79
|
handle_resize_locked
|
|
72
80
|
width, height = screen_size
|
|
73
81
|
hide_cursor_for_transcript_output_locked
|
|
@@ -76,7 +84,7 @@ module Kward
|
|
|
76
84
|
end
|
|
77
85
|
|
|
78
86
|
def restore_composer_cursor_locked
|
|
79
|
-
return unless @started && @asking
|
|
87
|
+
return unless @started && @asking && !terminal_owned_by_child_locked?
|
|
80
88
|
|
|
81
89
|
width, height = screen_size
|
|
82
90
|
_rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
@@ -85,10 +93,11 @@ module Kward
|
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
def redraw_screen_locked(width: screen_width, height: screen_height)
|
|
88
|
-
return unless @started
|
|
96
|
+
return unless @started && !terminal_owned_by_child_locked?
|
|
89
97
|
|
|
98
|
+
clear_image_viewer_output_locked if image_viewer_active?
|
|
90
99
|
restore_scroll_region_locked
|
|
91
|
-
|
|
100
|
+
print_output_locked(TTY::Cursor.clear_screen)
|
|
92
101
|
move_to_screen(1, 1)
|
|
93
102
|
@reserved_rows = 0
|
|
94
103
|
@last_composer_rows = []
|