kward 0.81.0 → 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 +29 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/Rakefile +44 -4
- data/doc/composer.md +2 -2
- data/doc/files.md +8 -2
- data/doc/releasing.md +71 -38
- data/doc/security.md +1 -1
- data/doc/shell.md +3 -3
- data/kward.gemspec +1 -1
- data/lib/kward/adaptive_pty_output_sink.rb +183 -0
- data/lib/kward/cli/git.rb +5 -1
- data/lib/kward/cli/rendering.rb +14 -2
- data/lib/kward/cli/runtime_helpers.rb +103 -39
- data/lib/kward/cli/settings.rb +5 -13
- data/lib/kward/cli/tabs.rb +59 -4
- data/lib/kward/cli.rb +2 -0
- data/lib/kward/image_attachments.rb +98 -15
- data/lib/kward/interactive_pty_runner.rb +25 -19
- data/lib/kward/prompt_interface/composer_controller.rb +3 -3
- data/lib/kward/prompt_interface/composer_renderer.rb +27 -0
- data/lib/kward/prompt_interface/editor/controller.rb +6 -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 +22 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -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 -20
- 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 +83 -32
- data/lib/kward/pty_output_sink.rb +45 -0
- data/lib/kward/pty_transcript_normalizer.rb +93 -0
- data/lib/kward/session_catalog.rb +87 -0
- data/lib/kward/session_store.rb +94 -5
- 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 +7 -4
- data/.github/workflows/ci.yml +0 -48
- data/.github/workflows/pages.yml +0 -48
|
@@ -265,6 +265,33 @@ module Kward
|
|
|
265
265
|
@composer.cursor_logical_position
|
|
266
266
|
end
|
|
267
267
|
|
|
268
|
+
def image_viewer_preview_rows(height, width: screen_width)
|
|
269
|
+
available_rows = max_project_browser_rows(height, width: width)
|
|
270
|
+
desired_rows = [(IMAGE_VIEWER_MAX_ROWS * image_viewer_zoom).round, 1].max
|
|
271
|
+
[available_rows, desired_rows].min
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def image_viewer_rows(width, height: screen_height)
|
|
275
|
+
content_width = [width - 4, 1].max
|
|
276
|
+
preview_rows = image_viewer_preview_rows(height, width: width)
|
|
277
|
+
title = visible_truncate(" Image: #{@image_viewer_state[:display_path]} ", content_width)
|
|
278
|
+
rows = [
|
|
279
|
+
colored("╭", :primary_green) + title + colored("─" * [width - ANSI.strip(title).length - 2, 0].max, :primary_green) + colored("╮", :primary_green),
|
|
280
|
+
image_viewer_graphic_row(width, image_viewer_sequence(width, height))
|
|
281
|
+
]
|
|
282
|
+
rows.concat(Array.new(preview_rows - 1) { box_content_row("", content_width) })
|
|
283
|
+
zoom = (image_viewer_zoom * 100).round
|
|
284
|
+
rows << footer_row(content_width, "Read-only image preview · #{zoom}% · +/- zoom · Esc/Q close")
|
|
285
|
+
rows << bottom_border(width)
|
|
286
|
+
rows
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def image_viewer_graphic_row(width, sequence)
|
|
290
|
+
left_border = "#{colored("│", :primary_green)} "
|
|
291
|
+
right_border = "#{TerminalSequences.move_to_column(width)}#{colored("│", :primary_green)}"
|
|
292
|
+
"#{TTY::Cursor.clear_line}#{left_border}#{sequence}#{right_border}"
|
|
293
|
+
end
|
|
294
|
+
|
|
268
295
|
end
|
|
269
296
|
end
|
|
270
297
|
end
|
|
@@ -992,8 +992,8 @@ module Kward
|
|
|
992
992
|
return false if text.empty?
|
|
993
993
|
|
|
994
994
|
@editor_state.push_kill(text)
|
|
995
|
-
|
|
996
|
-
|
|
995
|
+
print_output_locked(TerminalSequences.osc52(text))
|
|
996
|
+
flush_output_locked if @output_io.respond_to?(:flush)
|
|
997
997
|
@editor_state.clear_selection
|
|
998
998
|
@editor_state.status = "Copied selection"
|
|
999
999
|
true
|
|
@@ -1103,16 +1103,16 @@ module Kward
|
|
|
1103
1103
|
def enable_editor_mouse_reporting
|
|
1104
1104
|
return if @editor_mouse_reporting_enabled
|
|
1105
1105
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1106
|
+
print_output_locked(TerminalSequences::MOUSE_REPORTING_ENABLE)
|
|
1107
|
+
flush_output_locked if @output_io.respond_to?(:flush)
|
|
1108
1108
|
@editor_mouse_reporting_enabled = true
|
|
1109
1109
|
end
|
|
1110
1110
|
|
|
1111
1111
|
def disable_editor_mouse_reporting(force: false)
|
|
1112
1112
|
return unless force || @editor_mouse_reporting_enabled
|
|
1113
1113
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1114
|
+
print_output_locked(TerminalSequences::MOUSE_REPORTING_DISABLE)
|
|
1115
|
+
flush_output_locked if @output_io.respond_to?(:flush)
|
|
1116
1116
|
@editor_mouse_reporting_enabled = false
|
|
1117
1117
|
end
|
|
1118
1118
|
|
|
@@ -240,8 +240,8 @@ module Kward
|
|
|
240
240
|
end
|
|
241
241
|
|
|
242
242
|
@editor_state.copy_for_kill_ring(range[0], range[1])
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
print_output_locked(TerminalSequences.osc52(@editor_state.kill_buffer))
|
|
244
|
+
flush_output_locked if @output_io.respond_to?(:flush)
|
|
245
245
|
@editor_state.clear_selection
|
|
246
246
|
@editor_state.status = "Copied region"
|
|
247
247
|
true
|
|
@@ -2239,8 +2239,8 @@ module Kward
|
|
|
2239
2239
|
def vibe_copy_range(start_index, end_index, status, linewise: false)
|
|
2240
2240
|
@editor_state.copy_range(start_index, end_index)
|
|
2241
2241
|
@editor_state.vibe_kill_linewise = linewise
|
|
2242
|
-
|
|
2243
|
-
|
|
2242
|
+
print_output_locked(TerminalSequences.osc52(@editor_state.kill_buffer))
|
|
2243
|
+
flush_output_locked if @output_io.respond_to?(:flush)
|
|
2244
2244
|
@editor_state.status = status
|
|
2245
2245
|
end
|
|
2246
2246
|
|
|
@@ -27,9 +27,31 @@ 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?
|
|
@@ -10,6 +10,7 @@ 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?
|
|
14
15
|
return completion_overlay_rows(width, height: height) if @completion_overlay
|
|
15
16
|
return history_search_overlay_rows(width, height: height) if history_search_active?
|
|
@@ -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 = []
|
|
@@ -6,6 +6,28 @@ module Kward
|
|
|
6
6
|
module Screen
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
+
def terminal_owned_by_child_locked?
|
|
10
|
+
@terminal_owner == :child
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def print_output_locked(*values)
|
|
14
|
+
return if terminal_owned_by_child_locked?
|
|
15
|
+
|
|
16
|
+
values.each { |value| @output_io.print(value) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def puts_output_locked(*values)
|
|
20
|
+
return if terminal_owned_by_child_locked?
|
|
21
|
+
|
|
22
|
+
@output_io.puts(*values)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def flush_output_locked
|
|
26
|
+
return if terminal_owned_by_child_locked?
|
|
27
|
+
|
|
28
|
+
@output_io.flush
|
|
29
|
+
end
|
|
30
|
+
|
|
9
31
|
def enter_raw_mode_locked
|
|
10
32
|
return unless @input_io.respond_to?(:tty?) && @input_io.tty?
|
|
11
33
|
return unless @input_io.respond_to?(:console_mode) && @input_io.respond_to?(:console_mode=)
|
|
@@ -31,19 +53,19 @@ module Kward
|
|
|
31
53
|
end
|
|
32
54
|
|
|
33
55
|
def with_synchronized_output_locked
|
|
34
|
-
if @restoring_transcript || @synchronized_output_depth.positive?
|
|
56
|
+
if @restoring_transcript || @synchronized_output_depth.positive? || terminal_owned_by_child_locked?
|
|
35
57
|
yield
|
|
36
58
|
return
|
|
37
59
|
end
|
|
38
60
|
|
|
39
61
|
synchronized = true
|
|
40
62
|
@synchronized_output_depth += 1
|
|
41
|
-
|
|
63
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
42
64
|
yield
|
|
43
65
|
ensure
|
|
44
66
|
if synchronized
|
|
45
67
|
@synchronized_output_depth -= 1
|
|
46
|
-
|
|
68
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_DISABLE) if @synchronized_output_depth.zero?
|
|
47
69
|
end
|
|
48
70
|
end
|
|
49
71
|
|
|
@@ -58,27 +80,28 @@ module Kward
|
|
|
58
80
|
visible = select_editing_active? if @select_state
|
|
59
81
|
visible = git_composing? if @git_state
|
|
60
82
|
visible = project_browser_search_active? if project_browser_visible?
|
|
83
|
+
visible = false if image_viewer_active?
|
|
61
84
|
set_cursor_visible_locked(visible)
|
|
62
85
|
end
|
|
63
86
|
|
|
64
87
|
def set_cursor_visible_locked(visible, force: false)
|
|
65
88
|
return if !force && @cursor_visible == visible
|
|
66
89
|
|
|
67
|
-
|
|
90
|
+
print_output_locked(visible ? CURSOR_SHOW : CURSOR_HIDE)
|
|
68
91
|
@cursor_visible = visible
|
|
69
92
|
end
|
|
70
93
|
|
|
71
94
|
def set_editor_bar_cursor_locked
|
|
72
95
|
return if @editor_bar_cursor_active
|
|
73
96
|
|
|
74
|
-
|
|
97
|
+
print_output_locked(CURSOR_SHAPE_BAR)
|
|
75
98
|
@editor_bar_cursor_active = true
|
|
76
99
|
end
|
|
77
100
|
|
|
78
|
-
def restore_editor_cursor_shape_locked
|
|
79
|
-
return unless @editor_bar_cursor_active
|
|
101
|
+
def restore_editor_cursor_shape_locked(force: false)
|
|
102
|
+
return unless force || @editor_bar_cursor_active
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
print_output_locked(CURSOR_SHAPE_DEFAULT)
|
|
82
105
|
@editor_bar_cursor_active = false
|
|
83
106
|
end
|
|
84
107
|
|
|
@@ -87,11 +110,6 @@ module Kward
|
|
|
87
110
|
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
88
111
|
end
|
|
89
112
|
|
|
90
|
-
def make_room_for_composer_after_handoff_locked
|
|
91
|
-
rows, = composer_layout(screen_width, screen_height)
|
|
92
|
-
@output_io.print("\r\n" * rows.length)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
113
|
def ensure_scroll_region_locked(row_count, redraw_transcript: true, width: screen_width, height: screen_height)
|
|
96
114
|
new_reserved_rows = [[row_count, 1].max, [height - 1, 1].max].min
|
|
97
115
|
return if @reserved_rows == new_reserved_rows && @last_height == height
|
|
@@ -100,13 +118,15 @@ module Kward
|
|
|
100
118
|
old_top = [height - old_reserved_rows + 1, 1].max
|
|
101
119
|
@reserved_rows = new_reserved_rows
|
|
102
120
|
new_top = composer_top_row(height)
|
|
103
|
-
|
|
121
|
+
print_output_locked(TerminalSequences.scroll_region(1, transcript_bottom_row(height)))
|
|
104
122
|
clear_screen_rows_locked(old_top, new_top - 1) if new_top > old_top
|
|
105
123
|
@last_composer_rows = []
|
|
106
124
|
redraw_transcript_locked(width: width, height: height) if redraw_transcript && new_reserved_rows < old_reserved_rows
|
|
107
125
|
end
|
|
108
126
|
|
|
109
127
|
def handle_resize_locked
|
|
128
|
+
return false if terminal_owned_by_child_locked?
|
|
129
|
+
|
|
110
130
|
current_width, current_height = screen_size
|
|
111
131
|
return false if current_width == @last_width && current_height == @last_height
|
|
112
132
|
|
|
@@ -127,7 +147,7 @@ module Kward
|
|
|
127
147
|
end
|
|
128
148
|
|
|
129
149
|
def restore_scroll_region_locked
|
|
130
|
-
|
|
150
|
+
print_output_locked(TerminalSequences.restore_scroll_region)
|
|
131
151
|
@reserved_rows = 0
|
|
132
152
|
end
|
|
133
153
|
|
|
@@ -143,15 +163,15 @@ module Kward
|
|
|
143
163
|
|
|
144
164
|
move_to_screen(top + index, 1)
|
|
145
165
|
if row
|
|
146
|
-
|
|
166
|
+
print_output_locked(row)
|
|
147
167
|
else
|
|
148
|
-
|
|
168
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
149
169
|
end
|
|
150
170
|
end
|
|
151
171
|
|
|
152
172
|
rows.length.upto(rows.length + rows_to_clear - 1) do |index|
|
|
153
173
|
move_to_screen(top + index, 1)
|
|
154
|
-
|
|
174
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
@last_composer_rows = rows.dup
|
|
@@ -191,12 +211,12 @@ module Kward
|
|
|
191
211
|
def clear_screen_rows_locked(top, bottom)
|
|
192
212
|
top.upto(bottom) do |row|
|
|
193
213
|
move_to_screen(row, 1)
|
|
194
|
-
|
|
214
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
195
215
|
end
|
|
196
216
|
end
|
|
197
217
|
|
|
198
218
|
def move_to_screen(row, col)
|
|
199
|
-
|
|
219
|
+
print_output_locked(TerminalSequences.move_to(row, col))
|
|
200
220
|
end
|
|
201
221
|
|
|
202
222
|
def screen_size
|
|
@@ -324,7 +324,7 @@ module Kward
|
|
|
324
324
|
@select_state[:selection_index] = result[:selection_index].to_i if result.key?(:selection_index)
|
|
325
325
|
end
|
|
326
326
|
render_prompt_locked
|
|
327
|
-
|
|
327
|
+
flush_output_locked
|
|
328
328
|
end
|
|
329
329
|
SELECT_CONTINUE
|
|
330
330
|
end
|
|
@@ -342,7 +342,7 @@ module Kward
|
|
|
342
342
|
@asking = true
|
|
343
343
|
reset_spinner_locked
|
|
344
344
|
render_prompt_locked
|
|
345
|
-
|
|
345
|
+
flush_output_locked
|
|
346
346
|
end
|
|
347
347
|
end
|
|
348
348
|
|
|
@@ -352,7 +352,7 @@ module Kward
|
|
|
352
352
|
@busy_activity = "streaming"
|
|
353
353
|
@select_state&.delete(:busy_activity)
|
|
354
354
|
render_prompt_locked if @asking
|
|
355
|
-
|
|
355
|
+
flush_output_locked
|
|
356
356
|
end
|
|
357
357
|
end
|
|
358
358
|
|
|
@@ -362,7 +362,7 @@ module Kward
|
|
|
362
362
|
spun = tick_spinner_locked
|
|
363
363
|
footer_refreshed = tick_footer_locked
|
|
364
364
|
render_prompt_locked if resized || spun || footer_refreshed
|
|
365
|
-
|
|
365
|
+
flush_output_locked if resized || spun || footer_refreshed
|
|
366
366
|
end
|
|
367
367
|
end
|
|
368
368
|
|
|
@@ -656,7 +656,7 @@ module Kward
|
|
|
656
656
|
self.composer_cursor = 0
|
|
657
657
|
@asking = true
|
|
658
658
|
render_prompt_locked if render
|
|
659
|
-
|
|
659
|
+
flush_output_locked
|
|
660
660
|
end
|
|
661
661
|
end
|
|
662
662
|
|
|
@@ -19,7 +19,7 @@ module Kward
|
|
|
19
19
|
@stream_state.finish_block if finish
|
|
20
20
|
restore_composer_cursor_locked unless @restoring_transcript
|
|
21
21
|
end
|
|
22
|
-
|
|
22
|
+
flush_output_locked unless @restoring_transcript
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def write_transcript_text_locked(text)
|
|
@@ -32,7 +32,7 @@ module Kward
|
|
|
32
32
|
width, height = screen_size
|
|
33
33
|
output_text = terminal_newlines(text.to_s)
|
|
34
34
|
advance_pending_stream_wrap_locked(output_text, width: width, height: height)
|
|
35
|
-
|
|
35
|
+
print_output_locked(output_text)
|
|
36
36
|
update_stream_position(output_text, width: width)
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -58,7 +58,7 @@ module Kward
|
|
|
58
58
|
return if rows.empty?
|
|
59
59
|
|
|
60
60
|
move_to_screen(1, 1)
|
|
61
|
-
|
|
61
|
+
print_output_locked(terminal_newlines(rows.join("\n")))
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def transcript_viewport_rows(row_count, width)
|
|
@@ -98,7 +98,7 @@ module Kward
|
|
|
98
98
|
return if output_text.empty? || output_text.start_with?("\r", "\n")
|
|
99
99
|
|
|
100
100
|
move_to_screen(transcript_bottom_row(height), width)
|
|
101
|
-
|
|
101
|
+
print_output_locked("\r\n")
|
|
102
102
|
@stream_state.clear_pending_wrap
|
|
103
103
|
end
|
|
104
104
|
|