openclacky 0.6.1 → 0.6.3
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/README.md +39 -88
- data/homebrew/README.md +96 -0
- data/homebrew/openclacky.rb +24 -0
- data/lib/clacky/agent.rb +557 -122
- data/lib/clacky/cli.rb +431 -3
- data/lib/clacky/default_skills/skill-add/SKILL.md +66 -0
- data/lib/clacky/skill.rb +236 -0
- data/lib/clacky/skill_loader.rb +320 -0
- data/lib/clacky/tools/file_reader.rb +245 -9
- data/lib/clacky/tools/grep.rb +9 -14
- data/lib/clacky/tools/safe_shell.rb +53 -17
- data/lib/clacky/tools/shell.rb +109 -5
- data/lib/clacky/tools/web_fetch.rb +81 -18
- data/lib/clacky/ui2/components/command_suggestions.rb +273 -0
- data/lib/clacky/ui2/components/inline_input.rb +34 -15
- data/lib/clacky/ui2/components/input_area.rb +279 -141
- data/lib/clacky/ui2/layout_manager.rb +147 -67
- data/lib/clacky/ui2/line_editor.rb +142 -2
- data/lib/clacky/ui2/themes/hacker_theme.rb +3 -3
- data/lib/clacky/ui2/themes/minimal_theme.rb +3 -3
- data/lib/clacky/ui2/ui_controller.rb +80 -29
- data/lib/clacky/ui2.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +7 -2
- data/lib/clacky/utils/file_ignore_helper.rb +10 -12
- data/lib/clacky/utils/file_processor.rb +201 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +249 -0
- data/scripts/uninstall.sh +146 -0
- metadata +10 -2
- data/lib/clacky/ui2/components/output_area.rb +0 -112
|
@@ -6,15 +6,15 @@ module Clacky
|
|
|
6
6
|
module UI2
|
|
7
7
|
# LayoutManager manages screen layout with split areas (output area on top, input area on bottom)
|
|
8
8
|
class LayoutManager
|
|
9
|
-
attr_reader :screen, :
|
|
9
|
+
attr_reader :screen, :input_area, :todo_area
|
|
10
10
|
|
|
11
|
-
def initialize(
|
|
11
|
+
def initialize(input_area:, todo_area: nil)
|
|
12
12
|
@screen = ScreenBuffer.new
|
|
13
|
-
@output_area = output_area
|
|
14
13
|
@input_area = input_area
|
|
15
14
|
@todo_area = todo_area
|
|
16
15
|
@render_mutex = Mutex.new
|
|
17
16
|
@output_row = 0 # Track current output row position
|
|
17
|
+
@last_fixed_area_height = 0 # Track previous fixed area height to detect shrinkage
|
|
18
18
|
|
|
19
19
|
calculate_layout
|
|
20
20
|
setup_resize_handler
|
|
@@ -35,7 +35,6 @@ module Clacky
|
|
|
35
35
|
@input_row = @todo_row + todo_height
|
|
36
36
|
|
|
37
37
|
# Update component dimensions
|
|
38
|
-
@output_area.height = @output_height
|
|
39
38
|
@input_area.row = @input_row
|
|
40
39
|
end
|
|
41
40
|
|
|
@@ -93,10 +92,21 @@ module Clacky
|
|
|
93
92
|
def position_inline_input_cursor(inline_input)
|
|
94
93
|
return unless inline_input
|
|
95
94
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
# Use the shared method from LineEditor to calculate cursor position with wrap
|
|
96
|
+
prompt = inline_input.prompt
|
|
97
|
+
width = screen.width
|
|
98
|
+
wrap_row, wrap_col = inline_input.cursor_position_with_wrap(prompt, width)
|
|
99
|
+
|
|
100
|
+
# Get the number of lines InlineInput occupies (considering wrapping)
|
|
101
|
+
line_count = inline_input.line_count(width)
|
|
102
|
+
|
|
103
|
+
# InlineInput starts at @output_row - line_count
|
|
104
|
+
# Cursor is at wrap_row within that
|
|
105
|
+
cursor_row = @output_row - line_count + wrap_row
|
|
106
|
+
cursor_col = wrap_col
|
|
107
|
+
|
|
108
|
+
# Move terminal cursor to the correct position
|
|
109
|
+
screen.move_cursor(cursor_row, cursor_col)
|
|
100
110
|
screen.flush
|
|
101
111
|
end
|
|
102
112
|
|
|
@@ -159,14 +169,16 @@ module Clacky
|
|
|
159
169
|
# Clear output area (for /clear command)
|
|
160
170
|
def clear_output
|
|
161
171
|
@render_mutex.synchronize do
|
|
162
|
-
# Clear all lines in output area (from 0 to
|
|
163
|
-
|
|
164
|
-
(0...
|
|
172
|
+
# Clear all lines in output area (from 0 to where fixed area starts)
|
|
173
|
+
max_row = fixed_area_start_row
|
|
174
|
+
(0...max_row).each do |row|
|
|
165
175
|
screen.move_cursor(row, 0)
|
|
166
176
|
screen.clear_line
|
|
167
177
|
end
|
|
168
|
-
|
|
178
|
+
|
|
179
|
+
# Reset output position to beginning
|
|
169
180
|
@output_row = 0
|
|
181
|
+
|
|
170
182
|
# Re-render fixed areas to ensure they stay in place
|
|
171
183
|
render_fixed_areas
|
|
172
184
|
screen.flush
|
|
@@ -174,89 +186,111 @@ module Clacky
|
|
|
174
186
|
end
|
|
175
187
|
|
|
176
188
|
# Append content to output area
|
|
177
|
-
#
|
|
178
|
-
# @param content [String] Content to append
|
|
189
|
+
# This is the main output method - handles scrolling and fixed area preservation
|
|
190
|
+
# @param content [String] Content to append (can be multi-line)
|
|
179
191
|
def append_output(content)
|
|
180
192
|
return if content.nil?
|
|
181
193
|
|
|
182
194
|
@render_mutex.synchronize do
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# Special handling for empty string - just add a blank line
|
|
186
|
-
if content.empty?
|
|
187
|
-
print "\n"
|
|
188
|
-
@output_row += 1
|
|
189
|
-
render_fixed_areas
|
|
190
|
-
screen.flush
|
|
191
|
-
return
|
|
192
|
-
end
|
|
195
|
+
lines = content.split("\n", -1) # -1 to keep trailing empty strings
|
|
193
196
|
|
|
194
|
-
|
|
197
|
+
lines.each_with_index do |line, index|
|
|
195
198
|
# Wrap long lines to prevent display issues
|
|
196
199
|
wrapped_lines = wrap_long_line(line)
|
|
197
|
-
|
|
198
|
-
wrapped_lines.each do |wrapped_line|
|
|
199
|
-
# If at max row, need to scroll before outputting
|
|
200
|
-
if @output_row > max_output_row
|
|
201
|
-
# Move to bottom of screen and print newline to trigger scroll
|
|
202
|
-
screen.move_cursor(screen.height - 1, 0)
|
|
203
|
-
print "\n"
|
|
204
|
-
# Stay at max_output_row for next output
|
|
205
|
-
@output_row = max_output_row
|
|
206
|
-
end
|
|
207
200
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
screen.clear_line
|
|
211
|
-
output_area.append(wrapped_line)
|
|
212
|
-
@output_row += 1
|
|
201
|
+
wrapped_lines.each do |wrapped_line|
|
|
202
|
+
write_output_line(wrapped_line)
|
|
213
203
|
end
|
|
214
204
|
end
|
|
215
205
|
|
|
216
|
-
# Re-render fixed areas at
|
|
206
|
+
# Re-render fixed areas to ensure they stay at bottom
|
|
217
207
|
render_fixed_areas
|
|
218
208
|
screen.flush
|
|
219
209
|
end
|
|
220
210
|
end
|
|
221
211
|
|
|
222
|
-
# Update the last
|
|
223
|
-
# @param content [String] Content to update
|
|
224
|
-
|
|
212
|
+
# Update the last N lines in output area (for inline input updates)
|
|
213
|
+
# @param content [String] Content to update (may contain newlines for wrapped lines)
|
|
214
|
+
# @param old_line_count [Integer] Number of lines currently occupied (for clearing)
|
|
215
|
+
def update_last_line(content, old_line_count = 1)
|
|
225
216
|
@render_mutex.synchronize do
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
217
|
+
return if @output_row == 0 # No output yet
|
|
218
|
+
|
|
219
|
+
# Calculate start row (last N lines)
|
|
220
|
+
start_row = @output_row - old_line_count
|
|
221
|
+
start_row = 0 if start_row < 0
|
|
222
|
+
|
|
223
|
+
# Clear all lines that will be updated
|
|
224
|
+
(start_row...@output_row).each do |row|
|
|
225
|
+
screen.move_cursor(row, 0)
|
|
226
|
+
screen.clear_line
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Re-render the content
|
|
230
|
+
lines = content.split("\n", -1)
|
|
231
|
+
current_row = start_row
|
|
232
|
+
|
|
233
|
+
lines.each_with_index do |line, idx|
|
|
234
|
+
screen.move_cursor(current_row, 0)
|
|
235
|
+
print line
|
|
236
|
+
current_row += 1
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Update output_row to new line count
|
|
240
|
+
@output_row = start_row + lines.length
|
|
241
|
+
|
|
242
|
+
# Clear any remaining old lines if new content has fewer lines
|
|
243
|
+
# This handles the case where content shrinks (e.g., delete from 2 lines to 1 line)
|
|
244
|
+
old_end_row = @output_row + (old_line_count - lines.length)
|
|
245
|
+
if old_end_row > @output_row && old_end_row <= start_row + old_line_count
|
|
246
|
+
# Clear the extra old lines
|
|
247
|
+
(@output_row...old_end_row).each do |row|
|
|
248
|
+
screen.move_cursor(row, 0)
|
|
249
|
+
screen.clear_line
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Re-render fixed areas to restore cursor position in input area
|
|
231
254
|
render_fixed_areas
|
|
232
255
|
screen.flush
|
|
233
256
|
end
|
|
234
257
|
end
|
|
235
258
|
|
|
236
|
-
# Remove the last
|
|
237
|
-
|
|
259
|
+
# Remove the last N lines from output area
|
|
260
|
+
# @param line_count [Integer] Number of lines to remove (default: 1)
|
|
261
|
+
def remove_last_line(line_count = 1)
|
|
238
262
|
@render_mutex.synchronize do
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
263
|
+
return if @output_row == 0 # No output to remove
|
|
264
|
+
|
|
265
|
+
# Calculate start row for removal
|
|
266
|
+
start_row = @output_row - line_count
|
|
267
|
+
start_row = 0 if start_row < 0
|
|
268
|
+
|
|
269
|
+
# Clear all lines being removed
|
|
270
|
+
(start_row...@output_row).each do |row|
|
|
271
|
+
screen.move_cursor(row, 0)
|
|
272
|
+
screen.clear_line
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Update output_row
|
|
276
|
+
@output_row = start_row
|
|
277
|
+
|
|
278
|
+
# Re-render fixed areas to ensure consistency
|
|
243
279
|
render_fixed_areas
|
|
244
280
|
screen.flush
|
|
245
281
|
end
|
|
246
282
|
end
|
|
247
283
|
|
|
248
|
-
# Scroll output area up
|
|
284
|
+
# Scroll output area up (legacy no-op)
|
|
249
285
|
# @param lines [Integer] Number of lines to scroll
|
|
250
286
|
def scroll_output_up(lines = 1)
|
|
251
|
-
|
|
252
|
-
render_output
|
|
287
|
+
# No-op - terminal handles scrolling natively
|
|
253
288
|
end
|
|
254
289
|
|
|
255
|
-
# Scroll output area down
|
|
290
|
+
# Scroll output area down (legacy no-op)
|
|
256
291
|
# @param lines [Integer] Number of lines to scroll
|
|
257
292
|
def scroll_output_down(lines = 1)
|
|
258
|
-
|
|
259
|
-
render_output
|
|
293
|
+
# No-op - terminal handles scrolling natively
|
|
260
294
|
end
|
|
261
295
|
|
|
262
296
|
# Handle window resize
|
|
@@ -266,12 +300,14 @@ module Clacky
|
|
|
266
300
|
screen.update_dimensions
|
|
267
301
|
calculate_layout
|
|
268
302
|
|
|
269
|
-
# Adjust output_row if it exceeds new
|
|
270
|
-
|
|
271
|
-
|
|
303
|
+
# Adjust @output_row if it exceeds new layout
|
|
304
|
+
# After resize, @output_row should not exceed fixed_area_start_row
|
|
305
|
+
max_allowed = fixed_area_start_row
|
|
306
|
+
@output_row = [@output_row, max_allowed].min
|
|
272
307
|
|
|
273
|
-
# Clear old fixed area lines
|
|
274
|
-
|
|
308
|
+
# Clear old fixed area and some lines above (terminal may have wrapped content)
|
|
309
|
+
clear_start = [old_gap_row - 5, 0].max
|
|
310
|
+
(clear_start...screen.height).each do |row|
|
|
275
311
|
screen.move_cursor(row, 0)
|
|
276
312
|
screen.clear_line
|
|
277
313
|
end
|
|
@@ -282,6 +318,35 @@ module Clacky
|
|
|
282
318
|
|
|
283
319
|
private
|
|
284
320
|
|
|
321
|
+
# Write a single line to output area
|
|
322
|
+
# Handles scrolling when reaching fixed area
|
|
323
|
+
# @param line [String] Single line to write (should not contain newlines)
|
|
324
|
+
def write_output_line(line)
|
|
325
|
+
# Calculate where fixed area starts (this is where output area ends)
|
|
326
|
+
max_output_row = fixed_area_start_row
|
|
327
|
+
|
|
328
|
+
# If we're about to write into the fixed area, scroll first
|
|
329
|
+
if @output_row >= max_output_row
|
|
330
|
+
# Trigger terminal scroll by printing newline at bottom
|
|
331
|
+
screen.move_cursor(screen.height - 1, 0)
|
|
332
|
+
print "\n"
|
|
333
|
+
|
|
334
|
+
# After scroll, position to write at the last row of output area
|
|
335
|
+
@output_row = max_output_row - 1
|
|
336
|
+
|
|
337
|
+
# Important: Re-render fixed areas after scroll to prevent corruption
|
|
338
|
+
render_fixed_areas
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Now write the line at current position
|
|
342
|
+
screen.move_cursor(@output_row, 0)
|
|
343
|
+
screen.clear_line
|
|
344
|
+
print line
|
|
345
|
+
|
|
346
|
+
# Move to next row for next write
|
|
347
|
+
@output_row += 1
|
|
348
|
+
end
|
|
349
|
+
|
|
285
350
|
# Wrap a long line into multiple lines based on terminal width
|
|
286
351
|
# Considers display width of multi-byte characters (e.g., Chinese characters)
|
|
287
352
|
# @param line [String] Line to wrap
|
|
@@ -392,11 +457,26 @@ module Clacky
|
|
|
392
457
|
# The InlineInput is rendered inline with output
|
|
393
458
|
return if input_area.paused?
|
|
394
459
|
|
|
460
|
+
current_fixed_height = fixed_area_height
|
|
395
461
|
start_row = fixed_area_start_row
|
|
396
462
|
gap_row = start_row
|
|
397
463
|
todo_row = gap_row + 1
|
|
398
464
|
input_row = todo_row + (@todo_area&.height || 0)
|
|
399
465
|
|
|
466
|
+
# If fixed area shrank, clear the extra lines at the top to remove residual content
|
|
467
|
+
if @last_fixed_area_height > current_fixed_height
|
|
468
|
+
height_diff = @last_fixed_area_height - current_fixed_height
|
|
469
|
+
old_start_row = screen.height - @last_fixed_area_height
|
|
470
|
+
# Clear the extra lines that are no longer part of fixed area
|
|
471
|
+
(old_start_row...(old_start_row + height_diff)).each do |row|
|
|
472
|
+
screen.move_cursor(row, 0)
|
|
473
|
+
screen.clear_line
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Update last height for next comparison
|
|
478
|
+
@last_fixed_area_height = current_fixed_height
|
|
479
|
+
|
|
400
480
|
# Render gap line
|
|
401
481
|
screen.move_cursor(gap_row, 0)
|
|
402
482
|
screen.clear_line
|
|
@@ -412,7 +492,7 @@ module Clacky
|
|
|
412
492
|
|
|
413
493
|
# Internal render all (without mutex)
|
|
414
494
|
def render_all_internal
|
|
415
|
-
|
|
495
|
+
# Output flows naturally, just render fixed areas
|
|
416
496
|
render_fixed_areas
|
|
417
497
|
screen.flush
|
|
418
498
|
end
|
|
@@ -188,14 +188,154 @@ module Clacky
|
|
|
188
188
|
# Strip ANSI codes from prompt to get actual display width
|
|
189
189
|
visible_prompt = strip_ansi_codes(prompt)
|
|
190
190
|
prompt_display_width = calculate_display_width(visible_prompt)
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
# Calculate display width of text before cursor
|
|
193
193
|
chars = @line.chars
|
|
194
194
|
text_before_cursor = chars[0...@cursor_position].join
|
|
195
195
|
text_display_width = calculate_display_width(text_before_cursor)
|
|
196
|
-
|
|
196
|
+
|
|
197
197
|
prompt_display_width + text_display_width
|
|
198
198
|
end
|
|
199
|
+
|
|
200
|
+
# Get cursor position considering line wrapping
|
|
201
|
+
# @param prompt [String] Prompt string before the line (may contain ANSI codes)
|
|
202
|
+
# @param width [Integer] Terminal width for wrapping
|
|
203
|
+
# @return [Array<Integer>] Row and column position (0-indexed)
|
|
204
|
+
def cursor_position_with_wrap(prompt = "", width = TTY::Screen.width)
|
|
205
|
+
return [0, cursor_column(prompt)] if width <= 0
|
|
206
|
+
|
|
207
|
+
prompt_width = calculate_display_width(strip_ansi_codes(prompt))
|
|
208
|
+
available_width = width - prompt_width
|
|
209
|
+
|
|
210
|
+
# Get wrapped segments for current line
|
|
211
|
+
wrapped_segments = wrap_line(@line, available_width)
|
|
212
|
+
|
|
213
|
+
# Find which segment contains cursor
|
|
214
|
+
cursor_segment_idx = 0
|
|
215
|
+
cursor_pos_in_segment = @cursor_position
|
|
216
|
+
|
|
217
|
+
wrapped_segments.each_with_index do |segment, idx|
|
|
218
|
+
if @cursor_position >= segment[:start] && @cursor_position < segment[:end]
|
|
219
|
+
cursor_segment_idx = idx
|
|
220
|
+
cursor_pos_in_segment = @cursor_position - segment[:start]
|
|
221
|
+
break
|
|
222
|
+
elsif @cursor_position >= segment[:end] && idx == wrapped_segments.size - 1
|
|
223
|
+
cursor_segment_idx = idx
|
|
224
|
+
cursor_pos_in_segment = segment[:end] - segment[:start]
|
|
225
|
+
break
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Calculate display width of text before cursor in this segment
|
|
230
|
+
chars = @line.chars
|
|
231
|
+
segment_start = wrapped_segments[cursor_segment_idx][:start]
|
|
232
|
+
text_in_segment_before_cursor = chars[segment_start...(segment_start + cursor_pos_in_segment)].join
|
|
233
|
+
display_width = calculate_display_width(text_in_segment_before_cursor)
|
|
234
|
+
|
|
235
|
+
col = prompt_width + display_width
|
|
236
|
+
row = cursor_segment_idx
|
|
237
|
+
|
|
238
|
+
[row, col]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Wrap a line into multiple segments based on available width
|
|
242
|
+
# Considers display width of characters (multi-byte characters like Chinese)
|
|
243
|
+
# @param line [String] The line to wrap
|
|
244
|
+
# @param max_width [Integer] Maximum display width per wrapped line
|
|
245
|
+
# @return [Array<Hash>] Array of segment info: { text: String, start: Integer, end: Integer }
|
|
246
|
+
def wrap_line(line, max_width)
|
|
247
|
+
return [{ text: "", start: 0, end: 0 }] if line.empty?
|
|
248
|
+
return [{ text: line, start: 0, end: line.length }] if max_width <= 0
|
|
249
|
+
|
|
250
|
+
segments = []
|
|
251
|
+
chars = line.chars
|
|
252
|
+
segment_start = 0
|
|
253
|
+
current_width = 0
|
|
254
|
+
current_end = 0
|
|
255
|
+
|
|
256
|
+
chars.each_with_index do |char, idx|
|
|
257
|
+
char_width = char_display_width(char)
|
|
258
|
+
|
|
259
|
+
# If adding this character exceeds max width, complete current segment
|
|
260
|
+
if current_width + char_width > max_width && current_end > segment_start
|
|
261
|
+
segments << {
|
|
262
|
+
text: chars[segment_start...current_end].join,
|
|
263
|
+
start: segment_start,
|
|
264
|
+
end: current_end
|
|
265
|
+
}
|
|
266
|
+
segment_start = idx
|
|
267
|
+
current_end = idx + 1
|
|
268
|
+
current_width = char_width
|
|
269
|
+
else
|
|
270
|
+
current_end = idx + 1
|
|
271
|
+
current_width += char_width
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Add the last segment
|
|
276
|
+
if current_end > segment_start
|
|
277
|
+
segments << {
|
|
278
|
+
text: chars[segment_start...current_end].join,
|
|
279
|
+
start: segment_start,
|
|
280
|
+
end: current_end
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
segments.empty? ? [{ text: "", start: 0, end: 0 }] : segments
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Calculate display width of a single character
|
|
288
|
+
# @param char [String] Single character
|
|
289
|
+
# @return [Integer] Display width (1 or 2)
|
|
290
|
+
def char_display_width(char)
|
|
291
|
+
code = char.ord
|
|
292
|
+
# East Asian Wide and Fullwidth characters take 2 columns
|
|
293
|
+
if (code >= 0x1100 && code <= 0x115F) ||
|
|
294
|
+
(code >= 0x2329 && code <= 0x232A) ||
|
|
295
|
+
(code >= 0x2E80 && code <= 0x303E) ||
|
|
296
|
+
(code >= 0x3040 && code <= 0xA4CF) ||
|
|
297
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
298
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
299
|
+
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
300
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
301
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
302
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
303
|
+
(code >= 0x1F300 && code <= 0x1F9FF) ||
|
|
304
|
+
(code >= 0x20000 && code <= 0x2FFFD) ||
|
|
305
|
+
(code >= 0x30000 && code <= 0x3FFFD)
|
|
306
|
+
2
|
|
307
|
+
else
|
|
308
|
+
1
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Render a segment of a line with cursor if cursor is in this segment
|
|
313
|
+
# @param line [String] Full line text
|
|
314
|
+
# @param segment_start [Integer] Start position of segment in line (char index)
|
|
315
|
+
# @param segment_end [Integer] End position of segment in line (char index)
|
|
316
|
+
# @return [String] Rendered segment with cursor if applicable
|
|
317
|
+
def render_line_segment_with_cursor(line, segment_start, segment_end)
|
|
318
|
+
chars = line.chars
|
|
319
|
+
segment_chars = chars[segment_start...segment_end]
|
|
320
|
+
|
|
321
|
+
# Check if cursor is in this segment
|
|
322
|
+
if @cursor_position >= segment_start && @cursor_position < segment_end
|
|
323
|
+
# Cursor is in this segment
|
|
324
|
+
cursor_pos_in_segment = @cursor_position - segment_start
|
|
325
|
+
before_cursor = segment_chars[0...cursor_pos_in_segment].join
|
|
326
|
+
cursor_char = segment_chars[cursor_pos_in_segment] || " "
|
|
327
|
+
after_cursor = segment_chars[(cursor_pos_in_segment + 1)..-1]&.join || ""
|
|
328
|
+
|
|
329
|
+
"#{@pastel.white(before_cursor)}#{@pastel.on_white(@pastel.black(cursor_char))}#{@pastel.white(after_cursor)}"
|
|
330
|
+
elsif @cursor_position == segment_end && segment_end == line.length
|
|
331
|
+
# Cursor is at the very end of the line, show it in last segment
|
|
332
|
+
segment_text = segment_chars.join
|
|
333
|
+
"#{@pastel.white(segment_text)}#{@pastel.on_white(@pastel.black(' '))}"
|
|
334
|
+
else
|
|
335
|
+
# Cursor is not in this segment, just format normally
|
|
336
|
+
@pastel.white(segment_chars.join)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
199
339
|
end
|
|
200
340
|
end
|
|
201
341
|
end
|
|
@@ -29,12 +29,12 @@ module Clacky
|
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
31
|
COLORS = {
|
|
32
|
-
user: [:
|
|
32
|
+
user: [:white, :white],
|
|
33
33
|
assistant: [:bright_green, :white],
|
|
34
34
|
tool_call: [:bright_cyan, :cyan],
|
|
35
|
-
tool_result: [:
|
|
35
|
+
tool_result: [:bright_cyan, :cyan],
|
|
36
36
|
tool_denied: [:bright_yellow, :yellow],
|
|
37
|
-
tool_planned: [:
|
|
37
|
+
tool_planned: [:bright_cyan, :cyan],
|
|
38
38
|
tool_error: [:bright_red, :red],
|
|
39
39
|
thinking: [:dim, :dim],
|
|
40
40
|
working: [:bright_yellow, :yellow],
|
|
@@ -26,12 +26,12 @@ module Clacky
|
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
28
|
COLORS = {
|
|
29
|
-
user: [:
|
|
29
|
+
user: [:white, :white],
|
|
30
30
|
assistant: [:green, :white],
|
|
31
31
|
tool_call: [:cyan, :cyan],
|
|
32
|
-
tool_result: [:
|
|
32
|
+
tool_result: [:cyan, :cyan],
|
|
33
33
|
tool_denied: [:yellow, :yellow],
|
|
34
|
-
tool_planned: [:
|
|
34
|
+
tool_planned: [:cyan, :cyan],
|
|
35
35
|
tool_error: [:red, :red],
|
|
36
36
|
thinking: [:dim, :dim],
|
|
37
37
|
working: [:bright_yellow, :yellow],
|