kward 0.67.0 → 0.68.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 +26 -0
- data/Gemfile.lock +2 -2
- data/README.md +5 -5
- data/doc/authentication.md +24 -1
- data/doc/configuration.md +9 -2
- data/doc/extensibility.md +1 -1
- data/doc/getting-started.md +4 -6
- data/doc/plugins.md +0 -2
- data/doc/releasing.md +7 -8
- data/doc/rpc.md +6 -6
- data/doc/usage.md +5 -2
- data/doc/web-search.md +2 -2
- data/kward.gemspec +4 -0
- data/lib/kward/agent.rb +29 -2
- data/lib/kward/ansi.rb +3 -0
- data/lib/kward/auth/anthropic_oauth.rb +291 -0
- data/lib/kward/auth/file.rb +2 -0
- data/lib/kward/auth/github_oauth.rb +3 -0
- data/lib/kward/auth/openai_oauth.rb +4 -0
- data/lib/kward/auth/openrouter_api_key.rb +2 -0
- data/lib/kward/cancellation.rb +3 -0
- data/lib/kward/cli/auth_commands.rb +82 -0
- data/lib/kward/cli/commands.rb +222 -0
- data/lib/kward/cli/compaction.rb +25 -0
- data/lib/kward/cli/doctor.rb +121 -0
- data/lib/kward/cli/interactive_turn.rb +225 -0
- data/lib/kward/cli/memory_commands.rb +133 -0
- data/lib/kward/cli/plugins.rb +112 -0
- data/lib/kward/cli/prompt_interface.rb +132 -0
- data/lib/kward/cli/rendering.rb +389 -0
- data/lib/kward/cli/runtime_helpers.rb +159 -0
- data/lib/kward/cli/sessions.rb +376 -0
- data/lib/kward/cli/settings.rb +663 -0
- data/lib/kward/cli/slash_commands.rb +112 -0
- data/lib/kward/cli/stats.rb +64 -0
- data/lib/kward/cli/tool_summaries.rb +153 -0
- data/lib/kward/cli.rb +38 -2790
- data/lib/kward/cli_transcript_formatter.rb +4 -7
- data/lib/kward/clipboard.rb +1 -0
- data/lib/kward/compaction/file_operation_tracker.rb +3 -0
- data/lib/kward/compactor.rb +29 -7
- data/lib/kward/config_files.rb +33 -24
- data/lib/kward/conversation.rb +70 -5
- data/lib/kward/events.rb +2 -0
- data/lib/kward/export_path.rb +2 -0
- data/lib/kward/image_attachments.rb +2 -0
- data/lib/kward/markdown_transcript.rb +2 -0
- data/lib/kward/memory/manager.rb +13 -0
- data/lib/kward/message_access.rb +23 -2
- data/lib/kward/message_text.rb +45 -0
- data/lib/kward/model/chat_invocation.rb +2 -0
- data/lib/kward/model/client.rb +295 -77
- data/lib/kward/model/context_overflow.rb +2 -0
- data/lib/kward/model/context_usage.rb +3 -0
- data/lib/kward/model/model_info.rb +143 -4
- data/lib/kward/model/payloads.rb +166 -13
- data/lib/kward/model/retry_message.rb +2 -0
- data/lib/kward/model/stream_parser.rb +129 -0
- data/lib/kward/pan/server.rb +3 -1
- data/lib/kward/plugin_registry.rb +12 -0
- data/lib/kward/private_file.rb +2 -0
- data/lib/kward/prompt_interface/banner.rb +3 -0
- data/lib/kward/prompt_interface/composer_controller.rb +262 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
- data/lib/kward/prompt_interface/composer_state.rb +221 -0
- data/lib/kward/prompt_interface/key_handler.rb +365 -0
- data/lib/kward/prompt_interface/layout.rb +31 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
- data/lib/kward/prompt_interface/question_prompt.rb +328 -0
- data/lib/kward/prompt_interface/runtime_state.rb +59 -0
- data/lib/kward/prompt_interface/screen.rb +186 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
- data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
- data/lib/kward/prompt_interface/stream_state.rb +65 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
- data/lib/kward/prompt_interface/transcript_renderer.rb +142 -0
- data/lib/kward/prompt_interface.rb +69 -1832
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +3 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/question_contract.rb +66 -0
- data/lib/kward/resources/avatar_kward_logo.rb +2 -0
- data/lib/kward/resources/pixel_logo.rb +2 -0
- data/lib/kward/rpc/attachment_normalizer.rb +60 -0
- data/lib/kward/rpc/auth_manager.rb +65 -11
- data/lib/kward/rpc/config_manager.rb +11 -0
- data/lib/kward/rpc/prompt_bridge.rb +5 -26
- data/lib/kward/rpc/redactor.rb +3 -0
- data/lib/kward/rpc/runtime_payloads.rb +4 -1
- data/lib/kward/rpc/server.rb +37 -10
- data/lib/kward/rpc/session_manager.rb +123 -347
- data/lib/kward/rpc/session_metrics.rb +68 -0
- data/lib/kward/rpc/session_tree.rb +48 -0
- data/lib/kward/rpc/session_tree_rows.rb +208 -0
- data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
- data/lib/kward/rpc/tool_metadata.rb +3 -0
- data/lib/kward/rpc/transcript_normalizer.rb +3 -0
- data/lib/kward/rpc/transport.rb +3 -0
- data/lib/kward/session_diff.rb +2 -0
- data/lib/kward/session_store.rb +125 -31
- data/lib/kward/session_trash.rb +1 -0
- data/lib/kward/session_tree_renderer.rb +8 -41
- data/lib/kward/session_tree_tool_display.rb +56 -0
- data/lib/kward/skills/registry.rb +3 -0
- data/lib/kward/starter_pack_installer.rb +1 -0
- data/lib/kward/steering.rb +2 -0
- data/lib/kward/telemetry/logger.rb +3 -0
- data/lib/kward/telemetry/stats.rb +3 -0
- data/lib/kward/tools/ask_user_question.rb +20 -32
- data/lib/kward/tools/base.rb +8 -0
- data/lib/kward/tools/code_search.rb +5 -0
- data/lib/kward/tools/edit_file.rb +5 -0
- data/lib/kward/tools/list_directory.rb +5 -0
- data/lib/kward/tools/read_file.rb +5 -0
- data/lib/kward/tools/read_skill.rb +5 -0
- data/lib/kward/tools/registry.rb +33 -2
- data/lib/kward/tools/run_shell_command.rb +5 -0
- data/lib/kward/tools/search/code.rb +7 -0
- data/lib/kward/tools/search/web.rb +17 -14
- data/lib/kward/tools/tool_call.rb +25 -5
- data/lib/kward/tools/web_search.rb +7 -1
- data/lib/kward/tools/write_file.rb +5 -0
- data/lib/kward/transcript_export.rb +2 -0
- data/lib/kward/version.rb +2 -1
- data/lib/kward/workspace.rb +45 -5
- metadata +43 -1
|
@@ -5,8 +5,34 @@ require "tty-reader"
|
|
|
5
5
|
require "tty-screen"
|
|
6
6
|
require_relative "ansi"
|
|
7
7
|
require_relative "prompt_interface/banner"
|
|
8
|
-
|
|
8
|
+
require_relative "prompt_interface/composer_state"
|
|
9
|
+
require_relative "prompt_interface/transcript_buffer"
|
|
10
|
+
require_relative "prompt_interface/transcript_renderer"
|
|
11
|
+
require_relative "prompt_interface/prompt_renderer"
|
|
12
|
+
require_relative "prompt_interface/stream_state"
|
|
13
|
+
require_relative "prompt_interface/slash_overlay"
|
|
14
|
+
require_relative "prompt_interface/selection_prompt"
|
|
15
|
+
require_relative "prompt_interface/question_prompt"
|
|
16
|
+
require_relative "prompt_interface/overlay_renderer"
|
|
17
|
+
require_relative "prompt_interface/composer_renderer"
|
|
18
|
+
require_relative "prompt_interface/composer_controller"
|
|
19
|
+
require_relative "prompt_interface/layout"
|
|
20
|
+
require_relative "prompt_interface/screen"
|
|
21
|
+
require_relative "prompt_interface/key_handler"
|
|
22
|
+
require_relative "prompt_interface/runtime_state"
|
|
23
|
+
|
|
24
|
+
# Namespace for the Kward CLI agent runtime.
|
|
9
25
|
module Kward
|
|
26
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
27
|
+
#
|
|
28
|
+
# `PromptInterface` owns terminal mechanics: keyboard input, composer state,
|
|
29
|
+
# transcript rendering, overlays, footer updates, and terminal escape sequence
|
|
30
|
+
# setup/restore. It should not own agent turns, sessions, model calls, or tool
|
|
31
|
+
# policy; `CLI` coordinates those and calls this object for display/input.
|
|
32
|
+
#
|
|
33
|
+
# The implementation is split into small mixin modules under
|
|
34
|
+
# `prompt_interface/` to keep rendering, layout, keyboard handling, and runtime
|
|
35
|
+
# state readable while sharing one terminal state object.
|
|
10
36
|
class PromptInterface
|
|
11
37
|
HELP_TEXT = "Enter sends • Shift+Enter inserts newline • ↑/↓ history • Ctrl+D exits empty prompt".freeze
|
|
12
38
|
BUSY_HELP_TEXT = "Ctrl+C cancels".freeze
|
|
@@ -17,6 +43,19 @@ module Kward
|
|
|
17
43
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
18
44
|
BANNER_LOGO_PIXELS = Banner::LOGO_PIXELS
|
|
19
45
|
BANNER_MESSAGE = Banner::MESSAGE
|
|
46
|
+
|
|
47
|
+
include SlashOverlay
|
|
48
|
+
include SelectionPrompt
|
|
49
|
+
include QuestionPrompt
|
|
50
|
+
include OverlayRenderer
|
|
51
|
+
include ComposerRenderer
|
|
52
|
+
include ComposerController
|
|
53
|
+
include Layout
|
|
54
|
+
include Screen
|
|
55
|
+
include KeyHandler
|
|
56
|
+
include RuntimeState
|
|
57
|
+
include TranscriptRenderer
|
|
58
|
+
include PromptRenderer
|
|
20
59
|
KEYBOARD_PROTOCOL_ENABLE = "\e[>1u".freeze
|
|
21
60
|
KEYBOARD_PROTOCOL_RESTORE = "\e[<u".freeze
|
|
22
61
|
BRACKETED_PASTE_ENABLE = "\e[?2004h".freeze
|
|
@@ -32,6 +71,7 @@ module Kward
|
|
|
32
71
|
CANCEL_INPUT = :cancel_input
|
|
33
72
|
SELECT_CANCEL = :select_cancel
|
|
34
73
|
|
|
74
|
+
# Submitted input string carrying optional display text for transcripts.
|
|
35
75
|
class SubmittedInput < String
|
|
36
76
|
attr_reader :display_input
|
|
37
77
|
|
|
@@ -46,8 +86,9 @@ module Kward
|
|
|
46
86
|
@output_io = output
|
|
47
87
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
48
88
|
@mutex = Mutex.new
|
|
49
|
-
@
|
|
50
|
-
|
|
89
|
+
@composer = ComposerState.new
|
|
90
|
+
self.composer_input = @composer.input
|
|
91
|
+
self.composer_cursor = @composer.cursor
|
|
51
92
|
@started = false
|
|
52
93
|
@asking = false
|
|
53
94
|
@busy = false
|
|
@@ -59,27 +100,17 @@ module Kward
|
|
|
59
100
|
@last_footer_refresh = monotonic_now
|
|
60
101
|
@prompt_label = "You>"
|
|
61
102
|
@assistant_label = "Assistant"
|
|
62
|
-
@
|
|
103
|
+
@stream_state = StreamState.new
|
|
63
104
|
@rendered_rows = 0
|
|
64
105
|
@last_composer_rows = []
|
|
65
106
|
@cursor_rendered_row = 0
|
|
66
|
-
@
|
|
67
|
-
@stream_pending_wrap = false
|
|
68
|
-
@transcript_buffer = +""
|
|
69
|
-
@transcript_display_rows_cache_width = nil
|
|
70
|
-
@transcript_display_rows_cache = nil
|
|
107
|
+
@transcript_buffer = TranscriptBuffer.new(limit: TRANSCRIPT_BUFFER_LIMIT)
|
|
71
108
|
@visual_banner_count = 0
|
|
72
109
|
@transcript_viewport_rows = 0
|
|
73
110
|
@restoring_transcript = false
|
|
74
111
|
@pending_keys = []
|
|
75
|
-
@attachments = []
|
|
76
|
-
@kill_buffer = ""
|
|
77
112
|
@original_console_mode = nil
|
|
78
113
|
@raw_mode_active = false
|
|
79
|
-
@history = []
|
|
80
|
-
@history_index = nil
|
|
81
|
-
@history_draft = nil
|
|
82
|
-
@prefill_input = nil
|
|
83
114
|
@slash_commands = normalize_slash_commands(slash_commands)
|
|
84
115
|
@slash_selection_index = 0
|
|
85
116
|
@slash_overlay_dismissed_input = nil
|
|
@@ -135,7 +166,7 @@ module Kward
|
|
|
135
166
|
if @restoring_transcript
|
|
136
167
|
write_transcript_text_locked(text)
|
|
137
168
|
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
138
|
-
@
|
|
169
|
+
@stream_state.finish_block
|
|
139
170
|
next
|
|
140
171
|
end
|
|
141
172
|
|
|
@@ -143,7 +174,7 @@ module Kward
|
|
|
143
174
|
clear_prompt_for_output_locked
|
|
144
175
|
write_transcript_text_locked(text)
|
|
145
176
|
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
146
|
-
@
|
|
177
|
+
@stream_state.finish_block
|
|
147
178
|
render_prompt_after_output_locked
|
|
148
179
|
end
|
|
149
180
|
@output_io.flush
|
|
@@ -159,7 +190,7 @@ module Kward
|
|
|
159
190
|
text = message.to_s
|
|
160
191
|
write_visual_transcript_text_locked(text)
|
|
161
192
|
write_visual_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
162
|
-
@
|
|
193
|
+
@stream_state.finish_block
|
|
163
194
|
render_prompt_after_output_locked
|
|
164
195
|
end
|
|
165
196
|
@output_io.flush
|
|
@@ -170,13 +201,11 @@ module Kward
|
|
|
170
201
|
@mutex.synchronize do
|
|
171
202
|
clear_prompt_for_output_locked
|
|
172
203
|
@output_io.print(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
173
|
-
@transcript_buffer
|
|
174
|
-
invalidate_transcript_display_rows_cache
|
|
204
|
+
@transcript_buffer.clear
|
|
175
205
|
@visual_banner_count = 0
|
|
176
206
|
@transcript_viewport_rows = 0
|
|
177
|
-
@
|
|
178
|
-
@
|
|
179
|
-
@stream_pending_wrap = false
|
|
207
|
+
@stream_state.finish_block
|
|
208
|
+
@stream_state.reset
|
|
180
209
|
@restoring_transcript = true
|
|
181
210
|
end
|
|
182
211
|
|
|
@@ -195,13 +224,13 @@ module Kward
|
|
|
195
224
|
was_composing = @started && @asking
|
|
196
225
|
start
|
|
197
226
|
@mutex.synchronize do
|
|
198
|
-
preserve_input = was_composing && !@busy &&
|
|
227
|
+
preserve_input = was_composing && !@busy && !composer_input.empty?
|
|
199
228
|
@prompt_label = message.to_s
|
|
200
229
|
unless preserve_input
|
|
201
|
-
|
|
202
|
-
@prefill_input = nil
|
|
203
|
-
|
|
204
|
-
@
|
|
230
|
+
self.composer_input = @composer.prefill_input.to_s
|
|
231
|
+
@composer.prefill_input = nil
|
|
232
|
+
self.composer_cursor = composer_input.length
|
|
233
|
+
@composer.clear_attachments
|
|
205
234
|
reset_history_navigation
|
|
206
235
|
end
|
|
207
236
|
@pending_keys.clear
|
|
@@ -247,10 +276,10 @@ module Kward
|
|
|
247
276
|
start
|
|
248
277
|
@mutex.synchronize do
|
|
249
278
|
@prompt_label = message.to_s
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
@
|
|
253
|
-
|
|
279
|
+
self.composer_input = ""
|
|
280
|
+
self.composer_cursor = 0
|
|
281
|
+
@composer.clear_attachments
|
|
282
|
+
@pending_keys.clear
|
|
254
283
|
@asking = true
|
|
255
284
|
@busy = false
|
|
256
285
|
@queued_count = 0
|
|
@@ -324,10 +353,10 @@ module Kward
|
|
|
324
353
|
@mutex.synchronize do
|
|
325
354
|
@prompt_label = message.to_s
|
|
326
355
|
@busy_activity = normalize_busy_activity(activity)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
@
|
|
330
|
-
|
|
356
|
+
self.composer_input = ""
|
|
357
|
+
self.composer_cursor = 0
|
|
358
|
+
@composer.clear_attachments
|
|
359
|
+
@pending_keys.clear
|
|
331
360
|
@asking = true
|
|
332
361
|
@busy = true
|
|
333
362
|
@queued_count = 0
|
|
@@ -411,7 +440,7 @@ module Kward
|
|
|
411
440
|
@visual_banner_count += 1
|
|
412
441
|
invalidate_transcript_display_rows_cache
|
|
413
442
|
remember_transcript_viewport_locked(height)
|
|
414
|
-
@
|
|
443
|
+
@stream_state.finish_block
|
|
415
444
|
restore_composer_cursor_locked
|
|
416
445
|
end
|
|
417
446
|
@output_io.flush
|
|
@@ -452,13 +481,11 @@ module Kward
|
|
|
452
481
|
|
|
453
482
|
def clear_transcript
|
|
454
483
|
@mutex.synchronize do
|
|
455
|
-
@transcript_buffer
|
|
456
|
-
invalidate_transcript_display_rows_cache
|
|
484
|
+
@transcript_buffer.clear
|
|
457
485
|
@visual_banner_count = 0
|
|
458
486
|
@transcript_viewport_rows = 0
|
|
459
|
-
@
|
|
460
|
-
@
|
|
461
|
-
@stream_pending_wrap = false
|
|
487
|
+
@stream_state.finish_block
|
|
488
|
+
@stream_state.reset
|
|
462
489
|
width, height = screen_size
|
|
463
490
|
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
464
491
|
@output_io.flush
|
|
@@ -467,2000 +494,210 @@ module Kward
|
|
|
467
494
|
|
|
468
495
|
private
|
|
469
496
|
|
|
470
|
-
def enter_raw_mode_locked
|
|
471
|
-
return unless @input_io.respond_to?(:tty?) && @input_io.tty?
|
|
472
|
-
return unless @input_io.respond_to?(:console_mode) && @input_io.respond_to?(:console_mode=)
|
|
473
|
-
return if @raw_mode_active
|
|
474
|
-
|
|
475
|
-
@original_console_mode = @input_io.console_mode
|
|
476
|
-
raw_mode = @input_io.console_mode.raw
|
|
477
|
-
raw_mode.echo = false
|
|
478
|
-
@input_io.console_mode = raw_mode
|
|
479
|
-
@raw_mode_active = true
|
|
480
|
-
rescue StandardError
|
|
481
|
-
@original_console_mode = nil
|
|
482
|
-
@raw_mode_active = false
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def restore_console_mode_locked
|
|
486
|
-
return unless @raw_mode_active
|
|
487
|
-
|
|
488
|
-
@input_io.console_mode = @original_console_mode if @original_console_mode
|
|
489
|
-
ensure
|
|
490
|
-
@original_console_mode = nil
|
|
491
|
-
@raw_mode_active = false
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def write_stream_block_locked(label, delta, finish: false)
|
|
495
|
-
with_synchronized_output_locked do
|
|
496
|
-
prepare_transcript_output_locked unless @restoring_transcript
|
|
497
|
-
if label && @stream_block != label
|
|
498
|
-
ensure_transcript_block_separator_locked
|
|
499
|
-
write_transcript_text_locked("#{colored("#{transcript_label(label)}>", label_color(label), :bold)}\n")
|
|
500
|
-
@stream_block = label
|
|
501
|
-
end
|
|
502
|
-
write_transcript_text_locked(delta) unless delta.empty?
|
|
503
|
-
write_transcript_text_locked("\n") if finish && @stream_block
|
|
504
|
-
@stream_block = nil if finish
|
|
505
|
-
restore_composer_cursor_locked unless @restoring_transcript
|
|
506
|
-
end
|
|
507
|
-
@output_io.flush unless @restoring_transcript
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
def with_synchronized_output_locked
|
|
511
|
-
if @restoring_transcript || @synchronized_output_depth.positive?
|
|
512
|
-
yield
|
|
513
|
-
return
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
synchronized = true
|
|
517
|
-
@synchronized_output_depth += 1
|
|
518
|
-
@output_io.print(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
519
|
-
yield
|
|
520
|
-
ensure
|
|
521
|
-
if synchronized
|
|
522
|
-
@synchronized_output_depth -= 1
|
|
523
|
-
@output_io.print(SYNCHRONIZED_OUTPUT_DISABLE) if @synchronized_output_depth.zero?
|
|
524
|
-
end
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
def write_transcript_text_locked(text)
|
|
528
|
-
append_transcript_buffer(text.to_s)
|
|
529
|
-
remember_transcript_viewport_locked unless text.to_s.empty?
|
|
530
|
-
write_visual_transcript_text_locked(text)
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
def write_visual_transcript_text_locked(text)
|
|
534
|
-
width, height = screen_size
|
|
535
|
-
output_text = terminal_newlines(text.to_s)
|
|
536
|
-
advance_pending_stream_wrap_locked(output_text, width: width, height: height)
|
|
537
|
-
@output_io.print(output_text)
|
|
538
|
-
update_stream_position(output_text, width: width)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def append_transcript_buffer(text)
|
|
542
|
-
@transcript_buffer << ANSI.sanitize_transcript(text)
|
|
543
|
-
invalidate_transcript_display_rows_cache
|
|
544
|
-
return if @transcript_buffer.length <= TRANSCRIPT_BUFFER_LIMIT
|
|
545
|
-
|
|
546
|
-
@transcript_buffer = @transcript_buffer[-TRANSCRIPT_BUFFER_LIMIT, TRANSCRIPT_BUFFER_LIMIT]
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
def invalidate_transcript_display_rows_cache
|
|
550
|
-
@transcript_display_rows_cache_width = nil
|
|
551
|
-
@transcript_display_rows_cache = nil
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def ensure_transcript_block_separator_locked
|
|
555
|
-
return if @transcript_buffer.empty? || @transcript_buffer.end_with?("\n\n")
|
|
556
|
-
|
|
557
|
-
write_transcript_text_locked(@transcript_buffer.end_with?("\n") ? "\n" : "\n\n")
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
def terminal_newlines(text)
|
|
561
|
-
text.gsub(/\r\n|\r|\n/, "\r\n")
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
def reset_spinner_locked
|
|
565
|
-
@spinner_frame_index = 0
|
|
566
|
-
@last_spinner_tick = monotonic_now
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
def normalize_busy_activity(activity)
|
|
570
|
-
text = activity.to_s.gsub(/\s+/, " ").strip
|
|
571
|
-
text.empty? ? "streaming" : text
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
def tick_spinner_locked
|
|
575
|
-
return false unless @busy && @queued_count.zero? && @started && @asking
|
|
576
|
-
|
|
577
|
-
now = monotonic_now
|
|
578
|
-
elapsed = now - @last_spinner_tick
|
|
579
|
-
return false if elapsed < SPINNER_INTERVAL
|
|
580
|
-
|
|
581
|
-
steps = (elapsed / SPINNER_INTERVAL).floor
|
|
582
|
-
@spinner_frame_index = (@spinner_frame_index + steps) % SPINNER_FRAMES.length
|
|
583
|
-
@last_spinner_tick += steps * SPINNER_INTERVAL
|
|
584
|
-
true
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
def spinner_frame
|
|
588
|
-
SPINNER_FRAMES[@spinner_frame_index % SPINNER_FRAMES.length]
|
|
589
|
-
end
|
|
590
|
-
|
|
591
|
-
def tick_footer_locked
|
|
592
|
-
return false unless @footer && @started && @asking
|
|
593
|
-
|
|
594
|
-
now = monotonic_now
|
|
595
|
-
elapsed = now - @last_footer_refresh
|
|
596
|
-
return false if elapsed < FOOTER_REFRESH_INTERVAL
|
|
597
|
-
|
|
598
|
-
steps = (elapsed / FOOTER_REFRESH_INTERVAL).floor
|
|
599
|
-
@last_footer_refresh += steps * FOOTER_REFRESH_INTERVAL
|
|
600
|
-
true
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
def monotonic_now
|
|
604
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
def submit_input
|
|
608
|
-
value = submitted_input
|
|
609
|
-
add_history(@input)
|
|
610
|
-
if @busy
|
|
611
|
-
clear_prompt_for_output_locked
|
|
612
|
-
@input = ""
|
|
613
|
-
@cursor = 0
|
|
614
|
-
@attachments.clear
|
|
615
|
-
reset_history_navigation
|
|
616
|
-
@asking = true
|
|
617
|
-
render_prompt_after_output_locked
|
|
618
|
-
else
|
|
619
|
-
clear_prompt_locked
|
|
620
|
-
@input = ""
|
|
621
|
-
@cursor = 0
|
|
622
|
-
@attachments.clear
|
|
623
|
-
@asking = false
|
|
624
|
-
@rendered_rows = 0
|
|
625
|
-
@cursor_rendered_row = 0
|
|
626
|
-
end
|
|
627
|
-
@output_io.flush
|
|
628
|
-
value
|
|
629
|
-
end
|
|
630
|
-
|
|
631
|
-
def submitted_input
|
|
632
|
-
return @input if @attachments.empty?
|
|
633
|
-
|
|
634
|
-
sources = @attachments.map { |attachment| attachment[:source_text].to_s }.reject(&:empty?)
|
|
635
|
-
display_input = @input.to_s.rstrip
|
|
636
|
-
full_input = [display_input, *sources].reject { |part| part.to_s.strip.empty? }.join("\n")
|
|
637
|
-
SubmittedInput.new(full_input, display_input: display_input)
|
|
638
|
-
end
|
|
639
|
-
|
|
640
|
-
def exit_input
|
|
641
|
-
if @busy
|
|
642
|
-
clear_prompt_for_output_locked
|
|
643
|
-
@input = ""
|
|
644
|
-
@cursor = 0
|
|
645
|
-
@attachments.clear
|
|
646
|
-
@asking = true
|
|
647
|
-
render_prompt_after_output_locked
|
|
648
|
-
else
|
|
649
|
-
clear_prompt_locked
|
|
650
|
-
@input = ""
|
|
651
|
-
@cursor = 0
|
|
652
|
-
@attachments.clear
|
|
653
|
-
@asking = false
|
|
654
|
-
@rendered_rows = 0
|
|
655
|
-
@cursor_rendered_row = 0
|
|
656
|
-
end
|
|
657
|
-
@output_io.flush
|
|
658
|
-
EXIT_INPUT
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
def read_key(nonblock: false)
|
|
662
|
-
pending = @pending_keys.shift unless @pending_keys.empty?
|
|
663
|
-
return pending if pending
|
|
664
|
-
|
|
665
|
-
@reader.read_keypress(echo: false, raw: true, nonblock: nonblock)
|
|
666
|
-
rescue TTY::Reader::InputInterrupt
|
|
667
|
-
"\x03"
|
|
668
|
-
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
669
|
-
nil
|
|
670
|
-
end
|
|
671
|
-
|
|
672
|
-
def handle_key(key)
|
|
673
|
-
return submit_input if key.nil?
|
|
674
|
-
return if handle_bracketed_paste_key(key)
|
|
675
|
-
|
|
676
|
-
csi_result = handle_csi_u_key(key)
|
|
677
|
-
return csi_result unless csi_result == false
|
|
678
|
-
return if handle_shift_enter_key(key)
|
|
679
|
-
if key.is_a?(String) && key.length > 1
|
|
680
|
-
token = next_key_token(key)
|
|
681
|
-
if token.length < key.length
|
|
682
|
-
queue_pending_keys(key[token.length..])
|
|
683
|
-
return handle_key(token)
|
|
684
|
-
end
|
|
685
|
-
end
|
|
686
|
-
|
|
687
|
-
binding_result = handle_composer_key_binding(key)
|
|
688
|
-
return binding_result unless binding_result == false
|
|
689
|
-
|
|
690
|
-
key_name = @reader.console.keys[key]
|
|
691
|
-
case key_name
|
|
692
|
-
when :return, :enter
|
|
693
|
-
submit_input
|
|
694
|
-
when :backspace
|
|
695
|
-
delete_before_cursor
|
|
696
|
-
when :delete
|
|
697
|
-
delete_at_cursor
|
|
698
|
-
when :ctrl_d
|
|
699
|
-
delete_at_cursor_or_exit
|
|
700
|
-
when :ctrl_c
|
|
701
|
-
cancel_input_or_interrupt
|
|
702
|
-
when :ctrl_a
|
|
703
|
-
move_to_start_of_line
|
|
704
|
-
when :ctrl_e
|
|
705
|
-
move_to_end_of_line
|
|
706
|
-
when :ctrl_b
|
|
707
|
-
move_cursor_left
|
|
708
|
-
when :ctrl_f
|
|
709
|
-
move_cursor_right
|
|
710
|
-
when :ctrl_w
|
|
711
|
-
delete_word_before_cursor
|
|
712
|
-
when :ctrl_u
|
|
713
|
-
kill_line_before_cursor
|
|
714
|
-
when :ctrl_k
|
|
715
|
-
kill_line_after_cursor
|
|
716
|
-
when :ctrl_y
|
|
717
|
-
yank_kill_buffer
|
|
718
|
-
when :ctrl_l
|
|
719
|
-
redraw_screen_locked
|
|
720
|
-
when :left
|
|
721
|
-
move_cursor_left
|
|
722
|
-
when :right
|
|
723
|
-
move_cursor_right
|
|
724
|
-
when :home
|
|
725
|
-
move_to_start_of_line
|
|
726
|
-
when :end
|
|
727
|
-
move_to_end_of_line
|
|
728
|
-
when :up
|
|
729
|
-
slash_overlay_visible? ? select_previous_slash_command : recall_previous_history
|
|
730
|
-
when :down
|
|
731
|
-
slash_overlay_visible? ? select_next_slash_command : recall_next_history
|
|
732
|
-
else
|
|
733
|
-
case key
|
|
734
|
-
when "\n", "\r"
|
|
735
|
-
submit_input
|
|
736
|
-
when "\t"
|
|
737
|
-
complete_selected_slash_command || insert_key(key)
|
|
738
|
-
when "\b", "\x7F"
|
|
739
|
-
delete_before_cursor
|
|
740
|
-
when "\x04"
|
|
741
|
-
delete_at_cursor_or_exit
|
|
742
|
-
when "\x03"
|
|
743
|
-
cancel_input_or_interrupt
|
|
744
|
-
when "\e"
|
|
745
|
-
handle_escape_sequence
|
|
746
|
-
else
|
|
747
|
-
insert_key(key)
|
|
748
|
-
end
|
|
749
|
-
end
|
|
750
|
-
end
|
|
751
|
-
|
|
752
|
-
def cancel_input_or_interrupt
|
|
753
|
-
return CANCEL_INPUT if @busy
|
|
754
|
-
|
|
755
|
-
raise Interrupt
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
def handle_escape_sequence
|
|
759
|
-
pending_sequence = read_pending_escape_sequence
|
|
760
|
-
return true if pending_sequence.empty? && dismiss_slash_overlay
|
|
761
|
-
|
|
762
|
-
full_sequence = "\e#{pending_sequence}"
|
|
763
|
-
sequence = next_key_token(full_sequence)
|
|
764
|
-
queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
|
|
765
|
-
return true if sequence == "\e" && dismiss_slash_overlay
|
|
766
|
-
return true if handle_shift_enter_key(sequence)
|
|
767
|
-
|
|
768
|
-
binding_result = handle_composer_key_binding(sequence)
|
|
769
|
-
return binding_result unless binding_result == false
|
|
770
|
-
|
|
771
|
-
key_name = @reader.console.keys[sequence]
|
|
772
|
-
case key_name
|
|
773
|
-
when :up
|
|
774
|
-
slash_overlay_visible? ? select_previous_slash_command : recall_previous_history
|
|
775
|
-
when :down
|
|
776
|
-
slash_overlay_visible? ? select_next_slash_command : recall_next_history
|
|
777
|
-
when :left
|
|
778
|
-
move_cursor_left
|
|
779
|
-
when :right
|
|
780
|
-
move_cursor_right
|
|
781
|
-
when :home
|
|
782
|
-
move_to_start_of_line
|
|
783
|
-
when :end
|
|
784
|
-
move_to_end_of_line
|
|
785
|
-
when :delete
|
|
786
|
-
delete_at_cursor
|
|
787
|
-
end
|
|
788
|
-
true
|
|
789
|
-
end
|
|
790
|
-
|
|
791
|
-
def ask_single_user_question(question, index, total)
|
|
792
|
-
@mutex.synchronize do
|
|
793
|
-
@prompt_label = "Answer>"
|
|
794
|
-
@input = ""
|
|
795
|
-
@cursor = 0
|
|
796
|
-
@pending_keys.clear
|
|
797
|
-
@asking = true
|
|
798
|
-
@busy = false
|
|
799
|
-
@queued_count = 0
|
|
800
|
-
@question_state = {
|
|
801
|
-
question: question[:question] || question["question"],
|
|
802
|
-
header: question[:header] || question["header"],
|
|
803
|
-
options: question[:options] || question["options"],
|
|
804
|
-
selection_index: 0,
|
|
805
|
-
index: index,
|
|
806
|
-
total: total
|
|
807
|
-
}
|
|
808
|
-
reset_history_navigation
|
|
809
|
-
render_prompt_locked
|
|
810
|
-
end
|
|
811
|
-
|
|
812
|
-
loop do
|
|
813
|
-
key = read_key(nonblock: true)
|
|
814
|
-
result = nil
|
|
815
|
-
@mutex.synchronize do
|
|
816
|
-
if key.nil?
|
|
817
|
-
resized = handle_resize_locked
|
|
818
|
-
footer_refreshed = tick_footer_locked
|
|
819
|
-
render_prompt_locked if resized || footer_refreshed
|
|
820
|
-
else
|
|
821
|
-
result = handle_question_key(key)
|
|
822
|
-
render_prompt_locked unless result.is_a?(Hash) || result == SELECT_CANCEL
|
|
823
|
-
end
|
|
824
|
-
end
|
|
825
|
-
|
|
826
|
-
return result if result.is_a?(Hash) || result == SELECT_CANCEL
|
|
827
|
-
|
|
828
|
-
sleep 0.02 if key.nil?
|
|
829
|
-
end
|
|
830
|
-
end
|
|
831
|
-
|
|
832
|
-
def begin_question_prompt_state
|
|
833
|
-
{
|
|
834
|
-
prompt_label: @prompt_label,
|
|
835
|
-
input: @input,
|
|
836
|
-
cursor: @cursor,
|
|
837
|
-
asking: @asking,
|
|
838
|
-
busy: @busy,
|
|
839
|
-
queued_count: @queued_count,
|
|
840
|
-
steered_count: @steered_count,
|
|
841
|
-
pending_keys: @pending_keys.dup,
|
|
842
|
-
select_state: @select_state
|
|
843
|
-
}
|
|
844
|
-
end
|
|
845
|
-
|
|
846
|
-
def finish_question_prompt(saved_state)
|
|
847
|
-
@mutex.synchronize do
|
|
848
|
-
@question_state = nil
|
|
849
|
-
@select_state = saved_state[:select_state]
|
|
850
|
-
@prompt_label = saved_state[:prompt_label]
|
|
851
|
-
@input = saved_state[:input]
|
|
852
|
-
@cursor = saved_state[:cursor]
|
|
853
|
-
@asking = saved_state[:asking]
|
|
854
|
-
@busy = saved_state[:busy]
|
|
855
|
-
@queued_count = saved_state[:queued_count]
|
|
856
|
-
@steered_count = saved_state[:steered_count]
|
|
857
|
-
@pending_keys = saved_state[:pending_keys]
|
|
858
|
-
render_prompt_locked if @started && @asking
|
|
859
|
-
@output_io.flush
|
|
860
|
-
end
|
|
861
|
-
end
|
|
862
|
-
|
|
863
|
-
def handle_question_key(key)
|
|
864
|
-
return if handle_question_bracketed_paste_key(key)
|
|
865
|
-
|
|
866
|
-
csi_result = handle_question_csi_u_key(key)
|
|
867
|
-
return csi_result unless csi_result == false
|
|
868
|
-
|
|
869
|
-
if key.is_a?(String) && key.length > 1
|
|
870
|
-
token = next_key_token(key)
|
|
871
|
-
if token.length < key.length
|
|
872
|
-
queue_pending_keys(key[token.length..])
|
|
873
|
-
return handle_question_key(token)
|
|
874
|
-
end
|
|
875
|
-
end
|
|
876
|
-
|
|
877
|
-
key_name = @reader.console.keys[key]
|
|
878
|
-
case key_name
|
|
879
|
-
when :return, :enter
|
|
880
|
-
current_question_answer
|
|
881
|
-
when :backspace
|
|
882
|
-
question_delete_before_cursor
|
|
883
|
-
when :delete
|
|
884
|
-
question_delete_at_cursor
|
|
885
|
-
when :left
|
|
886
|
-
@cursor -= 1 if @cursor.positive?
|
|
887
|
-
when :right
|
|
888
|
-
@cursor += 1 if @cursor < @input.length
|
|
889
|
-
when :home
|
|
890
|
-
@cursor = 0
|
|
891
|
-
when :end
|
|
892
|
-
@cursor = @input.length
|
|
893
|
-
when :up
|
|
894
|
-
question_previous_choice
|
|
895
|
-
when :down
|
|
896
|
-
question_next_choice
|
|
897
|
-
else
|
|
898
|
-
case key
|
|
899
|
-
when "\n", "\r"
|
|
900
|
-
current_question_answer
|
|
901
|
-
when "\b", "\x7F"
|
|
902
|
-
question_delete_before_cursor
|
|
903
|
-
when "\e"
|
|
904
|
-
handle_question_escape_sequence
|
|
905
|
-
else
|
|
906
|
-
question_insert_key(key)
|
|
907
|
-
end
|
|
908
|
-
end
|
|
909
|
-
end
|
|
910
|
-
|
|
911
|
-
def handle_question_csi_u_key(key)
|
|
912
|
-
match = key.to_s.match(/\A\e\[(\d+)(?:;([\d:]+))?u/)
|
|
913
|
-
return false unless match
|
|
914
|
-
|
|
915
|
-
sequence = match[0]
|
|
916
|
-
code = match[1].to_i
|
|
917
|
-
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
918
|
-
|
|
919
|
-
case code
|
|
920
|
-
when 13
|
|
921
|
-
current_question_answer
|
|
922
|
-
when 27
|
|
923
|
-
SELECT_CANCEL
|
|
924
|
-
when 8, 127
|
|
925
|
-
question_delete_before_cursor
|
|
926
|
-
nil
|
|
927
|
-
else
|
|
928
|
-
false
|
|
929
|
-
end
|
|
930
|
-
end
|
|
931
|
-
|
|
932
|
-
def handle_question_escape_sequence
|
|
933
|
-
sequence = read_pending_escape_sequence
|
|
934
|
-
return SELECT_CANCEL if sequence.empty?
|
|
935
|
-
|
|
936
|
-
key_name = @reader.console.keys["\e#{sequence}"]
|
|
937
|
-
case key_name
|
|
938
|
-
when :up
|
|
939
|
-
question_previous_choice
|
|
940
|
-
when :down
|
|
941
|
-
question_next_choice
|
|
942
|
-
when :left
|
|
943
|
-
@cursor -= 1 if @cursor.positive?
|
|
944
|
-
when :right
|
|
945
|
-
@cursor += 1 if @cursor < @input.length
|
|
946
|
-
end
|
|
947
|
-
true
|
|
948
|
-
end
|
|
949
|
-
|
|
950
|
-
def handle_question_bracketed_paste_key(key)
|
|
951
|
-
text = key.to_s
|
|
952
|
-
return false unless text.start_with?(BRACKETED_PASTE_START)
|
|
953
|
-
|
|
954
|
-
pasted = text[BRACKETED_PASTE_START.length..] || ""
|
|
955
|
-
until pasted.include?(BRACKETED_PASTE_END)
|
|
956
|
-
chunk = @reader.read_keypress(echo: false, raw: true)
|
|
957
|
-
break if chunk.nil?
|
|
958
|
-
|
|
959
|
-
pasted << chunk.to_s
|
|
960
|
-
end
|
|
961
|
-
|
|
962
|
-
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
963
|
-
question_insert_string(normalize_paste(content || ""))
|
|
964
|
-
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
965
|
-
true
|
|
966
|
-
end
|
|
967
|
-
|
|
968
|
-
def handle_select_key(key)
|
|
969
|
-
return select_current_choice if key.nil?
|
|
970
|
-
return if handle_select_bracketed_paste_key(key)
|
|
971
|
-
|
|
972
|
-
csi_result = handle_select_csi_u_key(key)
|
|
973
|
-
return csi_result unless csi_result == false
|
|
974
|
-
|
|
975
|
-
if key.is_a?(String) && key.length > 1
|
|
976
|
-
token = next_key_token(key)
|
|
977
|
-
if token.length < key.length
|
|
978
|
-
queue_pending_keys(key[token.length..])
|
|
979
|
-
return handle_select_key(token)
|
|
980
|
-
end
|
|
981
|
-
end
|
|
982
|
-
|
|
983
|
-
key_name = @reader.console.keys[key]
|
|
984
|
-
case key_name
|
|
985
|
-
when :return, :enter
|
|
986
|
-
select_current_choice
|
|
987
|
-
when :backspace
|
|
988
|
-
select_delete_before_cursor
|
|
989
|
-
when :delete
|
|
990
|
-
select_delete_at_cursor
|
|
991
|
-
when :left
|
|
992
|
-
@cursor -= 1 if @cursor.positive?
|
|
993
|
-
when :right
|
|
994
|
-
@cursor += 1 if @cursor < @input.length
|
|
995
|
-
when :home
|
|
996
|
-
@cursor = 0
|
|
997
|
-
when :end
|
|
998
|
-
@cursor = @input.length
|
|
999
|
-
when :up
|
|
1000
|
-
select_previous_choice
|
|
1001
|
-
when :down
|
|
1002
|
-
select_next_choice
|
|
1003
|
-
else
|
|
1004
|
-
case key
|
|
1005
|
-
when "\n", "\r"
|
|
1006
|
-
select_current_choice
|
|
1007
|
-
when "\b", "\x7F"
|
|
1008
|
-
select_delete_before_cursor
|
|
1009
|
-
when "\e"
|
|
1010
|
-
handle_select_escape_sequence
|
|
1011
|
-
else
|
|
1012
|
-
select_insert_key(key)
|
|
1013
|
-
end
|
|
1014
|
-
end
|
|
1015
|
-
end
|
|
1016
|
-
|
|
1017
|
-
def handle_select_csi_u_key(key)
|
|
1018
|
-
match = key.to_s.match(/\A\e\[(\d+)(?:;([\d:]+))?u/)
|
|
1019
|
-
return false unless match
|
|
1020
|
-
|
|
1021
|
-
sequence = match[0]
|
|
1022
|
-
code = match[1].to_i
|
|
1023
|
-
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
1024
|
-
|
|
1025
|
-
case code
|
|
1026
|
-
when 13
|
|
1027
|
-
select_current_choice
|
|
1028
|
-
when 27
|
|
1029
|
-
SELECT_CANCEL
|
|
1030
|
-
when 8, 127
|
|
1031
|
-
select_delete_before_cursor
|
|
1032
|
-
nil
|
|
1033
|
-
else
|
|
1034
|
-
false
|
|
1035
|
-
end
|
|
1036
|
-
end
|
|
1037
|
-
|
|
1038
|
-
def handle_select_escape_sequence
|
|
1039
|
-
sequence = read_pending_escape_sequence
|
|
1040
|
-
return SELECT_CANCEL if sequence.empty?
|
|
1041
|
-
|
|
1042
|
-
key_name = @reader.console.keys["\e#{sequence}"]
|
|
1043
|
-
case key_name
|
|
1044
|
-
when :up
|
|
1045
|
-
select_previous_choice
|
|
1046
|
-
when :down
|
|
1047
|
-
select_next_choice
|
|
1048
|
-
when :left
|
|
1049
|
-
@cursor -= 1 if @cursor.positive?
|
|
1050
|
-
when :right
|
|
1051
|
-
@cursor += 1 if @cursor < @input.length
|
|
1052
|
-
end
|
|
1053
|
-
true
|
|
1054
|
-
end
|
|
1055
|
-
|
|
1056
|
-
def handle_select_bracketed_paste_key(key)
|
|
1057
|
-
text = key.to_s
|
|
1058
|
-
return false unless text.start_with?(BRACKETED_PASTE_START)
|
|
1059
|
-
|
|
1060
|
-
pasted = text[BRACKETED_PASTE_START.length..] || ""
|
|
1061
|
-
until pasted.include?(BRACKETED_PASTE_END)
|
|
1062
|
-
chunk = @reader.read_keypress(echo: false, raw: true)
|
|
1063
|
-
break if chunk.nil?
|
|
1064
|
-
|
|
1065
|
-
pasted << chunk.to_s
|
|
1066
|
-
end
|
|
1067
|
-
|
|
1068
|
-
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
1069
|
-
select_insert_string(normalize_paste(content || ""))
|
|
1070
|
-
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
1071
|
-
true
|
|
1072
|
-
end
|
|
1073
|
-
|
|
1074
|
-
def handle_bracketed_paste_key(key)
|
|
1075
|
-
text = key.to_s
|
|
1076
|
-
return false unless text.start_with?(BRACKETED_PASTE_START)
|
|
1077
|
-
|
|
1078
|
-
pasted = text[BRACKETED_PASTE_START.length..] || ""
|
|
1079
|
-
until pasted.include?(BRACKETED_PASTE_END)
|
|
1080
|
-
chunk = @reader.read_keypress(echo: false, raw: true)
|
|
1081
|
-
break if chunk.nil?
|
|
1082
|
-
|
|
1083
|
-
pasted << chunk.to_s
|
|
1084
|
-
end
|
|
1085
|
-
|
|
1086
|
-
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
1087
|
-
insert_paste(normalize_paste(content || ""))
|
|
1088
|
-
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
1089
|
-
true
|
|
1090
|
-
end
|
|
1091
|
-
|
|
1092
|
-
def normalize_paste(content)
|
|
1093
|
-
content.gsub("\r\n", "\n").gsub("\r", "\n")
|
|
1094
|
-
end
|
|
1095
|
-
|
|
1096
|
-
def handle_csi_u_key(key)
|
|
1097
|
-
match = key.to_s.match(/\A\e\[(\d+)(?:;([\d:]+))?u/)
|
|
1098
|
-
return false unless match
|
|
1099
|
-
|
|
1100
|
-
sequence = match[0]
|
|
1101
|
-
code = match[1].to_i
|
|
1102
|
-
modifier = (match[2] || "1").split(":", 2).first.to_i
|
|
1103
|
-
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
1104
|
-
|
|
1105
|
-
case code
|
|
1106
|
-
when 13
|
|
1107
|
-
modifier == 2 ? insert_string("\n") : submit_input
|
|
1108
|
-
when 27
|
|
1109
|
-
dismiss_slash_overlay || false
|
|
1110
|
-
when 8, 127
|
|
1111
|
-
alt_modifier?(modifier) ? delete_word_before_cursor : delete_before_cursor
|
|
1112
|
-
nil
|
|
1113
|
-
when 4
|
|
1114
|
-
delete_at_cursor_or_exit
|
|
1115
|
-
else
|
|
1116
|
-
handle_modified_csi_u_key(code, modifier)
|
|
1117
|
-
end
|
|
1118
|
-
end
|
|
1119
|
-
|
|
1120
|
-
def handle_modified_csi_u_key(code, modifier)
|
|
1121
|
-
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
1122
|
-
|
|
1123
|
-
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
1124
|
-
if ctrl_modifier?(modifier)
|
|
1125
|
-
case normalized_code
|
|
1126
|
-
when 97
|
|
1127
|
-
move_to_start_of_line
|
|
1128
|
-
when 98
|
|
1129
|
-
move_cursor_left
|
|
1130
|
-
when 99
|
|
1131
|
-
cancel_input_or_interrupt
|
|
1132
|
-
when 100
|
|
1133
|
-
delete_at_cursor_or_exit
|
|
1134
|
-
when 101
|
|
1135
|
-
move_to_end_of_line
|
|
1136
|
-
when 102
|
|
1137
|
-
move_cursor_right
|
|
1138
|
-
when 104
|
|
1139
|
-
delete_before_cursor
|
|
1140
|
-
when 107
|
|
1141
|
-
kill_line_after_cursor
|
|
1142
|
-
when 108
|
|
1143
|
-
redraw_screen_locked
|
|
1144
|
-
when 117
|
|
1145
|
-
kill_line_before_cursor
|
|
1146
|
-
when 119
|
|
1147
|
-
delete_word_before_cursor
|
|
1148
|
-
when 121
|
|
1149
|
-
yank_kill_buffer
|
|
1150
|
-
else
|
|
1151
|
-
false
|
|
1152
|
-
end
|
|
1153
|
-
elsif alt_modifier?(modifier)
|
|
1154
|
-
case normalized_code
|
|
1155
|
-
when 98
|
|
1156
|
-
move_to_previous_word
|
|
1157
|
-
when 100
|
|
1158
|
-
delete_word_after_cursor
|
|
1159
|
-
when 102
|
|
1160
|
-
move_to_next_word
|
|
1161
|
-
else
|
|
1162
|
-
false
|
|
1163
|
-
end
|
|
1164
|
-
else
|
|
1165
|
-
false
|
|
1166
|
-
end
|
|
1167
|
-
end
|
|
1168
|
-
|
|
1169
|
-
def ctrl_modifier?(modifier)
|
|
1170
|
-
((modifier.to_i - 1) & 4).positive?
|
|
1171
|
-
end
|
|
1172
|
-
|
|
1173
|
-
def alt_modifier?(modifier)
|
|
1174
|
-
((modifier.to_i - 1) & 2).positive?
|
|
1175
|
-
end
|
|
1176
|
-
|
|
1177
|
-
def handle_shift_enter_key(key)
|
|
1178
|
-
sequence = shift_enter_sequence_for(key)
|
|
1179
|
-
return false unless sequence
|
|
1180
|
-
|
|
1181
|
-
insert_string("\n")
|
|
1182
|
-
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
1183
|
-
true
|
|
1184
|
-
end
|
|
1185
|
-
|
|
1186
|
-
def queue_pending_keys(keys)
|
|
1187
|
-
remaining = keys.to_s
|
|
1188
|
-
until remaining.empty?
|
|
1189
|
-
token = next_key_token(remaining)
|
|
1190
|
-
@pending_keys << token
|
|
1191
|
-
remaining = remaining[token.length..] || ""
|
|
1192
|
-
end
|
|
1193
|
-
end
|
|
1194
|
-
|
|
1195
|
-
def next_key_token(keys)
|
|
1196
|
-
text = keys.to_s
|
|
1197
|
-
text.match(/\A\e\[[0-9;:]*[A-Za-z~]/)&.[](0) ||
|
|
1198
|
-
text.match(/\A\eO[A-Za-z]/)&.[](0) ||
|
|
1199
|
-
shift_enter_sequence_for(text) ||
|
|
1200
|
-
(text.start_with?("\e") && text.length > 1 && alt_key_sequence?(text[1]) ? text[0, 2] : text[0, 1])
|
|
1201
|
-
end
|
|
1202
|
-
|
|
1203
|
-
def alt_key_sequence?(char)
|
|
1204
|
-
char = char.to_s
|
|
1205
|
-
char.match?(/[[:alpha:]]/) || char == "\b" || char == "\x7F"
|
|
1206
|
-
end
|
|
1207
|
-
|
|
1208
|
-
def shift_enter_sequence_for(key)
|
|
1209
|
-
return nil unless key.is_a?(String)
|
|
1210
|
-
|
|
1211
|
-
SHIFT_ENTER_SEQUENCES.find { |sequence| key.start_with?(sequence) }
|
|
1212
|
-
end
|
|
1213
|
-
|
|
1214
|
-
def read_pending_escape_sequence
|
|
1215
|
-
sequence = +""
|
|
1216
|
-
until @pending_keys.empty?
|
|
1217
|
-
sequence << @pending_keys.shift.to_s
|
|
1218
|
-
end
|
|
1219
|
-
while (char = @reader.read_keypress(echo: false, raw: true, nonblock: true))
|
|
1220
|
-
sequence << char.to_s
|
|
1221
|
-
end
|
|
1222
|
-
sequence
|
|
1223
|
-
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
1224
|
-
sequence
|
|
1225
|
-
end
|
|
1226
|
-
|
|
1227
|
-
def current_question_answer
|
|
1228
|
-
choice = selected_question_choice
|
|
1229
|
-
return nil unless choice
|
|
1230
|
-
|
|
1231
|
-
if choice[:custom]
|
|
1232
|
-
answer = @input.strip
|
|
1233
|
-
return nil if answer.empty?
|
|
1234
|
-
|
|
1235
|
-
{ question: current_question_text, answer: answer, custom: true }
|
|
1236
|
-
else
|
|
1237
|
-
{ question: current_question_text, answer: choice[:label], custom: false }
|
|
1238
|
-
end
|
|
1239
|
-
end
|
|
1240
|
-
|
|
1241
|
-
def selected_question_choice
|
|
1242
|
-
choices = question_choices
|
|
1243
|
-
return nil if choices.empty?
|
|
1244
|
-
|
|
1245
|
-
choices[question_selection_index]
|
|
1246
|
-
end
|
|
1247
|
-
|
|
1248
|
-
def question_choices
|
|
1249
|
-
options = Array(@question_state ? @question_state[:options] : []).map do |option|
|
|
1250
|
-
{ label: (option[:label] || option["label"]).to_s, description: (option[:description] || option["description"]).to_s }
|
|
1251
|
-
end
|
|
1252
|
-
choices = options + [{ label: "Type something.", description: @input.strip, custom: true }]
|
|
1253
|
-
clamp_question_selection_index(choices.length)
|
|
1254
|
-
choices
|
|
1255
|
-
end
|
|
1256
|
-
|
|
1257
|
-
def current_question_text
|
|
1258
|
-
(@question_state && @question_state[:question]).to_s
|
|
1259
|
-
end
|
|
1260
|
-
|
|
1261
|
-
def question_selection_index
|
|
1262
|
-
@question_state ? @question_state[:selection_index].to_i : 0
|
|
1263
|
-
end
|
|
1264
497
|
|
|
1265
|
-
def clamp_question_selection_index(count)
|
|
1266
|
-
return unless @question_state
|
|
1267
498
|
|
|
1268
|
-
@question_state[:selection_index] = 0 if count <= 0
|
|
1269
|
-
@question_state[:selection_index] = count - 1 if count.positive? && question_selection_index >= count
|
|
1270
|
-
end
|
|
1271
499
|
|
|
1272
|
-
def question_previous_choice
|
|
1273
|
-
choices = question_choices
|
|
1274
|
-
return if choices.empty?
|
|
1275
500
|
|
|
1276
|
-
@question_state[:selection_index] = (question_selection_index - 1) % choices.length
|
|
1277
|
-
end
|
|
1278
501
|
|
|
1279
|
-
def question_next_choice
|
|
1280
|
-
choices = question_choices
|
|
1281
|
-
return if choices.empty?
|
|
1282
502
|
|
|
1283
|
-
@question_state[:selection_index] = (question_selection_index + 1) % choices.length
|
|
1284
|
-
end
|
|
1285
503
|
|
|
1286
|
-
def question_insert_key(key)
|
|
1287
|
-
return unless key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
1288
504
|
|
|
1289
|
-
question_insert_string(key)
|
|
1290
|
-
end
|
|
1291
505
|
|
|
1292
|
-
def question_insert_string(string)
|
|
1293
|
-
return if string.empty?
|
|
1294
506
|
|
|
1295
|
-
@input = @input[0...@cursor] + string + @input[@cursor..]
|
|
1296
|
-
@cursor += string.length
|
|
1297
|
-
@question_state[:selection_index] = question_choices.length - 1 if @question_state
|
|
1298
|
-
end
|
|
1299
507
|
|
|
1300
|
-
def question_delete_before_cursor
|
|
1301
|
-
return unless @cursor.positive?
|
|
1302
508
|
|
|
1303
|
-
@input = @input[0...(@cursor - 1)] + @input[@cursor..]
|
|
1304
|
-
@cursor -= 1
|
|
1305
|
-
@question_state[:selection_index] = question_choices.length - 1 if @question_state && !@input.empty?
|
|
1306
|
-
end
|
|
1307
509
|
|
|
1308
|
-
def question_delete_at_cursor
|
|
1309
|
-
return unless @cursor < @input.length
|
|
1310
510
|
|
|
1311
|
-
@input = @input[0...@cursor] + @input[(@cursor + 1)..]
|
|
1312
|
-
@question_state[:selection_index] = question_choices.length - 1 if @question_state && !@input.empty?
|
|
1313
|
-
end
|
|
1314
511
|
|
|
1315
|
-
def select_current_choice
|
|
1316
|
-
selected_selection_choice || custom_selection_choice || SELECT_CANCEL
|
|
1317
|
-
end
|
|
1318
512
|
|
|
1319
|
-
def custom_selection_choice
|
|
1320
|
-
return nil unless @select_state && @select_state[:custom]
|
|
1321
513
|
|
|
1322
|
-
value = @input.strip
|
|
1323
|
-
value.empty? ? nil : value
|
|
1324
|
-
end
|
|
1325
514
|
|
|
1326
|
-
def selected_selection_choice
|
|
1327
|
-
matches = selection_matches
|
|
1328
|
-
return nil if matches.empty?
|
|
1329
515
|
|
|
1330
|
-
matches[selection_index]
|
|
1331
|
-
end
|
|
1332
516
|
|
|
1333
|
-
def select_previous_choice
|
|
1334
|
-
matches = selection_matches
|
|
1335
|
-
return if matches.empty?
|
|
1336
517
|
|
|
1337
|
-
@select_state[:selection_index] = (selection_index - 1) % matches.length
|
|
1338
|
-
end
|
|
1339
518
|
|
|
1340
|
-
def select_next_choice
|
|
1341
|
-
matches = selection_matches
|
|
1342
|
-
return if matches.empty?
|
|
1343
519
|
|
|
1344
|
-
@select_state[:selection_index] = (selection_index + 1) % matches.length
|
|
1345
|
-
end
|
|
1346
520
|
|
|
1347
|
-
def select_insert_key(key)
|
|
1348
|
-
return unless key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
1349
521
|
|
|
1350
|
-
select_insert_string(key)
|
|
1351
|
-
end
|
|
1352
522
|
|
|
1353
|
-
def select_insert_string(string)
|
|
1354
|
-
return if string.empty?
|
|
1355
523
|
|
|
1356
|
-
@input = @input[0...@cursor] + string + @input[@cursor..]
|
|
1357
|
-
@cursor += string.length
|
|
1358
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
1359
|
-
end
|
|
1360
524
|
|
|
1361
|
-
def select_delete_before_cursor
|
|
1362
|
-
return unless @cursor.positive?
|
|
1363
525
|
|
|
1364
|
-
@input = @input[0...(@cursor - 1)] + @input[@cursor..]
|
|
1365
|
-
@cursor -= 1
|
|
1366
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
1367
|
-
end
|
|
1368
526
|
|
|
1369
|
-
def select_delete_at_cursor
|
|
1370
|
-
return unless @cursor < @input.length
|
|
1371
527
|
|
|
1372
|
-
@input = @input[0...@cursor] + @input[(@cursor + 1)..]
|
|
1373
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
1374
|
-
end
|
|
1375
528
|
|
|
1376
|
-
def selection_matches
|
|
1377
|
-
choices = @select_state ? @select_state[:choices] : []
|
|
1378
|
-
filter = @input.downcase.strip
|
|
1379
|
-
matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
|
|
1380
|
-
clamp_selection_index(matches.length)
|
|
1381
|
-
matches
|
|
1382
|
-
end
|
|
1383
529
|
|
|
1384
|
-
def selection_index
|
|
1385
|
-
@select_state ? @select_state[:selection_index].to_i : 0
|
|
1386
|
-
end
|
|
1387
530
|
|
|
1388
|
-
def clamp_selection_index(count)
|
|
1389
|
-
return unless @select_state
|
|
1390
531
|
|
|
1391
|
-
@select_state[:selection_index] = 0 if count <= 0
|
|
1392
|
-
@select_state[:selection_index] = count - 1 if count.positive? && selection_index >= count
|
|
1393
|
-
end
|
|
1394
532
|
|
|
1395
|
-
def finish_select_prompt
|
|
1396
|
-
@mutex.synchronize do
|
|
1397
|
-
@select_state = nil
|
|
1398
|
-
clear_prompt_locked
|
|
1399
|
-
@input = ""
|
|
1400
|
-
@cursor = 0
|
|
1401
|
-
@asking = false
|
|
1402
|
-
@rendered_rows = 0
|
|
1403
|
-
@cursor_rendered_row = 0
|
|
1404
|
-
@output_io.flush
|
|
1405
|
-
end
|
|
1406
|
-
end
|
|
1407
533
|
|
|
1408
|
-
def insert_key(key)
|
|
1409
|
-
return unless key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
1410
534
|
|
|
1411
|
-
insert_string(key)
|
|
1412
|
-
end
|
|
1413
535
|
|
|
1414
|
-
def insert_string(string)
|
|
1415
|
-
return if string.empty?
|
|
1416
536
|
|
|
1417
|
-
reset_slash_selection
|
|
1418
|
-
reset_history_navigation
|
|
1419
|
-
@slash_overlay_dismissed_input = nil
|
|
1420
|
-
@input = @input[0...@cursor] + string + @input[@cursor..]
|
|
1421
|
-
@cursor += string.length
|
|
1422
|
-
end
|
|
1423
537
|
|
|
1424
|
-
def insert_paste(string)
|
|
1425
|
-
parsed = parse_attachments(string)
|
|
1426
|
-
Array(parsed[:attachments]).each { |attachment| add_attachment(attachment) }
|
|
1427
|
-
insert_string(parsed[:text].to_s) unless parsed[:text].to_s.empty?
|
|
1428
|
-
end
|
|
1429
538
|
|
|
1430
|
-
def parse_attachments(string)
|
|
1431
|
-
return { text: string.to_s, attachments: [] } unless @attachment_parser
|
|
1432
539
|
|
|
1433
|
-
result = @attachment_parser.call(string.to_s)
|
|
1434
|
-
return { text: string.to_s, attachments: [] } unless result.is_a?(Hash)
|
|
1435
540
|
|
|
1436
|
-
{
|
|
1437
|
-
text: result[:text] || result["text"] || "",
|
|
1438
|
-
attachments: result[:attachments] || result["attachments"] || []
|
|
1439
|
-
}
|
|
1440
|
-
rescue StandardError
|
|
1441
|
-
{ text: string.to_s, attachments: [] }
|
|
1442
|
-
end
|
|
1443
541
|
|
|
1444
|
-
def add_attachment(attachment)
|
|
1445
|
-
return unless attachment.respond_to?(:key?)
|
|
1446
542
|
|
|
1447
|
-
source = attachment[:source_text] || attachment["source_text"] || attachment[:original_path] || attachment["original_path"]
|
|
1448
|
-
return if source.to_s.empty?
|
|
1449
|
-
return if @attachments.any? { |item| (item[:source_text] || item["source_text"]).to_s == source.to_s }
|
|
1450
543
|
|
|
1451
|
-
@attachments << attachment
|
|
1452
|
-
end
|
|
1453
544
|
|
|
1454
|
-
def delete_before_cursor
|
|
1455
|
-
if @cursor.zero?
|
|
1456
|
-
remove_last_attachment
|
|
1457
|
-
return
|
|
1458
|
-
end
|
|
1459
545
|
|
|
1460
|
-
reset_slash_selection
|
|
1461
|
-
reset_history_navigation
|
|
1462
|
-
@input = @input[0...(@cursor - 1)] + @input[@cursor..]
|
|
1463
|
-
@cursor -= 1
|
|
1464
|
-
end
|
|
1465
546
|
|
|
1466
|
-
def remove_last_attachment
|
|
1467
|
-
return if @attachments.empty?
|
|
1468
547
|
|
|
1469
|
-
reset_slash_selection
|
|
1470
|
-
reset_history_navigation
|
|
1471
|
-
@slash_overlay_dismissed_input = nil
|
|
1472
|
-
@attachments.pop
|
|
1473
|
-
end
|
|
1474
548
|
|
|
1475
|
-
def delete_at_cursor
|
|
1476
|
-
return unless @cursor < @input.length
|
|
1477
549
|
|
|
1478
|
-
reset_slash_selection
|
|
1479
|
-
reset_history_navigation
|
|
1480
|
-
@slash_overlay_dismissed_input = nil
|
|
1481
|
-
@input = @input[0...@cursor] + @input[(@cursor + 1)..]
|
|
1482
|
-
end
|
|
1483
550
|
|
|
1484
|
-
def handle_composer_key_binding(key)
|
|
1485
|
-
case key
|
|
1486
|
-
when "\x01"
|
|
1487
|
-
move_to_start_of_line
|
|
1488
|
-
when "\x02"
|
|
1489
|
-
move_cursor_left
|
|
1490
|
-
when "\x04"
|
|
1491
|
-
delete_at_cursor_or_exit
|
|
1492
|
-
when "\x05"
|
|
1493
|
-
move_to_end_of_line
|
|
1494
|
-
when "\x06"
|
|
1495
|
-
move_cursor_right
|
|
1496
|
-
when "\x0B"
|
|
1497
|
-
kill_line_after_cursor
|
|
1498
|
-
when "\x0C"
|
|
1499
|
-
redraw_screen_locked
|
|
1500
|
-
when "\x15"
|
|
1501
|
-
kill_line_before_cursor
|
|
1502
|
-
when "\x17"
|
|
1503
|
-
delete_word_before_cursor
|
|
1504
|
-
when "\x19"
|
|
1505
|
-
yank_kill_buffer
|
|
1506
|
-
when "\e[D", "\eOD"
|
|
1507
|
-
move_cursor_left
|
|
1508
|
-
when "\e[C", "\eOC"
|
|
1509
|
-
move_cursor_right
|
|
1510
|
-
when "\e[H", "\eOH", "\e[1~", "\e[7~"
|
|
1511
|
-
move_to_start_of_line
|
|
1512
|
-
when "\e[F", "\eOF", "\e[4~", "\e[8~"
|
|
1513
|
-
move_to_end_of_line
|
|
1514
|
-
when "\e[3~"
|
|
1515
|
-
delete_at_cursor
|
|
1516
|
-
when "\eb", "\eB"
|
|
1517
|
-
move_to_previous_word
|
|
1518
|
-
when "\ef", "\eF"
|
|
1519
|
-
move_to_next_word
|
|
1520
|
-
when "\ed", "\eD"
|
|
1521
|
-
delete_word_after_cursor
|
|
1522
|
-
when "\e\b", "\e\x7F"
|
|
1523
|
-
delete_word_before_cursor
|
|
1524
|
-
else
|
|
1525
|
-
handle_modified_ansi_key(key) || false
|
|
1526
|
-
end
|
|
1527
|
-
end
|
|
1528
551
|
|
|
1529
|
-
def handle_modified_ansi_key(key)
|
|
1530
|
-
match = key.to_s.match(/\A\e\[(\d+);(\d+)([CDFH])\z/)
|
|
1531
|
-
if match
|
|
1532
|
-
modifier = match[2].to_i
|
|
1533
|
-
final = match[3]
|
|
1534
|
-
return false unless alt_modifier?(modifier)
|
|
1535
|
-
|
|
1536
|
-
case final
|
|
1537
|
-
when "C"
|
|
1538
|
-
move_to_next_word
|
|
1539
|
-
when "D"
|
|
1540
|
-
move_to_previous_word
|
|
1541
|
-
when "F"
|
|
1542
|
-
move_to_end_of_line
|
|
1543
|
-
when "H"
|
|
1544
|
-
move_to_start_of_line
|
|
1545
|
-
else
|
|
1546
|
-
false
|
|
1547
|
-
end
|
|
1548
|
-
elsif (match = key.to_s.match(/\A\e\[3;(\d+)~\z/))
|
|
1549
|
-
alt_modifier?(match[1].to_i) ? delete_word_after_cursor : delete_at_cursor
|
|
1550
|
-
else
|
|
1551
|
-
false
|
|
1552
|
-
end
|
|
1553
|
-
end
|
|
1554
552
|
|
|
1555
|
-
def move_cursor_left
|
|
1556
|
-
@cursor -= 1 if @cursor.positive?
|
|
1557
|
-
end
|
|
1558
553
|
|
|
1559
|
-
def move_cursor_right
|
|
1560
|
-
@cursor += 1 if @cursor < @input.length
|
|
1561
|
-
end
|
|
1562
554
|
|
|
1563
|
-
def move_to_start_of_line
|
|
1564
|
-
@cursor = 0
|
|
1565
|
-
end
|
|
1566
555
|
|
|
1567
|
-
def move_to_end_of_line
|
|
1568
|
-
@cursor = @input.length
|
|
1569
|
-
end
|
|
1570
556
|
|
|
1571
|
-
def move_to_previous_word
|
|
1572
|
-
@cursor = previous_word_boundary(@cursor)
|
|
1573
|
-
end
|
|
1574
557
|
|
|
1575
|
-
def move_to_next_word
|
|
1576
|
-
@cursor = next_word_boundary(@cursor)
|
|
1577
|
-
end
|
|
1578
558
|
|
|
1579
|
-
def delete_at_cursor_or_exit
|
|
1580
|
-
@input.empty? ? exit_input : delete_at_cursor
|
|
1581
|
-
end
|
|
1582
559
|
|
|
1583
|
-
def delete_word_before_cursor
|
|
1584
|
-
start_index = previous_word_boundary(@cursor)
|
|
1585
|
-
kill_range(start_index, @cursor)
|
|
1586
|
-
end
|
|
1587
560
|
|
|
1588
|
-
def delete_word_after_cursor
|
|
1589
|
-
end_index = next_word_boundary(@cursor)
|
|
1590
|
-
kill_range(@cursor, end_index)
|
|
1591
|
-
end
|
|
1592
561
|
|
|
1593
|
-
def kill_line_before_cursor
|
|
1594
|
-
kill_range(0, @cursor)
|
|
1595
|
-
end
|
|
1596
562
|
|
|
1597
|
-
def kill_line_after_cursor
|
|
1598
|
-
kill_range(@cursor, @input.length)
|
|
1599
|
-
end
|
|
1600
563
|
|
|
1601
|
-
def kill_range(start_index, end_index)
|
|
1602
|
-
return if start_index == end_index
|
|
1603
564
|
|
|
1604
|
-
reset_slash_selection
|
|
1605
|
-
reset_history_navigation
|
|
1606
|
-
@kill_buffer = @input[start_index...end_index].to_s
|
|
1607
|
-
@input = @input[0...start_index].to_s + @input[end_index..].to_s
|
|
1608
|
-
@cursor = start_index
|
|
1609
|
-
end
|
|
1610
565
|
|
|
1611
|
-
def yank_kill_buffer
|
|
1612
|
-
insert_string(@kill_buffer.to_s) unless @kill_buffer.to_s.empty?
|
|
1613
|
-
end
|
|
1614
566
|
|
|
1615
|
-
def previous_word_boundary(index)
|
|
1616
|
-
cursor = index
|
|
1617
|
-
cursor -= 1 while cursor.positive? && word_separator?(@input[cursor - 1])
|
|
1618
|
-
cursor -= 1 while cursor.positive? && !word_separator?(@input[cursor - 1])
|
|
1619
|
-
cursor
|
|
1620
|
-
end
|
|
1621
567
|
|
|
1622
|
-
def next_word_boundary(index)
|
|
1623
|
-
cursor = index
|
|
1624
|
-
cursor += 1 while cursor < @input.length && word_separator?(@input[cursor])
|
|
1625
|
-
cursor += 1 while cursor < @input.length && !word_separator?(@input[cursor])
|
|
1626
|
-
cursor
|
|
1627
|
-
end
|
|
1628
568
|
|
|
1629
|
-
def word_separator?(char)
|
|
1630
|
-
char.to_s.match?(/\s/)
|
|
1631
|
-
end
|
|
1632
569
|
|
|
1633
|
-
def add_history(value)
|
|
1634
|
-
stripped = value.to_s.strip
|
|
1635
|
-
return if stripped.empty?
|
|
1636
|
-
return if @history.last == value
|
|
1637
570
|
|
|
1638
|
-
@history << value
|
|
1639
|
-
end
|
|
1640
571
|
|
|
1641
|
-
def recall_previous_history
|
|
1642
|
-
return if @history.empty?
|
|
1643
572
|
|
|
1644
|
-
@history_draft = @input if @history_index.nil?
|
|
1645
|
-
@history_index = @history_index.nil? ? @history.length - 1 : [@history_index - 1, 0].max
|
|
1646
|
-
replace_input(@history[@history_index])
|
|
1647
|
-
end
|
|
1648
573
|
|
|
1649
|
-
def recall_next_history
|
|
1650
|
-
return if @history_index.nil?
|
|
1651
574
|
|
|
1652
|
-
if @history_index < @history.length - 1
|
|
1653
|
-
@history_index += 1
|
|
1654
|
-
replace_input(@history[@history_index])
|
|
1655
|
-
else
|
|
1656
|
-
replace_input(@history_draft || "")
|
|
1657
|
-
reset_history_navigation
|
|
1658
|
-
end
|
|
1659
|
-
end
|
|
1660
575
|
|
|
1661
|
-
def replace_input(value)
|
|
1662
|
-
@input = value.to_s
|
|
1663
|
-
@cursor = @input.length
|
|
1664
|
-
end
|
|
1665
576
|
|
|
1666
|
-
def prefill_input(value)
|
|
1667
|
-
@mutex.synchronize do
|
|
1668
|
-
@prefill_input = value.to_s
|
|
1669
|
-
end
|
|
1670
|
-
end
|
|
1671
577
|
|
|
1672
|
-
def reset_history_navigation
|
|
1673
|
-
@history_index = nil
|
|
1674
|
-
@history_draft = nil
|
|
1675
|
-
end
|
|
1676
578
|
|
|
1677
|
-
def reset_slash_selection
|
|
1678
|
-
@slash_selection_index = 0
|
|
1679
|
-
end
|
|
1680
579
|
|
|
1681
|
-
def dismiss_slash_overlay
|
|
1682
|
-
return false unless slash_overlay_visible?
|
|
1683
580
|
|
|
1684
|
-
@slash_overlay_dismissed_input = @input.dup
|
|
1685
|
-
reset_slash_selection
|
|
1686
|
-
true
|
|
1687
|
-
end
|
|
1688
581
|
|
|
1689
|
-
def normalize_slash_commands(commands)
|
|
1690
|
-
commands.map do |command|
|
|
1691
|
-
{
|
|
1692
|
-
name: slash_command_value(command, :name).to_s,
|
|
1693
|
-
description: slash_command_value(command, :description).to_s,
|
|
1694
|
-
argument_hint: slash_command_value(command, :argument_hint).to_s
|
|
1695
|
-
}
|
|
1696
|
-
end.reject { |command| command[:name].empty? }.sort_by { |command| command[:name] }
|
|
1697
|
-
end
|
|
1698
582
|
|
|
1699
|
-
def slash_command_value(command, key)
|
|
1700
|
-
return command[key] if command.respond_to?(:key?) && command.key?(key)
|
|
1701
|
-
return command[key.to_s] if command.respond_to?(:key?) && command.key?(key.to_s)
|
|
1702
|
-
return command.public_send(key) if command.respond_to?(key)
|
|
1703
583
|
|
|
1704
|
-
""
|
|
1705
|
-
end
|
|
1706
584
|
|
|
1707
|
-
def slash_overlay_visible?
|
|
1708
|
-
@input.match?(%r{\A/[^\s/]*\z}) && @slash_overlay_dismissed_input != @input && !slash_overlay_matches.empty?
|
|
1709
|
-
end
|
|
1710
585
|
|
|
1711
|
-
def slash_overlay_matches
|
|
1712
|
-
prefix = @input.delete_prefix("/").downcase
|
|
1713
|
-
@slash_commands.select { |command| command[:name].downcase.start_with?(prefix) }.first(8)
|
|
1714
|
-
end
|
|
1715
586
|
|
|
1716
|
-
def selected_slash_command
|
|
1717
|
-
return nil unless slash_overlay_visible?
|
|
1718
587
|
|
|
1719
|
-
matches = slash_overlay_matches
|
|
1720
|
-
return nil if matches.empty?
|
|
1721
588
|
|
|
1722
|
-
matches[[@slash_selection_index, matches.length - 1].min]
|
|
1723
|
-
end
|
|
1724
589
|
|
|
1725
|
-
def select_previous_slash_command
|
|
1726
|
-
matches = slash_overlay_matches
|
|
1727
|
-
return if matches.empty?
|
|
1728
590
|
|
|
1729
|
-
@slash_selection_index = (@slash_selection_index - 1) % matches.length
|
|
1730
|
-
end
|
|
1731
591
|
|
|
1732
|
-
def select_next_slash_command
|
|
1733
|
-
matches = slash_overlay_matches
|
|
1734
|
-
return if matches.empty?
|
|
1735
592
|
|
|
1736
|
-
@slash_selection_index = (@slash_selection_index + 1) % matches.length
|
|
1737
|
-
end
|
|
1738
593
|
|
|
1739
|
-
def complete_selected_slash_command
|
|
1740
|
-
command = selected_slash_command
|
|
1741
|
-
return false unless command
|
|
1742
594
|
|
|
1743
|
-
replace_input("/#{command[:name]} ")
|
|
1744
|
-
reset_slash_selection
|
|
1745
|
-
true
|
|
1746
|
-
end
|
|
1747
595
|
|
|
1748
|
-
def render_prompt_locked
|
|
1749
|
-
return unless @started && @asking
|
|
1750
|
-
|
|
1751
|
-
handle_resize_locked
|
|
1752
|
-
width, height = screen_size
|
|
1753
|
-
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
1754
|
-
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
1755
|
-
@rendered_rows = rows.length
|
|
1756
|
-
render_composer_rows_locked(rows, height: height)
|
|
1757
|
-
@cursor_rendered_row = cursor_row
|
|
1758
|
-
@last_width = width
|
|
1759
|
-
@last_height = height
|
|
1760
|
-
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
1761
|
-
render_cursor_visibility_locked
|
|
1762
|
-
@output_io.flush
|
|
1763
|
-
end
|
|
1764
596
|
|
|
1765
|
-
def render_prompt_after_output_locked
|
|
1766
|
-
render_prompt_locked
|
|
1767
|
-
end
|
|
1768
|
-
|
|
1769
|
-
def clear_prompt_locked
|
|
1770
|
-
handle_resize_locked
|
|
1771
|
-
width, height = screen_size
|
|
1772
|
-
clear_composer_region_locked(height: height)
|
|
1773
|
-
@rendered_rows = 0
|
|
1774
|
-
@cursor_rendered_row = 0
|
|
1775
|
-
redraw_transcript_locked(width: width, height: height)
|
|
1776
|
-
end
|
|
1777
|
-
|
|
1778
|
-
def clear_prompt_for_output_locked
|
|
1779
|
-
handle_resize_locked
|
|
1780
|
-
width, height = screen_size
|
|
1781
|
-
reserve_composer_region_locked(width: width, height: height) if @started && @asking
|
|
1782
|
-
clear_composer_region_locked(height: height)
|
|
1783
|
-
@rendered_rows = 0
|
|
1784
|
-
@cursor_rendered_row = 0
|
|
1785
|
-
move_to_transcript_cursor_locked(width: width, height: height) if @started
|
|
1786
|
-
end
|
|
1787
|
-
|
|
1788
|
-
def prepare_transcript_output_locked
|
|
1789
|
-
handle_resize_locked
|
|
1790
|
-
width, height = screen_size
|
|
1791
|
-
hide_cursor_for_transcript_output_locked
|
|
1792
|
-
reserve_composer_region_locked(width: width, height: height)
|
|
1793
|
-
move_to_transcript_cursor_locked(width: width, height: height)
|
|
1794
|
-
end
|
|
1795
|
-
|
|
1796
|
-
def hide_cursor_for_transcript_output_locked
|
|
1797
|
-
return unless @started && @asking
|
|
1798
|
-
|
|
1799
|
-
set_cursor_visible_locked(false)
|
|
1800
|
-
end
|
|
1801
|
-
|
|
1802
|
-
def restore_composer_cursor_locked
|
|
1803
|
-
return unless @started && @asking
|
|
1804
597
|
|
|
1805
|
-
width, height = screen_size
|
|
1806
|
-
_rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
1807
|
-
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
1808
|
-
render_cursor_visibility_locked
|
|
1809
|
-
end
|
|
1810
598
|
|
|
1811
|
-
def render_cursor_visibility_locked
|
|
1812
|
-
visible = !(@question_state && !selected_question_choice&.fetch(:custom, false))
|
|
1813
|
-
set_cursor_visible_locked(visible)
|
|
1814
|
-
end
|
|
1815
599
|
|
|
1816
|
-
def set_cursor_visible_locked(visible, force: false)
|
|
1817
|
-
return if !force && @cursor_visible == visible
|
|
1818
600
|
|
|
1819
|
-
@output_io.print(visible ? CURSOR_SHOW : CURSOR_HIDE)
|
|
1820
|
-
@cursor_visible = visible
|
|
1821
|
-
end
|
|
1822
|
-
|
|
1823
|
-
def reserve_composer_region_locked(width: screen_width, height: screen_height)
|
|
1824
|
-
rows, = composer_layout(width, height)
|
|
1825
|
-
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
1826
|
-
end
|
|
1827
|
-
|
|
1828
|
-
def ensure_scroll_region_locked(row_count, redraw_transcript: true, width: screen_width, height: screen_height)
|
|
1829
|
-
new_reserved_rows = [[row_count, 1].max, [height - 1, 1].max].min
|
|
1830
|
-
return if @reserved_rows == new_reserved_rows && @last_height == height
|
|
1831
|
-
|
|
1832
|
-
old_reserved_rows = @reserved_rows
|
|
1833
|
-
rows_to_clear = [old_reserved_rows, new_reserved_rows].max
|
|
1834
|
-
@reserved_rows = new_reserved_rows
|
|
1835
|
-
@output_io.print("\e[1;#{transcript_bottom_row(height)}r")
|
|
1836
|
-
clear_composer_region_locked(rows_to_clear, height: height)
|
|
1837
|
-
redraw_transcript_locked(width: width, height: height) if redraw_transcript && new_reserved_rows < old_reserved_rows
|
|
1838
|
-
end
|
|
1839
|
-
|
|
1840
|
-
def handle_resize_locked
|
|
1841
|
-
current_width, current_height = screen_size
|
|
1842
|
-
return false if current_width == @last_width && current_height == @last_height
|
|
1843
|
-
|
|
1844
|
-
old_width = @last_width
|
|
1845
|
-
old_height = @last_height
|
|
1846
|
-
old_reserved_rows = @reserved_rows
|
|
1847
|
-
restore_scroll_region_locked
|
|
1848
|
-
rows_to_clear = resize_prompt_clear_rows(old_width, current_width, old_reserved_rows)
|
|
1849
|
-
clear_resized_composer_region_locked(old_height, current_height, rows_to_clear)
|
|
1850
|
-
@reserved_rows = 0
|
|
1851
|
-
@last_width = current_width
|
|
1852
|
-
@last_height = current_height
|
|
1853
|
-
redraw_screen_locked(width: current_width, height: current_height)
|
|
1854
|
-
true
|
|
1855
|
-
end
|
|
1856
601
|
|
|
1857
|
-
def restore_scroll_region_locked
|
|
1858
|
-
@output_io.print("\e[r")
|
|
1859
|
-
@reserved_rows = 0
|
|
1860
|
-
end
|
|
1861
602
|
|
|
1862
|
-
def render_composer_rows_locked(rows, height: screen_height)
|
|
1863
|
-
top = composer_top_row(height)
|
|
1864
|
-
max_rows = [@last_composer_rows.length, rows.length].max
|
|
1865
|
-
rows_to_clear = [@reserved_rows - rows.length, 0].max
|
|
1866
603
|
|
|
1867
|
-
max_rows.times do |index|
|
|
1868
|
-
row = rows[index]
|
|
1869
|
-
previous = @last_composer_rows[index]
|
|
1870
|
-
next if row == previous
|
|
1871
604
|
|
|
1872
|
-
move_to_screen(top + index, 1)
|
|
1873
|
-
@output_io.print(TTY::Cursor.clear_line)
|
|
1874
|
-
@output_io.print(row) unless row.to_s.empty?
|
|
1875
|
-
end
|
|
1876
605
|
|
|
1877
|
-
rows.length.upto(rows.length + rows_to_clear - 1) do |index|
|
|
1878
|
-
move_to_screen(top + index, 1)
|
|
1879
|
-
@output_io.print(TTY::Cursor.clear_line)
|
|
1880
|
-
end
|
|
1881
606
|
|
|
1882
|
-
@last_composer_rows = rows.dup
|
|
1883
|
-
end
|
|
1884
607
|
|
|
1885
|
-
def clear_composer_region_locked(rows_to_clear = nil, height: screen_height)
|
|
1886
|
-
rows_to_clear ||= [@reserved_rows, @rendered_rows].max
|
|
1887
|
-
clear_bottom_rows_locked(height, rows_to_clear)
|
|
1888
|
-
@last_composer_rows = []
|
|
1889
|
-
end
|
|
1890
608
|
|
|
1891
|
-
def resize_prompt_clear_rows(old_width, current_width, old_reserved_rows)
|
|
1892
|
-
return old_reserved_rows unless old_reserved_rows.positive?
|
|
1893
609
|
|
|
1894
|
-
return old_reserved_rows unless current_width < old_width
|
|
1895
610
|
|
|
1896
|
-
wrapped_rows_per_row = ((old_width - 1) / current_width) + 1
|
|
1897
|
-
old_reserved_rows * wrapped_rows_per_row
|
|
1898
|
-
end
|
|
1899
611
|
|
|
1900
|
-
def clear_resized_composer_region_locked(old_height, current_height, rows_to_clear)
|
|
1901
|
-
return unless rows_to_clear.positive?
|
|
1902
612
|
|
|
1903
|
-
old_top = [old_height - rows_to_clear + 1, 1].max
|
|
1904
|
-
current_top = [current_height - rows_to_clear + 1, 1].max
|
|
1905
|
-
clear_screen_rows_locked([old_top, current_top].min, current_height)
|
|
1906
|
-
end
|
|
1907
613
|
|
|
1908
|
-
def clear_bottom_rows_locked(height, rows_to_clear)
|
|
1909
|
-
return unless rows_to_clear.positive?
|
|
1910
614
|
|
|
1911
|
-
bottom = height
|
|
1912
|
-
top = [bottom - rows_to_clear + 1, 1].max
|
|
1913
|
-
clear_screen_rows_locked(top, bottom)
|
|
1914
|
-
end
|
|
1915
615
|
|
|
1916
|
-
def clear_screen_rows_locked(top, bottom)
|
|
1917
|
-
top.upto(bottom) do |row|
|
|
1918
|
-
move_to_screen(row, 1)
|
|
1919
|
-
@output_io.print(TTY::Cursor.clear_line)
|
|
1920
|
-
end
|
|
1921
|
-
end
|
|
1922
616
|
|
|
1923
|
-
def redraw_screen_locked(width: screen_width, height: screen_height)
|
|
1924
|
-
return unless @started
|
|
1925
617
|
|
|
1926
|
-
restore_scroll_region_locked
|
|
1927
|
-
@output_io.print(TTY::Cursor.clear_screen)
|
|
1928
|
-
move_to_screen(1, 1)
|
|
1929
|
-
@reserved_rows = 0
|
|
1930
|
-
@last_composer_rows = []
|
|
1931
|
-
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
1932
|
-
ensure_scroll_region_locked(rows.length, redraw_transcript: false, width: width, height: height)
|
|
1933
|
-
redraw_transcript_locked(width: width, height: height)
|
|
1934
|
-
@rendered_rows = @asking ? rows.length : 0
|
|
1935
|
-
render_composer_rows_locked(rows, height: height) if @asking
|
|
1936
|
-
@cursor_rendered_row = @asking ? cursor_row : 0
|
|
1937
|
-
@last_width = width
|
|
1938
|
-
@last_height = height
|
|
1939
|
-
reset_stream_position_from_transcript_locked(width)
|
|
1940
|
-
if @asking
|
|
1941
|
-
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
1942
|
-
render_cursor_visibility_locked
|
|
1943
|
-
end
|
|
1944
|
-
end
|
|
1945
618
|
|
|
1946
|
-
def redraw_transcript_locked(width: screen_width, height: screen_height)
|
|
1947
|
-
return unless transcript_renderable?
|
|
1948
619
|
|
|
1949
|
-
rows = transcript_viewport_rows(transcript_redraw_row_count(height), width)
|
|
1950
|
-
clear_screen_rows_locked(1, rows.length)
|
|
1951
|
-
return if rows.empty?
|
|
1952
620
|
|
|
1953
|
-
move_to_screen(1, 1)
|
|
1954
|
-
@output_io.print(terminal_newlines(rows.join("\n")))
|
|
1955
|
-
end
|
|
1956
621
|
|
|
1957
|
-
def transcript_viewport_text(row_count, width)
|
|
1958
|
-
transcript_viewport_rows(row_count, width).join("\n")
|
|
1959
|
-
end
|
|
1960
622
|
|
|
1961
|
-
def transcript_viewport_rows(row_count, width)
|
|
1962
|
-
return [] unless row_count.positive?
|
|
1963
623
|
|
|
1964
|
-
rows = transcript_display_rows(width).last(row_count)
|
|
1965
|
-
rows = ([""] * (row_count - rows.length)) + rows if rows.length < row_count
|
|
1966
|
-
rows
|
|
1967
|
-
end
|
|
1968
624
|
|
|
1969
|
-
def transcript_redraw_row_count(height = screen_height)
|
|
1970
|
-
[[@transcript_viewport_rows, transcript_bottom_row(height)].max, height].min
|
|
1971
|
-
end
|
|
1972
625
|
|
|
1973
|
-
def remember_transcript_viewport_locked(height = screen_height)
|
|
1974
|
-
@transcript_viewport_rows = transcript_bottom_row(height)
|
|
1975
|
-
end
|
|
1976
626
|
|
|
1977
|
-
def transcript_renderable?
|
|
1978
|
-
@visual_banner_count.positive? || !@transcript_buffer.empty?
|
|
1979
|
-
end
|
|
1980
627
|
|
|
1981
|
-
def transcript_display_rows(width)
|
|
1982
|
-
return @transcript_display_rows_cache if @transcript_display_rows_cache_width == width && @transcript_display_rows_cache
|
|
1983
628
|
|
|
1984
|
-
rows = []
|
|
1985
|
-
@visual_banner_count.times { rows.concat(banner_rows(width)) }
|
|
1986
|
-
rows << "" if @visual_banner_count.positive? && @transcript_buffer.empty?
|
|
1987
|
-
rows.concat(transcript_text_display_rows(width))
|
|
1988
|
-
@transcript_display_rows_cache_width = width
|
|
1989
|
-
@transcript_display_rows_cache = rows
|
|
1990
|
-
end
|
|
1991
629
|
|
|
1992
|
-
def transcript_text_display_rows(width)
|
|
1993
|
-
@transcript_buffer.split(/\r\n|\r|\n/, -1).flat_map do |line|
|
|
1994
|
-
chunks = ANSI.wrap_visible(line, width)
|
|
1995
|
-
chunks.empty? ? [""] : chunks
|
|
1996
|
-
end
|
|
1997
|
-
end
|
|
1998
630
|
|
|
1999
|
-
def reset_stream_position_from_transcript_locked(width = screen_width)
|
|
2000
|
-
rows = transcript_display_rows(width)
|
|
2001
|
-
last_length = rows.empty? ? 0 : ANSI.strip(rows.last).length
|
|
2002
|
-
if last_length >= width
|
|
2003
|
-
@stream_col = 0
|
|
2004
|
-
@stream_pending_wrap = true
|
|
2005
|
-
else
|
|
2006
|
-
@stream_col = last_length
|
|
2007
|
-
@stream_pending_wrap = false
|
|
2008
|
-
end
|
|
2009
|
-
end
|
|
2010
631
|
|
|
2011
|
-
def move_to_transcript_cursor_locked(width: screen_width, height: screen_height)
|
|
2012
|
-
if @stream_pending_wrap
|
|
2013
|
-
move_to_screen(transcript_bottom_row(height), width)
|
|
2014
|
-
else
|
|
2015
|
-
move_to_screen(transcript_bottom_row(height), [@stream_col + 1, width].min)
|
|
2016
|
-
end
|
|
2017
|
-
end
|
|
2018
632
|
|
|
2019
|
-
def advance_pending_stream_wrap_locked(output_text, width: screen_width, height: screen_height)
|
|
2020
|
-
return unless @stream_pending_wrap
|
|
2021
|
-
return if output_text.empty? || output_text.start_with?("\r", "\n")
|
|
2022
633
|
|
|
2023
|
-
move_to_screen(transcript_bottom_row(height), width)
|
|
2024
|
-
@output_io.print("\r\n")
|
|
2025
|
-
@stream_col = 0
|
|
2026
|
-
@stream_pending_wrap = false
|
|
2027
|
-
end
|
|
2028
634
|
|
|
2029
|
-
def composer_layout(width, height = screen_height)
|
|
2030
|
-
return compact_composer_layout(width) if height < 4
|
|
2031
|
-
return question_composer_layout(width, height) if @question_state
|
|
2032
|
-
|
|
2033
|
-
content_width = [width - 4, 1].max
|
|
2034
|
-
input_layout_rows, input_cursor_row, input_cursor_col = input_layout(content_width)
|
|
2035
|
-
attachment_rows = attachment_badge_rows(content_width)
|
|
2036
|
-
overlay_rows = active_overlay_rows(width, height: height)
|
|
2037
|
-
footer_text = footer_text()
|
|
2038
|
-
max_input_rows = max_visible_input_rows(attachment_rows.length, overlay_rows.length, footer_text.empty? ? 0 : 1, height: height)
|
|
2039
|
-
visible_start = [[input_cursor_row - max_input_rows + 1, 0].max, [input_layout_rows.length - max_input_rows, 0].max].min
|
|
2040
|
-
visible_rows = input_layout_rows[visible_start, max_input_rows] || [""]
|
|
2041
|
-
rows = overlay_rows + [top_border(width)]
|
|
2042
|
-
rows.concat(attachment_rows)
|
|
2043
|
-
rows.concat(visible_rows.map { |row| box_content_row(row, content_width) })
|
|
2044
|
-
rows << footer_row(content_width, footer_text) unless footer_text.empty?
|
|
2045
|
-
rows << bottom_border(width)
|
|
2046
|
-
cursor_row = overlay_rows.length + 1 + attachment_rows.length + input_cursor_row - visible_start
|
|
2047
|
-
cursor_col = 2 + [input_cursor_col, content_width - 1].min
|
|
2048
|
-
[rows, cursor_row, cursor_col]
|
|
2049
|
-
end
|
|
2050
635
|
|
|
2051
|
-
def question_composer_layout(width, height = screen_height)
|
|
2052
|
-
content_width = [width - 4, 1].max
|
|
2053
|
-
overlay_rows = active_overlay_rows(width, height: height)
|
|
2054
|
-
rows = overlay_rows + [top_border(width), box_content_row("", content_width), bottom_border(width)]
|
|
2055
|
-
return [rows, question_custom_cursor_row, question_custom_cursor_col(width)] if selected_question_choice&.fetch(:custom, false)
|
|
2056
636
|
|
|
2057
|
-
[rows, overlay_rows.length + 1, 2]
|
|
2058
|
-
end
|
|
2059
637
|
|
|
2060
|
-
def active_overlay_rows(width, height: screen_height)
|
|
2061
|
-
return question_overlay_rows(width) if @question_state
|
|
2062
|
-
return selection_overlay_rows(width, height: height) if @select_state
|
|
2063
638
|
|
|
2064
|
-
slash_overlay_rows(width, height: height)
|
|
2065
|
-
end
|
|
2066
639
|
|
|
2067
|
-
def banner_rows(width)
|
|
2068
|
-
@banner.rows(width)
|
|
2069
|
-
end
|
|
2070
640
|
|
|
2071
|
-
def banner_logo_rows
|
|
2072
|
-
@banner.logo_rows(screen_width)
|
|
2073
|
-
end
|
|
2074
641
|
|
|
2075
|
-
def question_overlay_rows(width)
|
|
2076
|
-
title = "Question #{@question_state[:index]}/#{@question_state[:total]} · #{@question_state[:header]}"
|
|
2077
|
-
lines = [
|
|
2078
|
-
overlay_text_line(@question_state[:question].to_s, :bold),
|
|
2079
|
-
overlay_text_line("↑/↓ select · Enter choose · Esc cancel", :muted),
|
|
2080
|
-
overlay_blank_line
|
|
2081
|
-
]
|
|
2082
|
-
question_choices.each_with_index do |choice, index|
|
|
2083
|
-
selected = index == question_selection_index
|
|
2084
|
-
lines << overlay_choice_line(choice_text(choice, selected: selected), selected: selected)
|
|
2085
|
-
end
|
|
2086
|
-
overlay_card_rows(title, lines, width)
|
|
2087
|
-
end
|
|
2088
642
|
|
|
2089
|
-
def slash_overlay_rows(width, height: screen_height)
|
|
2090
|
-
return [] unless slash_overlay_visible?
|
|
2091
643
|
|
|
2092
|
-
visible = visible_slash_overlay_matches(slash_overlay_matches, height: height)
|
|
2093
|
-
start_index = visible[:start]
|
|
2094
|
-
lines = visible[:commands].each_with_index.map do |command, offset|
|
|
2095
|
-
index = start_index + offset
|
|
2096
|
-
hint = command[:argument_hint].empty? ? "" : " #{command[:argument_hint]}"
|
|
2097
|
-
description = command[:description].empty? ? "" : " — #{command[:description]}"
|
|
2098
|
-
overlay_choice_line("/#{command[:name]}#{hint}#{description}", selected: index == @slash_selection_index)
|
|
2099
|
-
end
|
|
2100
|
-
overlay_card_rows("Slash commands", lines, width)
|
|
2101
|
-
end
|
|
2102
644
|
|
|
2103
|
-
def visible_slash_overlay_matches(matches, height: screen_height)
|
|
2104
|
-
max_rows = [[height - 7, 1].max, 8].min
|
|
2105
|
-
start = [[@slash_selection_index - max_rows + 1, 0].max, [matches.length - max_rows, 0].max].min
|
|
2106
|
-
{ start: start, commands: matches[start, max_rows] || [] }
|
|
2107
|
-
end
|
|
2108
645
|
|
|
2109
|
-
def selection_overlay_rows(width, height: screen_height)
|
|
2110
|
-
matches = selection_matches
|
|
2111
|
-
lines = [overlay_text_line("↑/↓ select · Enter open · Esc cancel", :muted), overlay_blank_line]
|
|
2112
|
-
if matches.empty?
|
|
2113
|
-
if @select_state && @select_state[:custom] && !@input.strip.empty?
|
|
2114
|
-
lines << overlay_choice_line("Use custom: #{@input.strip}", selected: true)
|
|
2115
|
-
else
|
|
2116
|
-
lines << overlay_text_line("No matches", :muted)
|
|
2117
|
-
end
|
|
2118
|
-
return overlay_card_rows(selection_overlay_title, lines, width)
|
|
2119
|
-
end
|
|
2120
646
|
|
|
2121
|
-
visible = visible_selection_matches(matches, height: height)
|
|
2122
|
-
start_index = visible[:start]
|
|
2123
|
-
visible[:choices].each_with_index do |choice, offset|
|
|
2124
|
-
index = start_index + offset
|
|
2125
|
-
lines << overlay_choice_line(choice, selected: index == selection_index)
|
|
2126
|
-
end
|
|
2127
|
-
overlay_card_rows(selection_overlay_title, lines, width)
|
|
2128
|
-
end
|
|
2129
647
|
|
|
2130
|
-
def selection_overlay_title
|
|
2131
|
-
title = @select_state && @select_state[:title].to_s
|
|
2132
|
-
title && !title.empty? ? title : "Sessions"
|
|
2133
|
-
end
|
|
2134
648
|
|
|
2135
|
-
def visible_selection_matches(matches, height: screen_height)
|
|
2136
|
-
max_rows = [[height - 7, 1].max, 8].min
|
|
2137
|
-
start = [[selection_index - max_rows + 1, 0].max, [matches.length - max_rows, 0].max].min
|
|
2138
|
-
{ start: start, choices: matches[start, max_rows] || [] }
|
|
2139
|
-
end
|
|
2140
649
|
|
|
2141
|
-
def question_custom_cursor_row
|
|
2142
|
-
4 + question_choices.index { |choice| choice[:custom] }.to_i
|
|
2143
|
-
end
|
|
2144
650
|
|
|
2145
|
-
def question_custom_cursor_col(width)
|
|
2146
|
-
card_width = overlay_card_width(width)
|
|
2147
|
-
left_padding = overlay_left_padding(width, card_width)
|
|
2148
|
-
custom_prefix = selected_question_choice&.fetch(:custom, false) || !@input.empty? ? "Type something: " : "Type something."
|
|
2149
|
-
visible_before_cursor = display_question_input(@input[0...@cursor])
|
|
2150
|
-
[[left_padding + 2 + 2 + custom_prefix.length + visible_before_cursor.length, width - 1].min, 0].max
|
|
2151
|
-
end
|
|
2152
651
|
|
|
2153
|
-
def choice_text(choice, selected: false)
|
|
2154
|
-
if choice[:custom]
|
|
2155
|
-
if selected || !@input.empty?
|
|
2156
|
-
"Type something: #{display_question_input(@input)}"
|
|
2157
|
-
else
|
|
2158
|
-
"Type something."
|
|
2159
|
-
end
|
|
2160
|
-
else
|
|
2161
|
-
description = choice[:description].empty? ? "" : " — #{choice[:description]}"
|
|
2162
|
-
"#{choice[:label]}#{description}"
|
|
2163
|
-
end
|
|
2164
|
-
end
|
|
2165
652
|
|
|
2166
|
-
def display_question_input(value)
|
|
2167
|
-
value.to_s.gsub(/\s+/, " ").strip
|
|
2168
|
-
end
|
|
2169
653
|
|
|
2170
|
-
def overlay_card_rows(title, content_rows, width)
|
|
2171
|
-
card_width = overlay_card_width(width)
|
|
2172
|
-
inner_width = [card_width - 4, 1].max
|
|
2173
|
-
rows = [overlay_top_border(title, card_width)]
|
|
2174
|
-
rows.concat(content_rows.map { |row| overlay_content_row(row, inner_width) })
|
|
2175
|
-
rows << overlay_bottom_border(card_width)
|
|
2176
|
-
rows.map { |row| align_overlay_row(row, width) }
|
|
2177
|
-
end
|
|
2178
654
|
|
|
2179
|
-
def overlay_card_width(width)
|
|
2180
|
-
return width if width < 32
|
|
2181
|
-
return width if @overlay_settings["width"] == "maximum"
|
|
2182
655
|
|
|
2183
|
-
[[width - 4, 32].max, 96].min
|
|
2184
|
-
end
|
|
2185
656
|
|
|
2186
|
-
def overlay_top_border(title, card_width)
|
|
2187
|
-
title = visible_truncate(title.to_s, [card_width - 4, 1].max)
|
|
2188
|
-
plain_length = ANSI.strip(title).length
|
|
2189
|
-
colored("╭", :primary_green) + " #{colored(title, :bright_accent_green, :bold)} " + colored("─" * [card_width - plain_length - 4, 0].max, :primary_green) + colored("╮", :primary_green)
|
|
2190
|
-
end
|
|
2191
657
|
|
|
2192
|
-
def overlay_bottom_border(card_width)
|
|
2193
|
-
colored("╰#{"─" * [card_width - 2, 0].max}╯", :primary_green)
|
|
2194
|
-
end
|
|
2195
658
|
|
|
2196
|
-
def overlay_content_row(row, inner_width)
|
|
2197
|
-
text = visible_truncate(row[:text], inner_width)
|
|
2198
|
-
text = colored(text, :bright_accent_green, :bold) if row[:selected]
|
|
2199
|
-
colored("│", :primary_green) + " " + visible_ljust(text, inner_width) + " " + colored("│", :primary_green)
|
|
2200
|
-
end
|
|
2201
659
|
|
|
2202
|
-
def overlay_text_line(text, style = nil)
|
|
2203
|
-
rendered = case style
|
|
2204
|
-
when :bold
|
|
2205
|
-
colored(text.to_s, :bold)
|
|
2206
|
-
when :muted
|
|
2207
|
-
colored(text.to_s, :gray)
|
|
2208
|
-
else
|
|
2209
|
-
text.to_s
|
|
2210
|
-
end
|
|
2211
|
-
{ text: rendered }
|
|
2212
|
-
end
|
|
2213
660
|
|
|
2214
|
-
def overlay_blank_line
|
|
2215
|
-
{ text: "" }
|
|
2216
|
-
end
|
|
2217
661
|
|
|
2218
|
-
def overlay_choice_line(text, selected: false)
|
|
2219
|
-
{ text: "#{selected ? "›" : " "} #{text}", selected: selected }
|
|
2220
|
-
end
|
|
2221
662
|
|
|
2222
|
-
def align_overlay_row(row, width)
|
|
2223
|
-
plain_length = ANSI.strip(row).length
|
|
2224
|
-
padding = [width - plain_length, 0].max
|
|
2225
|
-
left = overlay_left_padding(width, plain_length)
|
|
2226
|
-
right = padding - left
|
|
2227
|
-
(" " * left) + row + (" " * right)
|
|
2228
|
-
end
|
|
2229
663
|
|
|
2230
|
-
def overlay_left_padding(width, row_width)
|
|
2231
|
-
padding = [width - row_width, 0].max
|
|
2232
|
-
case @overlay_settings["alignment"]
|
|
2233
|
-
when "left"
|
|
2234
|
-
0
|
|
2235
|
-
when "right"
|
|
2236
|
-
padding
|
|
2237
|
-
else
|
|
2238
|
-
padding / 2
|
|
2239
|
-
end
|
|
2240
|
-
end
|
|
2241
664
|
|
|
2242
|
-
def normalize_overlay_settings(settings)
|
|
2243
|
-
values = { "alignment" => "center", "width" => "capped" }
|
|
2244
|
-
source = settings.is_a?(Hash) ? settings : {}
|
|
2245
|
-
alignment = (source[:alignment] || source["alignment"]).to_s
|
|
2246
|
-
width = (source[:width] || source["width"]).to_s
|
|
2247
|
-
values["alignment"] = alignment if %w[left center right].include?(alignment)
|
|
2248
|
-
values["width"] = width if %w[capped maximum].include?(width)
|
|
2249
|
-
values
|
|
2250
|
-
end
|
|
2251
665
|
|
|
2252
|
-
def visible_ljust(text, width)
|
|
2253
|
-
text.to_s + (" " * [width - ANSI.strip(text.to_s).length, 0].max)
|
|
2254
|
-
end
|
|
2255
666
|
|
|
2256
|
-
def visible_truncate(text, width)
|
|
2257
|
-
plain = ANSI.strip(text.to_s)
|
|
2258
|
-
return text.to_s if plain.length <= width
|
|
2259
667
|
|
|
2260
|
-
plain[0, width]
|
|
2261
|
-
end
|
|
2262
668
|
|
|
2263
|
-
def compact_composer_layout(width)
|
|
2264
|
-
cursor_line, cursor_col = cursor_logical_position
|
|
2265
|
-
prefix = "#{@prompt_label} "
|
|
2266
|
-
line = input_lines[cursor_line] || ""
|
|
2267
|
-
input_width = [width - prefix.length, 1].max
|
|
2268
|
-
visible_start = [[cursor_col - input_width + 1, 0].max, [line.length - input_width, 0].max].min
|
|
2269
|
-
visible = line[visible_start, input_width].to_s
|
|
2270
|
-
row = "#{prefix}#{visible}"[0, width].to_s.ljust(width)
|
|
2271
|
-
[[row], 0, [prefix.length + cursor_col - visible_start, width - 1].min]
|
|
2272
|
-
end
|
|
2273
669
|
|
|
2274
|
-
def input_layout(content_width)
|
|
2275
|
-
cursor_line, cursor_col = cursor_logical_position
|
|
2276
|
-
rows = []
|
|
2277
|
-
cursor_row = 0
|
|
2278
|
-
rendered_row_offset = 0
|
|
2279
|
-
|
|
2280
|
-
input_lines.each_with_index do |line, index|
|
|
2281
|
-
prefix = input_prefix(index)
|
|
2282
|
-
continuation_prefix = " " * prefix.length
|
|
2283
|
-
available = [content_width - prefix.length, 1].max
|
|
2284
|
-
chunks = line.scan(/.{1,#{available}}/m)
|
|
2285
|
-
chunks = [""] if chunks.empty?
|
|
2286
|
-
if index == cursor_line && cursor_col == line.length && line.length.positive? && (line.length % available).zero?
|
|
2287
|
-
chunks << ""
|
|
2288
|
-
end
|
|
2289
670
|
|
|
2290
|
-
if index == cursor_line
|
|
2291
|
-
cursor_row = rendered_row_offset + (cursor_col / available)
|
|
2292
|
-
end
|
|
2293
671
|
|
|
2294
|
-
chunks.each_with_index do |chunk, chunk_index|
|
|
2295
|
-
rows << "#{chunk_index.zero? ? prefix : continuation_prefix}#{chunk}"
|
|
2296
|
-
end
|
|
2297
|
-
rendered_row_offset += chunks.length
|
|
2298
|
-
end
|
|
2299
672
|
|
|
2300
|
-
prefix = input_prefix(cursor_line)
|
|
2301
|
-
available = [content_width - prefix.length, 1].max
|
|
2302
|
-
cursor_col_in_row = prefix.length + (cursor_col % available)
|
|
2303
|
-
[rows, cursor_row, cursor_col_in_row]
|
|
2304
|
-
end
|
|
2305
673
|
|
|
2306
|
-
def top_border(width)
|
|
2307
|
-
title = composer_title
|
|
2308
|
-
status = composer_status_text
|
|
2309
|
-
if status
|
|
2310
|
-
gap = width - 2 - ANSI.strip(title).length - ANSI.strip(status).length
|
|
2311
|
-
if gap >= 0
|
|
2312
|
-
return colored("╭", :primary_green) + title + colored("─" * gap, :primary_green) + status + colored("╮", :primary_green)
|
|
2313
|
-
end
|
|
2314
|
-
end
|
|
2315
|
-
plain_title = ANSI.strip(title)
|
|
2316
|
-
"#{colored("╭", :primary_green)}#{title}#{colored("─" * [width - plain_title.length - 2, 0].max, :primary_green)}#{colored("╮", :primary_green)}"
|
|
2317
|
-
end
|
|
2318
674
|
|
|
2319
|
-
def composer_title
|
|
2320
|
-
label = @prompt_label.delete_suffix(">")
|
|
2321
|
-
if @busy && @queued_count.positive?
|
|
2322
|
-
status_composer_text(busy_title("#{label} · #{@queued_count} queued"))
|
|
2323
|
-
elsif @busy && @steered_count.to_i.positive?
|
|
2324
|
-
status_composer_text(busy_title("#{label} · #{spinner_frame} steering"))
|
|
2325
|
-
elsif @busy
|
|
2326
|
-
status_composer_text(busy_title("#{label} · #{spinner_frame} #{@busy_activity}"))
|
|
2327
|
-
else
|
|
2328
|
-
status_composer_text(label)
|
|
2329
|
-
end
|
|
2330
|
-
end
|
|
2331
675
|
|
|
2332
|
-
def busy_title(text)
|
|
2333
|
-
@busy_help ? "#{text} · #{BUSY_HELP_TEXT}" : text
|
|
2334
|
-
end
|
|
2335
676
|
|
|
2336
|
-
def composer_status_text
|
|
2337
|
-
text = @composer_status&.call.to_s
|
|
2338
|
-
return nil if text.empty?
|
|
2339
677
|
|
|
2340
|
-
status_composer_text(text)
|
|
2341
|
-
end
|
|
2342
678
|
|
|
2343
|
-
def status_composer_text(text)
|
|
2344
|
-
" #{text} "
|
|
2345
|
-
end
|
|
2346
679
|
|
|
2347
|
-
def bottom_border(width)
|
|
2348
|
-
colored("╰#{"─" * [width - 2, 0].max}╯", :primary_green)
|
|
2349
|
-
end
|
|
2350
680
|
|
|
2351
|
-
def box_content_row(row, content_width)
|
|
2352
|
-
"#{colored("│", :primary_green)} #{row[0, content_width].to_s.ljust(content_width)} #{colored("│", :primary_green)}"
|
|
2353
|
-
end
|
|
2354
681
|
|
|
2355
|
-
def footer_row(content_width, text = footer_text)
|
|
2356
|
-
return nil if text.empty?
|
|
2357
682
|
|
|
2358
|
-
box_content_row(visible_truncate(text, content_width), content_width)
|
|
2359
|
-
end
|
|
2360
683
|
|
|
2361
|
-
def footer_text
|
|
2362
|
-
return "" unless @footer
|
|
2363
684
|
|
|
2364
|
-
@footer.call.to_s.gsub(/\s+/, " ").strip
|
|
2365
|
-
rescue StandardError
|
|
2366
|
-
""
|
|
2367
|
-
end
|
|
2368
685
|
|
|
2369
|
-
def attachment_badge_rows(content_width)
|
|
2370
|
-
attachment_badge_texts.map { |text| box_content_row(visible_truncate(text, content_width), content_width) }
|
|
2371
|
-
end
|
|
2372
686
|
|
|
2373
|
-
def attachment_badge_texts
|
|
2374
|
-
return [] unless @attachment_badges
|
|
2375
687
|
|
|
2376
|
-
Array(@attachment_badges.call(@input, @attachments)).map(&:to_s).reject(&:empty?)
|
|
2377
|
-
rescue ArgumentError
|
|
2378
|
-
Array(@attachment_badges.call(@input)).map(&:to_s).reject(&:empty?)
|
|
2379
|
-
rescue StandardError
|
|
2380
|
-
[]
|
|
2381
|
-
end
|
|
2382
688
|
|
|
2383
|
-
def max_visible_input_rows(attachment_count = 0, overlay_count = active_overlay_rows(screen_width).length, footer_count = footer_text.to_s.empty? ? 0 : 1, height: screen_height)
|
|
2384
|
-
input_cap = [COMPOSER_MAX_INPUT_ROWS - attachment_count, 1].max
|
|
2385
|
-
[[input_cap, height - 3 - overlay_count - footer_count - attachment_count].min, 1].max
|
|
2386
|
-
end
|
|
2387
689
|
|
|
2388
|
-
def composer_top_row(height = screen_height)
|
|
2389
|
-
[height - @reserved_rows + 1, 1].max
|
|
2390
|
-
end
|
|
2391
690
|
|
|
2392
|
-
def transcript_bottom_row(height = screen_height)
|
|
2393
|
-
[height - @reserved_rows, 1].max
|
|
2394
|
-
end
|
|
2395
691
|
|
|
2396
|
-
def move_to_screen(row, col)
|
|
2397
|
-
@output_io.print("\e[#{row};#{col}H")
|
|
2398
|
-
end
|
|
2399
692
|
|
|
2400
|
-
def input_lines
|
|
2401
|
-
lines = @input.split("\n", -1)
|
|
2402
|
-
lines.empty? ? [""] : lines
|
|
2403
|
-
end
|
|
2404
693
|
|
|
2405
|
-
def input_prefix(_index)
|
|
2406
|
-
""
|
|
2407
|
-
end
|
|
2408
694
|
|
|
2409
|
-
def cursor_logical_position
|
|
2410
|
-
before_cursor = @input[0...@cursor]
|
|
2411
|
-
[before_cursor.count("\n"), (before_cursor.split("\n", -1).last || "").length]
|
|
2412
|
-
end
|
|
2413
695
|
|
|
2414
|
-
def update_stream_position(text, width: screen_width)
|
|
2415
|
-
ANSI.strip(text).each_char do |char|
|
|
2416
|
-
case char
|
|
2417
|
-
when "\n", "\r"
|
|
2418
|
-
@stream_col = 0
|
|
2419
|
-
@stream_pending_wrap = false
|
|
2420
|
-
else
|
|
2421
|
-
@stream_pending_wrap = false
|
|
2422
|
-
@stream_col += 1
|
|
2423
|
-
if @stream_col >= width
|
|
2424
|
-
@stream_col = 0
|
|
2425
|
-
@stream_pending_wrap = true
|
|
2426
|
-
end
|
|
2427
|
-
end
|
|
2428
|
-
end
|
|
2429
|
-
end
|
|
2430
696
|
|
|
2431
|
-
def colored(text, *styles)
|
|
2432
|
-
ANSI.colorize(text, *styles, enabled: @color_enabled)
|
|
2433
|
-
end
|
|
2434
697
|
|
|
2435
|
-
def transcript_label(label)
|
|
2436
|
-
label == "Assistant" ? @assistant_label : label
|
|
2437
|
-
end
|
|
2438
698
|
|
|
2439
|
-
def label_color(label)
|
|
2440
|
-
case label
|
|
2441
|
-
when "Reasoning"
|
|
2442
|
-
:yellow
|
|
2443
|
-
when "Assistant", "Kward"
|
|
2444
|
-
:green
|
|
2445
|
-
when "Tool"
|
|
2446
|
-
:magenta
|
|
2447
|
-
when "Tool output"
|
|
2448
|
-
:cyan
|
|
2449
|
-
else
|
|
2450
|
-
:blue
|
|
2451
|
-
end
|
|
2452
|
-
end
|
|
2453
699
|
|
|
2454
|
-
def screen_size
|
|
2455
|
-
[screen_width, screen_height]
|
|
2456
|
-
end
|
|
2457
700
|
|
|
2458
|
-
def screen_width
|
|
2459
|
-
[TTY::Screen.width, 1].max
|
|
2460
|
-
end
|
|
2461
701
|
|
|
2462
|
-
def screen_height
|
|
2463
|
-
[TTY::Screen.height, 2].max
|
|
2464
|
-
end
|
|
2465
702
|
end
|
|
2466
703
|
end
|