openclacky 0.7.0 → 0.7.2
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/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
|
@@ -17,20 +17,39 @@ module Clacky
|
|
|
17
17
|
@height = 16
|
|
18
18
|
@title = ""
|
|
19
19
|
@fields = []
|
|
20
|
+
@choices = []
|
|
20
21
|
@values = {}
|
|
22
|
+
@selected_index = 0
|
|
23
|
+
@mode = :form
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
# Configure and show the modal
|
|
24
27
|
# @param title [String] Modal title
|
|
25
|
-
# @param fields [Array<Hash>] Field definitions
|
|
28
|
+
# @param fields [Array<Hash>] Field definitions (for form mode)
|
|
29
|
+
# @param choices [Array<Hash>] Choice definitions (for menu mode)
|
|
30
|
+
# Example: [{ name: "Option 1", value: :opt1 }, { name: "---", disabled: true }]
|
|
26
31
|
# @param validator [Proc, nil] Optional validation callback that receives values hash
|
|
27
32
|
# Should return { success: true } or { success: false, error: "message" }
|
|
28
|
-
# @return [Hash, nil] Hash of field values, or nil if cancelled
|
|
29
|
-
def show(title:, fields
|
|
33
|
+
# @return [Hash, nil] Hash of field values or selected value, or nil if cancelled
|
|
34
|
+
def show(title:, fields: nil, choices: nil, validator: nil)
|
|
30
35
|
@title = title
|
|
31
|
-
@
|
|
36
|
+
@mode = choices ? :menu : :form
|
|
37
|
+
@fields = fields || []
|
|
38
|
+
@choices = choices || []
|
|
32
39
|
@values = {}
|
|
33
40
|
@error_message = nil
|
|
41
|
+
@selected_index = 0
|
|
42
|
+
|
|
43
|
+
# For menu mode, find first non-disabled choice
|
|
44
|
+
if @mode == :menu
|
|
45
|
+
@selected_index = @choices.index { |c| !c[:disabled] } || 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Adjust height for menu mode
|
|
49
|
+
if @mode == :menu
|
|
50
|
+
visible_items = [@choices.length, 15].min
|
|
51
|
+
@height = visible_items + 4 # +4 for title, borders, and instructions
|
|
52
|
+
end
|
|
34
53
|
|
|
35
54
|
# Get terminal size
|
|
36
55
|
term_height, term_width = IO.console.winsize
|
|
@@ -40,60 +59,12 @@ module Clacky
|
|
|
40
59
|
start_col = [(term_width - @width) / 2, 1].max
|
|
41
60
|
|
|
42
61
|
begin
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
draw_error_message(start_row + @height - 5, start_col)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Draw instructions
|
|
53
|
-
draw_buttons(start_row + @height - 3, start_col)
|
|
54
|
-
|
|
55
|
-
# Collect input for each field
|
|
56
|
-
current_row = start_row + 3
|
|
57
|
-
@fields.each do |field|
|
|
58
|
-
value = collect_field_input(field, current_row, start_col)
|
|
59
|
-
if value == :cancelled
|
|
60
|
-
print "\e[?25l" # Hide cursor
|
|
61
|
-
return nil # User pressed Esc
|
|
62
|
-
end
|
|
63
|
-
@values[field[:name]] = value
|
|
64
|
-
current_row += 2
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# All fields collected - validate if validator provided
|
|
68
|
-
if validator
|
|
69
|
-
# Show "Testing..." message
|
|
70
|
-
testing_row = start_row + @height - 5
|
|
71
|
-
testing_col = start_col + 3
|
|
72
|
-
print "\e[#{testing_row};#{testing_col}H\e[K"
|
|
73
|
-
print @pastel.cyan("⏳ Testing connection...")
|
|
74
|
-
STDOUT.flush
|
|
75
|
-
|
|
76
|
-
validation_result = validator.call(@values)
|
|
77
|
-
|
|
78
|
-
# Clear testing message
|
|
79
|
-
print "\e[#{testing_row};#{testing_col}H\e[K"
|
|
80
|
-
|
|
81
|
-
if validation_result[:success]
|
|
82
|
-
# Validation passed - hide cursor and return values
|
|
83
|
-
print "\e[?25l"
|
|
84
|
-
return @values
|
|
85
|
-
else
|
|
86
|
-
# Validation failed - show error and loop again
|
|
87
|
-
@error_message = validation_result[:error] || "Validation failed"
|
|
88
|
-
# Clear modal to redraw with error
|
|
89
|
-
clear_modal(start_row, start_col)
|
|
90
|
-
sleep 1.5 # Give user time to read the error
|
|
91
|
-
end
|
|
92
|
-
else
|
|
93
|
-
# No validator - return immediately
|
|
94
|
-
print "\e[?25l"
|
|
95
|
-
return @values
|
|
96
|
-
end
|
|
62
|
+
if @mode == :menu
|
|
63
|
+
# Menu mode - show choices and handle selection
|
|
64
|
+
return show_menu_mode(start_row, start_col)
|
|
65
|
+
else
|
|
66
|
+
# Form mode - collect field inputs
|
|
67
|
+
return show_form_mode(start_row, start_col, validator)
|
|
97
68
|
end
|
|
98
69
|
ensure
|
|
99
70
|
# Clear modal area
|
|
@@ -109,6 +80,101 @@ module Clacky
|
|
|
109
80
|
|
|
110
81
|
private
|
|
111
82
|
|
|
83
|
+
# Show menu mode
|
|
84
|
+
private def show_menu_mode(start_row, start_col)
|
|
85
|
+
loop do
|
|
86
|
+
# Draw menu
|
|
87
|
+
draw_modal(start_row, start_col)
|
|
88
|
+
draw_menu_choices(start_row + 2, start_col)
|
|
89
|
+
draw_menu_instructions(start_row + @height - 2, start_col)
|
|
90
|
+
|
|
91
|
+
# Read input
|
|
92
|
+
char = STDIN.getch
|
|
93
|
+
|
|
94
|
+
case char
|
|
95
|
+
when "\r", "\n" # Enter - select current choice
|
|
96
|
+
selected = @choices[@selected_index]
|
|
97
|
+
return nil if selected.nil? || selected[:disabled]
|
|
98
|
+
print "\e[?25l" # Hide cursor
|
|
99
|
+
return selected[:value]
|
|
100
|
+
when "\e" # Escape sequence
|
|
101
|
+
seq = STDIN.read_nonblock(2) rescue ''
|
|
102
|
+
if seq.empty?
|
|
103
|
+
# Just Esc key - cancel
|
|
104
|
+
print "\e[?25l"
|
|
105
|
+
return nil
|
|
106
|
+
elsif seq == '[A' # Up arrow
|
|
107
|
+
move_menu_selection(-1)
|
|
108
|
+
elsif seq == '[B' # Down arrow
|
|
109
|
+
move_menu_selection(1)
|
|
110
|
+
end
|
|
111
|
+
when "\u0003" # Ctrl+C
|
|
112
|
+
print "\e[?25l"
|
|
113
|
+
return nil
|
|
114
|
+
when 'k', 'K' # Vim-style up
|
|
115
|
+
move_menu_selection(-1)
|
|
116
|
+
when 'j', 'J' # Vim-style down
|
|
117
|
+
move_menu_selection(1)
|
|
118
|
+
when 'q', 'Q' # Quit
|
|
119
|
+
print "\e[?25l"
|
|
120
|
+
return nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Show form mode
|
|
126
|
+
private def show_form_mode(start_row, start_col, validator)
|
|
127
|
+
loop do
|
|
128
|
+
# Draw modal background and border
|
|
129
|
+
draw_modal(start_row, start_col)
|
|
130
|
+
|
|
131
|
+
# Draw error message if present
|
|
132
|
+
if @error_message
|
|
133
|
+
draw_error_message(start_row + @height - 5, start_col)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Draw instructions
|
|
137
|
+
draw_buttons(start_row + @height - 3, start_col)
|
|
138
|
+
|
|
139
|
+
# Collect input for each field
|
|
140
|
+
current_row = start_row + 3
|
|
141
|
+
@fields.each do |field|
|
|
142
|
+
# Use previously entered value as default if validation failed
|
|
143
|
+
field_with_previous = field.dup
|
|
144
|
+
field_with_previous[:default] = @values[field[:name]] || field[:default]
|
|
145
|
+
|
|
146
|
+
value = collect_field_input(field_with_previous, current_row, start_col)
|
|
147
|
+
if value == :cancelled
|
|
148
|
+
print "\e[?25l" # Hide cursor
|
|
149
|
+
return nil # User pressed Esc
|
|
150
|
+
end
|
|
151
|
+
@values[field[:name]] = value
|
|
152
|
+
current_row += 2
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# All fields collected - validate if validator provided
|
|
156
|
+
if validator
|
|
157
|
+
# Show "Testing..." message
|
|
158
|
+
testing_row = start_row + @height - 5
|
|
159
|
+
testing_col = start_col + 3
|
|
160
|
+
print "\e[#{testing_row};#{testing_col}H\e[K"
|
|
161
|
+
print "\e[#{testing_row + 1};#{testing_col}H\e[K"
|
|
162
|
+
print @pastel.cyan("⏳ Testing connection...")
|
|
163
|
+
STDOUT.flush
|
|
164
|
+
|
|
165
|
+
validation_result = validator.call(@values)
|
|
166
|
+
|
|
167
|
+
# Clear testing messages
|
|
168
|
+
print "\e[#{testing_row};#{testing_col}H\e[K"
|
|
169
|
+
print "\e[#{testing_row + 1};#{testing_col}H\e[K"
|
|
170
|
+
else
|
|
171
|
+
# No validator - return immediately
|
|
172
|
+
print "\e[?25l"
|
|
173
|
+
return @values
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
112
178
|
# Draw the modal background and border
|
|
113
179
|
private def draw_modal(start_row, start_col)
|
|
114
180
|
# Use theme colors - cyan for border, bright_cyan for title
|
|
@@ -167,8 +233,9 @@ module Clacky
|
|
|
167
233
|
# Show placeholder in dim gray
|
|
168
234
|
display_text = @pastel.dim(placeholder)
|
|
169
235
|
elsif field[:mask]
|
|
170
|
-
# Show masked input
|
|
171
|
-
|
|
236
|
+
# Show masked input - limit display length to prevent overflow
|
|
237
|
+
mask_length = [buffer.length, input_width].min
|
|
238
|
+
display_text = @pastel.cyan('*' * mask_length)
|
|
172
239
|
else
|
|
173
240
|
# Show normal input
|
|
174
241
|
display_text = @pastel.cyan(buffer)
|
|
@@ -232,13 +299,17 @@ module Clacky
|
|
|
232
299
|
|
|
233
300
|
# Draw error message
|
|
234
301
|
private def draw_error_message(row, col)
|
|
302
|
+
return if @error_message.nil? || @error_message.empty?
|
|
303
|
+
|
|
235
304
|
max_width = @width - 6
|
|
236
305
|
# Truncate error message if too long
|
|
237
306
|
error_text = @error_message.length > max_width ? @error_message[0..max_width-4] + "..." : @error_message
|
|
238
307
|
error_col = col + 3
|
|
239
308
|
|
|
240
|
-
|
|
241
|
-
print "\e[#{row};#{error_col}H
|
|
309
|
+
# Clear the line first to prevent leftover characters
|
|
310
|
+
print "\e[#{row};#{error_col}H\e[K"
|
|
311
|
+
formatted = @pastel.red("⚠ #{error_text}")
|
|
312
|
+
print formatted
|
|
242
313
|
end
|
|
243
314
|
|
|
244
315
|
# Draw confirmation buttons
|
|
@@ -257,6 +328,72 @@ module Clacky
|
|
|
257
328
|
print "\e[#{start_row + i};#{start_col}H#{' ' * @width}"
|
|
258
329
|
end
|
|
259
330
|
end
|
|
331
|
+
|
|
332
|
+
# Draw menu choices
|
|
333
|
+
private def draw_menu_choices(start_row, start_col)
|
|
334
|
+
@choices.each_with_index do |choice, index|
|
|
335
|
+
row = start_row + index
|
|
336
|
+
draw_menu_choice(choice, index, row, start_col)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Draw a single menu choice
|
|
341
|
+
private def draw_menu_choice(choice, index, row, col)
|
|
342
|
+
is_selected = (index == @selected_index)
|
|
343
|
+
|
|
344
|
+
# Calculate available width for text
|
|
345
|
+
text_width = @width - 6 # Account for borders, marker, and padding
|
|
346
|
+
|
|
347
|
+
# Prepare choice text
|
|
348
|
+
choice_text = choice[:name] || ""
|
|
349
|
+
if choice_text.length > text_width
|
|
350
|
+
choice_text = choice_text[0..text_width-4] + "..."
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Prepare marker and styling
|
|
354
|
+
if choice[:disabled]
|
|
355
|
+
# Disabled choice (like separator)
|
|
356
|
+
marker = " "
|
|
357
|
+
text = @pastel.dim(choice_text)
|
|
358
|
+
elsif is_selected
|
|
359
|
+
marker = @pastel.bright_cyan("→ ")
|
|
360
|
+
text = @pastel.bright_white(choice_text)
|
|
361
|
+
else
|
|
362
|
+
marker = " "
|
|
363
|
+
text = @pastel.white(choice_text)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Draw line
|
|
367
|
+
print "\e[#{row};#{col}H"
|
|
368
|
+
print @pastel.cyan("│") + marker + text
|
|
369
|
+
|
|
370
|
+
# Pad to width
|
|
371
|
+
used_length = choice_text.length + 2 # marker is 2 chars
|
|
372
|
+
padding_needed = @width - used_length - 2 # -2 for borders
|
|
373
|
+
print " " * padding_needed + @pastel.cyan("│")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Draw menu instructions
|
|
377
|
+
private def draw_menu_instructions(row, col)
|
|
378
|
+
instructions = "↑↓/jk: Navigate • Enter: Select • Esc/q: Cancel"
|
|
379
|
+
padding = (@width - instructions.length - 2) / 2
|
|
380
|
+
remaining = @width - padding - instructions.length - 2
|
|
381
|
+
|
|
382
|
+
print "\e[#{row};#{col}H"
|
|
383
|
+
border_line = @pastel.cyan("└" + "─" * padding)
|
|
384
|
+
instr_part = @pastel.dim(instructions)
|
|
385
|
+
border_rest = @pastel.cyan("─" * remaining + "┘")
|
|
386
|
+
print border_line + instr_part + border_rest
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Move menu selection up or down
|
|
390
|
+
private def move_menu_selection(direction)
|
|
391
|
+
loop do
|
|
392
|
+
@selected_index = (@selected_index + direction) % @choices.length
|
|
393
|
+
# Skip disabled choices
|
|
394
|
+
break unless @choices[@selected_index][:disabled]
|
|
395
|
+
end
|
|
396
|
+
end
|
|
260
397
|
end
|
|
261
398
|
end
|
|
262
399
|
end
|
|
@@ -15,6 +15,7 @@ module Clacky
|
|
|
15
15
|
@render_mutex = Mutex.new
|
|
16
16
|
@output_row = 0 # Track current output row position
|
|
17
17
|
@last_fixed_area_height = 0 # Track previous fixed area height to detect shrinkage
|
|
18
|
+
@fullscreen_mode = false # Track if in fullscreen mode
|
|
18
19
|
|
|
19
20
|
calculate_layout
|
|
20
21
|
setup_resize_handler
|
|
@@ -92,10 +93,9 @@ module Clacky
|
|
|
92
93
|
def position_inline_input_cursor(inline_input)
|
|
93
94
|
return unless inline_input
|
|
94
95
|
|
|
95
|
-
# Use
|
|
96
|
-
prompt = inline_input.prompt
|
|
96
|
+
# Use InlineInput's method to calculate cursor position (handles continuation prompt correctly)
|
|
97
97
|
width = screen.width
|
|
98
|
-
wrap_row, wrap_col = inline_input.
|
|
98
|
+
wrap_row, wrap_col = inline_input.cursor_position_for_display(width)
|
|
99
99
|
|
|
100
100
|
# Get the number of lines InlineInput occupies (considering wrapping)
|
|
101
101
|
line_count = inline_input.line_count(width)
|
|
@@ -156,7 +156,7 @@ module Clacky
|
|
|
156
156
|
screen.move_cursor(row, 0)
|
|
157
157
|
screen.clear_line
|
|
158
158
|
end
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
# Move cursor to start of a new line after last output
|
|
161
161
|
# Use \r to ensure we're at column 0, then move down
|
|
162
162
|
screen.move_cursor([@output_row, 0].max, 0)
|
|
@@ -175,10 +175,10 @@ module Clacky
|
|
|
175
175
|
screen.move_cursor(row, 0)
|
|
176
176
|
screen.clear_line
|
|
177
177
|
end
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
# Reset output position to beginning
|
|
180
180
|
@output_row = 0
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
# Re-render fixed areas to ensure they stay in place
|
|
183
183
|
render_fixed_areas
|
|
184
184
|
screen.flush
|
|
@@ -316,33 +316,31 @@ module Clacky
|
|
|
316
316
|
screen.flush
|
|
317
317
|
end
|
|
318
318
|
|
|
319
|
-
private
|
|
320
|
-
|
|
321
319
|
# Write a single line to output area
|
|
322
320
|
# Handles scrolling when reaching fixed area
|
|
323
321
|
# @param line [String] Single line to write (should not contain newlines)
|
|
324
322
|
def write_output_line(line)
|
|
325
323
|
# Calculate where fixed area starts (this is where output area ends)
|
|
326
324
|
max_output_row = fixed_area_start_row
|
|
327
|
-
|
|
325
|
+
|
|
328
326
|
# If we're about to write into the fixed area, scroll first
|
|
329
327
|
if @output_row >= max_output_row
|
|
330
328
|
# Trigger terminal scroll by printing newline at bottom
|
|
331
329
|
screen.move_cursor(screen.height - 1, 0)
|
|
332
330
|
print "\n"
|
|
333
|
-
|
|
331
|
+
|
|
334
332
|
# After scroll, position to write at the last row of output area
|
|
335
333
|
@output_row = max_output_row - 1
|
|
336
|
-
|
|
334
|
+
|
|
337
335
|
# Important: Re-render fixed areas after scroll to prevent corruption
|
|
338
336
|
render_fixed_areas
|
|
339
337
|
end
|
|
340
|
-
|
|
338
|
+
|
|
341
339
|
# Now write the line at current position
|
|
342
340
|
screen.move_cursor(@output_row, 0)
|
|
343
341
|
screen.clear_line
|
|
344
342
|
print line
|
|
345
|
-
|
|
343
|
+
|
|
346
344
|
# Move to next row for next write
|
|
347
345
|
@output_row += 1
|
|
348
346
|
end
|
|
@@ -353,26 +351,26 @@ module Clacky
|
|
|
353
351
|
# @return [Array<String>] Array of wrapped lines
|
|
354
352
|
def wrap_long_line(line)
|
|
355
353
|
return [""] if line.nil? || line.empty?
|
|
356
|
-
|
|
354
|
+
|
|
357
355
|
max_width = screen.width
|
|
358
356
|
return [line] if max_width <= 0
|
|
359
|
-
|
|
357
|
+
|
|
360
358
|
# Strip ANSI codes for width calculation
|
|
361
359
|
visible_line = line.gsub(/\e\[[0-9;]*m/, '')
|
|
362
|
-
|
|
360
|
+
|
|
363
361
|
# Check if line needs wrapping
|
|
364
362
|
display_width = calculate_display_width(visible_line)
|
|
365
363
|
return [line] if display_width <= max_width
|
|
366
|
-
|
|
364
|
+
|
|
367
365
|
# Line needs wrapping - split by considering display width
|
|
368
366
|
wrapped = []
|
|
369
367
|
current_line = ""
|
|
370
368
|
current_width = 0
|
|
371
369
|
ansi_codes = [] # Track ANSI codes to carry over
|
|
372
|
-
|
|
370
|
+
|
|
373
371
|
# Extract ANSI codes and text segments
|
|
374
372
|
segments = line.split(/(\e\[[0-9;]*m)/)
|
|
375
|
-
|
|
373
|
+
|
|
376
374
|
segments.each do |segment|
|
|
377
375
|
if segment =~ /^\e\[[0-9;]*m$/
|
|
378
376
|
# ANSI code - add to current codes
|
|
@@ -382,7 +380,7 @@ module Clacky
|
|
|
382
380
|
# Text segment - process character by character
|
|
383
381
|
segment.each_char do |char|
|
|
384
382
|
char_width = char_display_width(char)
|
|
385
|
-
|
|
383
|
+
|
|
386
384
|
if current_width + char_width > max_width && !current_line.empty?
|
|
387
385
|
# Complete current line
|
|
388
386
|
wrapped << current_line
|
|
@@ -390,19 +388,19 @@ module Clacky
|
|
|
390
388
|
current_line = ansi_codes.join
|
|
391
389
|
current_width = 0
|
|
392
390
|
end
|
|
393
|
-
|
|
391
|
+
|
|
394
392
|
current_line += char
|
|
395
393
|
current_width += char_width
|
|
396
394
|
end
|
|
397
395
|
end
|
|
398
396
|
end
|
|
399
|
-
|
|
397
|
+
|
|
400
398
|
# Add remaining content
|
|
401
399
|
wrapped << current_line unless current_line.empty? || current_line == ansi_codes.join
|
|
402
|
-
|
|
400
|
+
|
|
403
401
|
wrapped.empty? ? [""] : wrapped
|
|
404
402
|
end
|
|
405
|
-
|
|
403
|
+
|
|
406
404
|
# Calculate display width of a single character
|
|
407
405
|
# @param char [String] Single character
|
|
408
406
|
# @return [Integer] Display width (1 or 2)
|
|
@@ -427,7 +425,7 @@ module Clacky
|
|
|
427
425
|
1
|
|
428
426
|
end
|
|
429
427
|
end
|
|
430
|
-
|
|
428
|
+
|
|
431
429
|
# Calculate display width of a string (considering multi-byte characters)
|
|
432
430
|
# @param text [String] Text to calculate
|
|
433
431
|
# @return [Integer] Display width
|
|
@@ -504,6 +502,58 @@ module Clacky
|
|
|
504
502
|
screen.show_cursor
|
|
505
503
|
end
|
|
506
504
|
|
|
505
|
+
# Restore screen from fullscreen mode (re-render everything)
|
|
506
|
+
def restore_screen
|
|
507
|
+
@render_mutex.synchronize do
|
|
508
|
+
screen.clear_screen
|
|
509
|
+
screen.hide_cursor
|
|
510
|
+
render_all_internal
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Check if in fullscreen mode
|
|
515
|
+
# @return [Boolean]
|
|
516
|
+
def fullscreen_mode?
|
|
517
|
+
@fullscreen_mode
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Enter fullscreen mode with alternate screen buffer
|
|
521
|
+
# @param lines [Array<String>] Lines to display
|
|
522
|
+
# @param hint [String] Hint message at bottom
|
|
523
|
+
def enter_fullscreen(lines, hint: "Press Ctrl+O to return")
|
|
524
|
+
return if @fullscreen_mode
|
|
525
|
+
|
|
526
|
+
@fullscreen_mode = true
|
|
527
|
+
|
|
528
|
+
# Enter alternate screen buffer
|
|
529
|
+
print "\e[?1049h"
|
|
530
|
+
# Clear screen and move cursor to top
|
|
531
|
+
print "\e[2J\e[H"
|
|
532
|
+
$stdout.flush
|
|
533
|
+
|
|
534
|
+
# Show all lines with proper line endings (CR+LF)
|
|
535
|
+
lines.each do |line|
|
|
536
|
+
# Strip trailing newline and print with CR+LF
|
|
537
|
+
print line.chomp + "\r\n"
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Show hint at bottom
|
|
541
|
+
print "\r\n"
|
|
542
|
+
print "\e[36m#{hint}\e[0m\r\n"
|
|
543
|
+
$stdout.flush
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Exit fullscreen mode and restore previous screen
|
|
547
|
+
def exit_fullscreen
|
|
548
|
+
return unless @fullscreen_mode
|
|
549
|
+
|
|
550
|
+
@fullscreen_mode = false
|
|
551
|
+
|
|
552
|
+
# Exit alternate screen buffer (automatically restores previous screen)
|
|
553
|
+
print "\e[?1049l"
|
|
554
|
+
$stdout.flush
|
|
555
|
+
end
|
|
556
|
+
|
|
507
557
|
# Setup handler for window resize
|
|
508
558
|
def setup_resize_handler
|
|
509
559
|
Signal.trap("WINCH") do
|
|
@@ -7,6 +7,11 @@ module Clacky
|
|
|
7
7
|
# LineEditor module provides single-line text editing functionality
|
|
8
8
|
# Shared by InputArea and InlineInput components
|
|
9
9
|
module LineEditor
|
|
10
|
+
# Maximum content width ratio (percentage of terminal width)
|
|
11
|
+
# Use 90% of terminal width for better readability on wide screens
|
|
12
|
+
# This dynamically adjusts based on terminal size
|
|
13
|
+
MAX_CONTENT_WIDTH_RATIO = 0.9
|
|
14
|
+
|
|
10
15
|
attr_reader :cursor_position
|
|
11
16
|
|
|
12
17
|
def initialize_line_editor
|
|
@@ -200,8 +205,9 @@ module Clacky
|
|
|
200
205
|
# Get cursor position considering line wrapping
|
|
201
206
|
# @param prompt [String] Prompt string before the line (may contain ANSI codes)
|
|
202
207
|
# @param width [Integer] Terminal width for wrapping
|
|
208
|
+
# @param continuation_prompt [String] Prompt for continuation lines (default: "> ")
|
|
203
209
|
# @return [Array<Integer>] Row and column position (0-indexed)
|
|
204
|
-
def cursor_position_with_wrap(prompt = "", width = TTY::Screen.width)
|
|
210
|
+
def cursor_position_with_wrap(prompt = "", width = TTY::Screen.width, continuation_prompt = "> ")
|
|
205
211
|
return [0, cursor_column(prompt)] if width <= 0
|
|
206
212
|
|
|
207
213
|
prompt_width = calculate_display_width(strip_ansi_codes(prompt))
|
|
@@ -232,7 +238,15 @@ module Clacky
|
|
|
232
238
|
text_in_segment_before_cursor = chars[segment_start...(segment_start + cursor_pos_in_segment)].join
|
|
233
239
|
display_width = calculate_display_width(text_in_segment_before_cursor)
|
|
234
240
|
|
|
235
|
-
|
|
241
|
+
# Use appropriate prompt width based on which segment (row) we're on
|
|
242
|
+
# First line uses original prompt, subsequent lines use continuation prompt
|
|
243
|
+
actual_prompt_width = if cursor_segment_idx == 0
|
|
244
|
+
prompt_width
|
|
245
|
+
else
|
|
246
|
+
calculate_display_width(strip_ansi_codes(continuation_prompt))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
col = actual_prompt_width + display_width
|
|
236
250
|
row = cursor_segment_idx
|
|
237
251
|
|
|
238
252
|
[row, col]
|
|
@@ -309,6 +323,13 @@ module Clacky
|
|
|
309
323
|
end
|
|
310
324
|
end
|
|
311
325
|
|
|
326
|
+
# Calculate effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
327
|
+
# @param screen_width [Integer] Terminal screen width
|
|
328
|
+
# @return [Integer] Effective content width to use
|
|
329
|
+
private def effective_content_width(screen_width)
|
|
330
|
+
(screen_width * MAX_CONTENT_WIDTH_RATIO).to_i
|
|
331
|
+
end
|
|
332
|
+
|
|
312
333
|
# Render a segment of a line with cursor if cursor is in this segment
|
|
313
334
|
# @param line [String] Full line text
|
|
314
335
|
# @param segment_start [Integer] Start position of segment in line (char index)
|
|
@@ -17,10 +17,10 @@ module Clacky
|
|
|
17
17
|
# Get current theme colors
|
|
18
18
|
theme = ThemeManager.current_theme
|
|
19
19
|
|
|
20
|
-
# Configure tty-markdown
|
|
21
|
-
# tty-markdown uses Pastel internally, we can configure symbols
|
|
20
|
+
# Configure tty-markdown with custom theme and symbols
|
|
22
21
|
parsed = TTY::Markdown.parse(content,
|
|
23
|
-
|
|
22
|
+
theme: theme_colors,
|
|
23
|
+
symbols: custom_symbols,
|
|
24
24
|
width: TTY::Screen.width - 4 # Leave some margin
|
|
25
25
|
)
|
|
26
26
|
|
|
@@ -57,21 +57,42 @@ module Clacky
|
|
|
57
57
|
theme = ThemeManager.current_theme
|
|
58
58
|
|
|
59
59
|
# Map our theme colors to tty-markdown's expected format
|
|
60
|
+
# Note: theme.colors values are already arrays, so we need to flatten when adding styles
|
|
60
61
|
{
|
|
61
62
|
# Headers use info color (cyan/blue)
|
|
62
|
-
|
|
63
|
+
h1: Array(theme.colors[:info]) + [:bold],
|
|
64
|
+
h2: Array(theme.colors[:info]) + [:bold],
|
|
65
|
+
h3: Array(theme.colors[:info]),
|
|
66
|
+
h4: Array(theme.colors[:info]),
|
|
67
|
+
h5: Array(theme.colors[:info]),
|
|
68
|
+
h6: Array(theme.colors[:info]),
|
|
69
|
+
# Horizontal rule - make it subtle (dim gray)
|
|
70
|
+
hr: [:bright_black],
|
|
63
71
|
# Code blocks use dim color
|
|
64
|
-
code: theme.colors[:thinking],
|
|
72
|
+
code: Array(theme.colors[:thinking]),
|
|
65
73
|
# Links use success color (green)
|
|
66
|
-
link: theme.colors[:success],
|
|
74
|
+
link: Array(theme.colors[:success]),
|
|
67
75
|
# Lists use default text color
|
|
68
|
-
list: :bright_white,
|
|
76
|
+
list: [:bright_white],
|
|
69
77
|
# Strong/bold use bright white
|
|
70
|
-
strong: :bright_white,
|
|
78
|
+
strong: [:bright_white, :bold],
|
|
71
79
|
# Emphasis/italic use white
|
|
72
|
-
em: :white,
|
|
80
|
+
em: [:white],
|
|
73
81
|
# Note/blockquote use dim color
|
|
74
|
-
note: theme.colors[:thinking],
|
|
82
|
+
note: Array(theme.colors[:thinking]),
|
|
83
|
+
quote: Array(theme.colors[:thinking]),
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get custom symbols for markdown rendering
|
|
88
|
+
# @return [Hash] Symbol configuration for tty-markdown
|
|
89
|
+
def custom_symbols
|
|
90
|
+
{
|
|
91
|
+
override: {
|
|
92
|
+
# Make horizontal rule simpler - just a line without decorative diamonds
|
|
93
|
+
diamond: "",
|
|
94
|
+
line: "-"
|
|
95
|
+
}
|
|
75
96
|
}
|
|
76
97
|
end
|
|
77
98
|
end
|
|
@@ -224,10 +224,12 @@ module Clacky
|
|
|
224
224
|
when "\u0006" then :ctrl_f
|
|
225
225
|
when "\u000B" then :ctrl_k
|
|
226
226
|
when "\u000C" then :ctrl_l
|
|
227
|
+
when "\u000F" then :ctrl_o
|
|
227
228
|
when "\u0012" then :ctrl_r
|
|
228
229
|
when "\u0015" then :ctrl_u
|
|
229
230
|
when "\u0016" then :ctrl_v
|
|
230
231
|
when "\u0017" then :ctrl_w
|
|
232
|
+
when "\t" then :tab
|
|
231
233
|
else char
|
|
232
234
|
end
|
|
233
235
|
end
|