openclacky 0.6.0 → 0.6.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/CHANGELOG.md +54 -0
- data/README.md +39 -88
- data/homebrew/README.md +96 -0
- data/homebrew/openclacky.rb +24 -0
- data/lib/clacky/agent.rb +139 -67
- data/lib/clacky/cli.rb +105 -6
- data/lib/clacky/tools/file_reader.rb +135 -2
- data/lib/clacky/tools/glob.rb +2 -2
- data/lib/clacky/tools/grep.rb +2 -2
- data/lib/clacky/tools/run_project.rb +5 -5
- data/lib/clacky/tools/safe_shell.rb +140 -17
- data/lib/clacky/tools/shell.rb +69 -2
- data/lib/clacky/tools/todo_manager.rb +50 -3
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/web_fetch.rb +2 -2
- data/lib/clacky/tools/web_search.rb +2 -2
- data/lib/clacky/ui2/components/common_component.rb +14 -5
- data/lib/clacky/ui2/components/input_area.rb +300 -89
- data/lib/clacky/ui2/components/message_component.rb +7 -3
- data/lib/clacky/ui2/components/todo_area.rb +38 -45
- data/lib/clacky/ui2/components/welcome_banner.rb +10 -0
- data/lib/clacky/ui2/layout_manager.rb +180 -50
- data/lib/clacky/ui2/markdown_renderer.rb +80 -0
- data/lib/clacky/ui2/screen_buffer.rb +26 -7
- data/lib/clacky/ui2/themes/base_theme.rb +32 -46
- data/lib/clacky/ui2/themes/hacker_theme.rb +4 -2
- data/lib/clacky/ui2/themes/minimal_theme.rb +4 -2
- data/lib/clacky/ui2/ui_controller.rb +150 -32
- data/lib/clacky/ui2/view_renderer.rb +21 -4
- data/lib/clacky/ui2.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +7 -2
- data/lib/clacky/utils/file_processor.rb +201 -0
- data/lib/clacky/version.rb +1 -1
- data/scripts/install.sh +249 -0
- data/scripts/uninstall.sh +146 -0
- metadata +21 -2
- data/lib/clacky/ui2/components/output_area.rb +0 -112
|
@@ -10,7 +10,7 @@ module Clacky
|
|
|
10
10
|
attr_accessor :height
|
|
11
11
|
attr_reader :todos
|
|
12
12
|
|
|
13
|
-
MAX_DISPLAY_TASKS =
|
|
13
|
+
MAX_DISPLAY_TASKS = 3 # Show current + next 2 tasks
|
|
14
14
|
|
|
15
15
|
def initialize
|
|
16
16
|
@todos = []
|
|
@@ -27,12 +27,11 @@ module Clacky
|
|
|
27
27
|
@completed_count = @todos.count { |t| t[:status] == "completed" }
|
|
28
28
|
@total_count = @todos.size
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
if @pending_todos.empty?
|
|
30
|
+
# Calculate height: 0 if no pending, otherwise 1 line per task (up to MAX_DISPLAY_TASKS)
|
|
31
|
+
if @pending_todos.empty?
|
|
32
32
|
@height = 0
|
|
33
33
|
else
|
|
34
|
-
|
|
35
|
-
@height = 1 + display_count
|
|
34
|
+
@height = [@pending_todos.size, MAX_DISPLAY_TASKS].min
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
@@ -48,21 +47,37 @@ module Clacky
|
|
|
48
47
|
|
|
49
48
|
update_width
|
|
50
49
|
|
|
51
|
-
# Render
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
# Render each task on separate line
|
|
51
|
+
tasks_to_show = @pending_todos.take(MAX_DISPLAY_TASKS)
|
|
52
|
+
|
|
53
|
+
tasks_to_show.each_with_index do |task, index|
|
|
54
|
+
move_cursor(start_row + index, 0)
|
|
55
|
+
|
|
56
|
+
# Build the line content
|
|
57
|
+
line_content = if index == 0
|
|
58
|
+
# First line: Task [2/4]: #3 - Current task description
|
|
59
|
+
progress = "#{@completed_count}/#{@total_count}"
|
|
60
|
+
prefix = "Task [#{progress}]: "
|
|
61
|
+
task_text = "##{task[:id]} - #{task[:task]}"
|
|
62
|
+
available_width = @width - prefix.length - 2
|
|
63
|
+
truncated_task = truncate_text(task_text, available_width)
|
|
64
|
+
|
|
65
|
+
"#{@pastel.cyan(prefix)}#{truncated_task}"
|
|
66
|
+
else
|
|
67
|
+
# Subsequent lines: -> Next: #4 - Next task description
|
|
68
|
+
label = index == 1 ? "Next" : "After"
|
|
69
|
+
prefix = "-> #{label}: "
|
|
70
|
+
task_text = "##{task[:id]} - #{task[:task]}"
|
|
71
|
+
available_width = @width - prefix.length - 2
|
|
72
|
+
truncated_task = truncate_text(task_text, available_width)
|
|
73
|
+
|
|
74
|
+
"#{@pastel.dim(prefix)}#{@pastel.dim(truncated_task)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Use carriage return and print content directly (overwrite existing content)
|
|
78
|
+
print "\r#{line_content}"
|
|
79
|
+
# Clear any remaining characters from previous render if line is shorter
|
|
80
|
+
clear_to_end_of_line
|
|
66
81
|
end
|
|
67
82
|
|
|
68
83
|
flush
|
|
@@ -79,28 +94,6 @@ module Clacky
|
|
|
79
94
|
|
|
80
95
|
private
|
|
81
96
|
|
|
82
|
-
# Render header line with progress bar
|
|
83
|
-
def render_header
|
|
84
|
-
progress = "#{@completed_count}/#{@total_count}"
|
|
85
|
-
progress_bar = render_progress_bar(@completed_count, @total_count)
|
|
86
|
-
|
|
87
|
-
"#{@pastel.cyan("[##]")} Tasks [#{progress}]: #{progress_bar}"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Render a simple progress bar
|
|
91
|
-
def render_progress_bar(completed, total)
|
|
92
|
-
return "" if total == 0
|
|
93
|
-
|
|
94
|
-
bar_width = 10
|
|
95
|
-
filled = total > 0 ? (completed.to_f / total * bar_width).round : 0
|
|
96
|
-
empty = bar_width - filled
|
|
97
|
-
|
|
98
|
-
filled_bar = @pastel.green("█" * filled)
|
|
99
|
-
empty_bar = @pastel.dim("░" * empty)
|
|
100
|
-
|
|
101
|
-
"#{filled_bar}#{empty_bar}"
|
|
102
|
-
end
|
|
103
|
-
|
|
104
97
|
# Truncate text to fit width
|
|
105
98
|
def truncate_text(text, max_width)
|
|
106
99
|
return "" if text.nil?
|
|
@@ -122,9 +115,9 @@ module Clacky
|
|
|
122
115
|
print "\e[#{row + 1};#{col + 1}H"
|
|
123
116
|
end
|
|
124
117
|
|
|
125
|
-
# Clear
|
|
126
|
-
def
|
|
127
|
-
print "\e[
|
|
118
|
+
# Clear from cursor to end of line
|
|
119
|
+
def clear_to_end_of_line
|
|
120
|
+
print "\e[0K"
|
|
128
121
|
end
|
|
129
122
|
|
|
130
123
|
# Flush output
|
|
@@ -29,6 +29,16 @@ module Clacky
|
|
|
29
29
|
@pastel = Pastel.new
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Render only the logo (ASCII art)
|
|
33
|
+
# @return [String] Formatted logo only
|
|
34
|
+
def render_logo
|
|
35
|
+
lines = []
|
|
36
|
+
lines << ""
|
|
37
|
+
lines << @pastel.bright_green(LOGO)
|
|
38
|
+
lines << ""
|
|
39
|
+
lines.join("\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
# Render startup banner
|
|
33
43
|
# @return [String] Formatted startup banner
|
|
34
44
|
def render_startup
|
|
@@ -6,11 +6,10 @@ 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
|
|
@@ -35,7 +34,6 @@ module Clacky
|
|
|
35
34
|
@input_row = @todo_row + todo_height
|
|
36
35
|
|
|
37
36
|
# Update component dimensions
|
|
38
|
-
@output_area.height = @output_height
|
|
39
37
|
@input_area.row = @input_row
|
|
40
38
|
end
|
|
41
39
|
|
|
@@ -93,10 +91,13 @@ module Clacky
|
|
|
93
91
|
def position_inline_input_cursor(inline_input)
|
|
94
92
|
return unless inline_input
|
|
95
93
|
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
# Calculate the actual terminal cursor position considering multi-byte characters
|
|
95
|
+
# InlineInput is on the last output line (@output_row - 1)
|
|
96
|
+
cursor_row = @output_row - 1
|
|
97
|
+
cursor_col = inline_input.cursor_col # This already considers display width
|
|
98
|
+
|
|
99
|
+
# Move terminal cursor to the correct position
|
|
100
|
+
screen.move_cursor(cursor_row, cursor_col)
|
|
100
101
|
screen.flush
|
|
101
102
|
end
|
|
102
103
|
|
|
@@ -159,14 +160,16 @@ module Clacky
|
|
|
159
160
|
# Clear output area (for /clear command)
|
|
160
161
|
def clear_output
|
|
161
162
|
@render_mutex.synchronize do
|
|
162
|
-
# Clear all lines in output area (from 0 to
|
|
163
|
-
|
|
164
|
-
(0...
|
|
163
|
+
# Clear all lines in output area (from 0 to where fixed area starts)
|
|
164
|
+
max_row = fixed_area_start_row
|
|
165
|
+
(0...max_row).each do |row|
|
|
165
166
|
screen.move_cursor(row, 0)
|
|
166
167
|
screen.clear_line
|
|
167
168
|
end
|
|
168
|
-
|
|
169
|
+
|
|
170
|
+
# Reset output position to beginning
|
|
169
171
|
@output_row = 0
|
|
172
|
+
|
|
170
173
|
# Re-render fixed areas to ensure they stay in place
|
|
171
174
|
render_fixed_areas
|
|
172
175
|
screen.flush
|
|
@@ -174,32 +177,24 @@ module Clacky
|
|
|
174
177
|
end
|
|
175
178
|
|
|
176
179
|
# Append content to output area
|
|
177
|
-
#
|
|
178
|
-
# @param content [String] Content to append
|
|
180
|
+
# This is the main output method - handles scrolling and fixed area preservation
|
|
181
|
+
# @param content [String] Content to append (can be multi-line)
|
|
179
182
|
def append_output(content)
|
|
180
|
-
return if content.nil?
|
|
183
|
+
return if content.nil?
|
|
181
184
|
|
|
182
185
|
@render_mutex.synchronize do
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Stay at max_output_row for next output
|
|
192
|
-
@output_row = max_output_row
|
|
186
|
+
lines = content.split("\n", -1) # -1 to keep trailing empty strings
|
|
187
|
+
|
|
188
|
+
lines.each_with_index do |line, index|
|
|
189
|
+
# Wrap long lines to prevent display issues
|
|
190
|
+
wrapped_lines = wrap_long_line(line)
|
|
191
|
+
|
|
192
|
+
wrapped_lines.each do |wrapped_line|
|
|
193
|
+
write_output_line(wrapped_line)
|
|
193
194
|
end
|
|
194
|
-
|
|
195
|
-
# Output line at current position
|
|
196
|
-
screen.move_cursor(@output_row, 0)
|
|
197
|
-
screen.clear_line
|
|
198
|
-
output_area.append(line)
|
|
199
|
-
@output_row += 1
|
|
200
195
|
end
|
|
201
196
|
|
|
202
|
-
# Re-render fixed areas at
|
|
197
|
+
# Re-render fixed areas to ensure they stay at bottom
|
|
203
198
|
render_fixed_areas
|
|
204
199
|
screen.flush
|
|
205
200
|
end
|
|
@@ -209,40 +204,52 @@ module Clacky
|
|
|
209
204
|
# @param content [String] Content to update
|
|
210
205
|
def update_last_line(content)
|
|
211
206
|
@render_mutex.synchronize do
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
return if @output_row == 0 # No output yet
|
|
208
|
+
|
|
209
|
+
# Last written line is at @output_row - 1
|
|
210
|
+
last_row = @output_row - 1
|
|
214
211
|
screen.move_cursor(last_row, 0)
|
|
215
212
|
screen.clear_line
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
print content
|
|
214
|
+
|
|
215
|
+
# Hide terminal cursor to avoid showing two cursors
|
|
216
|
+
# InlineInput uses visual cursor (white background) which is better for multi-byte chars
|
|
217
|
+
screen.hide_cursor
|
|
218
218
|
screen.flush
|
|
219
|
+
|
|
220
|
+
# Don't re-render fixed areas - we're just updating existing content
|
|
219
221
|
end
|
|
220
222
|
end
|
|
221
223
|
|
|
222
224
|
# Remove the last line from output area
|
|
223
225
|
def remove_last_line
|
|
224
226
|
@render_mutex.synchronize do
|
|
225
|
-
|
|
227
|
+
return if @output_row == 0 # No output to remove
|
|
228
|
+
|
|
229
|
+
# Clear the last written line
|
|
230
|
+
last_row = @output_row - 1
|
|
226
231
|
screen.move_cursor(last_row, 0)
|
|
227
232
|
screen.clear_line
|
|
228
|
-
|
|
233
|
+
|
|
234
|
+
# Move output row back
|
|
235
|
+
@output_row = last_row
|
|
236
|
+
|
|
237
|
+
# Re-render fixed areas to ensure consistency
|
|
229
238
|
render_fixed_areas
|
|
230
239
|
screen.flush
|
|
231
240
|
end
|
|
232
241
|
end
|
|
233
242
|
|
|
234
|
-
# Scroll output area up
|
|
243
|
+
# Scroll output area up (legacy no-op)
|
|
235
244
|
# @param lines [Integer] Number of lines to scroll
|
|
236
245
|
def scroll_output_up(lines = 1)
|
|
237
|
-
|
|
238
|
-
render_output
|
|
246
|
+
# No-op - terminal handles scrolling natively
|
|
239
247
|
end
|
|
240
248
|
|
|
241
|
-
# Scroll output area down
|
|
249
|
+
# Scroll output area down (legacy no-op)
|
|
242
250
|
# @param lines [Integer] Number of lines to scroll
|
|
243
251
|
def scroll_output_down(lines = 1)
|
|
244
|
-
|
|
245
|
-
render_output
|
|
252
|
+
# No-op - terminal handles scrolling natively
|
|
246
253
|
end
|
|
247
254
|
|
|
248
255
|
# Handle window resize
|
|
@@ -252,12 +259,14 @@ module Clacky
|
|
|
252
259
|
screen.update_dimensions
|
|
253
260
|
calculate_layout
|
|
254
261
|
|
|
255
|
-
# Adjust output_row if it exceeds new
|
|
256
|
-
|
|
257
|
-
|
|
262
|
+
# Adjust @output_row if it exceeds new layout
|
|
263
|
+
# After resize, @output_row should not exceed fixed_area_start_row
|
|
264
|
+
max_allowed = fixed_area_start_row
|
|
265
|
+
@output_row = [@output_row, max_allowed].min
|
|
258
266
|
|
|
259
|
-
# Clear old fixed area lines
|
|
260
|
-
|
|
267
|
+
# Clear old fixed area and some lines above (terminal may have wrapped content)
|
|
268
|
+
clear_start = [old_gap_row - 5, 0].max
|
|
269
|
+
(clear_start...screen.height).each do |row|
|
|
261
270
|
screen.move_cursor(row, 0)
|
|
262
271
|
screen.clear_line
|
|
263
272
|
end
|
|
@@ -268,6 +277,127 @@ module Clacky
|
|
|
268
277
|
|
|
269
278
|
private
|
|
270
279
|
|
|
280
|
+
# Write a single line to output area
|
|
281
|
+
# Handles scrolling when reaching fixed area
|
|
282
|
+
# @param line [String] Single line to write (should not contain newlines)
|
|
283
|
+
def write_output_line(line)
|
|
284
|
+
# Calculate where fixed area starts (this is where output area ends)
|
|
285
|
+
max_output_row = fixed_area_start_row
|
|
286
|
+
|
|
287
|
+
# If we're about to write into the fixed area, scroll first
|
|
288
|
+
if @output_row >= max_output_row
|
|
289
|
+
# Trigger terminal scroll by printing newline at bottom
|
|
290
|
+
screen.move_cursor(screen.height - 1, 0)
|
|
291
|
+
print "\n"
|
|
292
|
+
|
|
293
|
+
# After scroll, position to write at the last row of output area
|
|
294
|
+
@output_row = max_output_row - 1
|
|
295
|
+
|
|
296
|
+
# Important: Re-render fixed areas after scroll to prevent corruption
|
|
297
|
+
render_fixed_areas
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Now write the line at current position
|
|
301
|
+
screen.move_cursor(@output_row, 0)
|
|
302
|
+
screen.clear_line
|
|
303
|
+
print line
|
|
304
|
+
|
|
305
|
+
# Move to next row for next write
|
|
306
|
+
@output_row += 1
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Wrap a long line into multiple lines based on terminal width
|
|
310
|
+
# Considers display width of multi-byte characters (e.g., Chinese characters)
|
|
311
|
+
# @param line [String] Line to wrap
|
|
312
|
+
# @return [Array<String>] Array of wrapped lines
|
|
313
|
+
def wrap_long_line(line)
|
|
314
|
+
return [""] if line.nil? || line.empty?
|
|
315
|
+
|
|
316
|
+
max_width = screen.width
|
|
317
|
+
return [line] if max_width <= 0
|
|
318
|
+
|
|
319
|
+
# Strip ANSI codes for width calculation
|
|
320
|
+
visible_line = line.gsub(/\e\[[0-9;]*m/, '')
|
|
321
|
+
|
|
322
|
+
# Check if line needs wrapping
|
|
323
|
+
display_width = calculate_display_width(visible_line)
|
|
324
|
+
return [line] if display_width <= max_width
|
|
325
|
+
|
|
326
|
+
# Line needs wrapping - split by considering display width
|
|
327
|
+
wrapped = []
|
|
328
|
+
current_line = ""
|
|
329
|
+
current_width = 0
|
|
330
|
+
ansi_codes = [] # Track ANSI codes to carry over
|
|
331
|
+
|
|
332
|
+
# Extract ANSI codes and text segments
|
|
333
|
+
segments = line.split(/(\e\[[0-9;]*m)/)
|
|
334
|
+
|
|
335
|
+
segments.each do |segment|
|
|
336
|
+
if segment =~ /^\e\[[0-9;]*m$/
|
|
337
|
+
# ANSI code - add to current codes
|
|
338
|
+
ansi_codes << segment
|
|
339
|
+
current_line += segment
|
|
340
|
+
else
|
|
341
|
+
# Text segment - process character by character
|
|
342
|
+
segment.each_char do |char|
|
|
343
|
+
char_width = char_display_width(char)
|
|
344
|
+
|
|
345
|
+
if current_width + char_width > max_width && !current_line.empty?
|
|
346
|
+
# Complete current line
|
|
347
|
+
wrapped << current_line
|
|
348
|
+
# Start new line with carried-over ANSI codes
|
|
349
|
+
current_line = ansi_codes.join
|
|
350
|
+
current_width = 0
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
current_line += char
|
|
354
|
+
current_width += char_width
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Add remaining content
|
|
360
|
+
wrapped << current_line unless current_line.empty? || current_line == ansi_codes.join
|
|
361
|
+
|
|
362
|
+
wrapped.empty? ? [""] : wrapped
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Calculate display width of a single character
|
|
366
|
+
# @param char [String] Single character
|
|
367
|
+
# @return [Integer] Display width (1 or 2)
|
|
368
|
+
def char_display_width(char)
|
|
369
|
+
code = char.ord
|
|
370
|
+
# East Asian Wide and Fullwidth characters take 2 columns
|
|
371
|
+
if (code >= 0x1100 && code <= 0x115F) ||
|
|
372
|
+
(code >= 0x2329 && code <= 0x232A) ||
|
|
373
|
+
(code >= 0x2E80 && code <= 0x303E) ||
|
|
374
|
+
(code >= 0x3040 && code <= 0xA4CF) ||
|
|
375
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
376
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
377
|
+
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
378
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
379
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
380
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
381
|
+
(code >= 0x1F300 && code <= 0x1F9FF) ||
|
|
382
|
+
(code >= 0x20000 && code <= 0x2FFFD) ||
|
|
383
|
+
(code >= 0x30000 && code <= 0x3FFFD)
|
|
384
|
+
2
|
|
385
|
+
else
|
|
386
|
+
1
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Calculate display width of a string (considering multi-byte characters)
|
|
391
|
+
# @param text [String] Text to calculate
|
|
392
|
+
# @return [Integer] Display width
|
|
393
|
+
def calculate_display_width(text)
|
|
394
|
+
width = 0
|
|
395
|
+
text.each_char do |char|
|
|
396
|
+
width += char_display_width(char)
|
|
397
|
+
end
|
|
398
|
+
width
|
|
399
|
+
end
|
|
400
|
+
|
|
271
401
|
# Calculate fixed area height (gap + todo + input)
|
|
272
402
|
def fixed_area_height
|
|
273
403
|
todo_height = @todo_area&.height || 0
|
|
@@ -306,7 +436,7 @@ module Clacky
|
|
|
306
436
|
|
|
307
437
|
# Internal render all (without mutex)
|
|
308
438
|
def render_all_internal
|
|
309
|
-
|
|
439
|
+
# Output flows naturally, just render fixed areas
|
|
310
440
|
render_fixed_areas
|
|
311
441
|
screen.flush
|
|
312
442
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-markdown"
|
|
4
|
+
require_relative "theme_manager"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
module UI2
|
|
8
|
+
# MarkdownRenderer handles rendering Markdown content with syntax highlighting
|
|
9
|
+
module MarkdownRenderer
|
|
10
|
+
class << self
|
|
11
|
+
# Render markdown content with theme-aware colors
|
|
12
|
+
# @param content [String] Markdown content to render
|
|
13
|
+
# @return [String] Rendered content with ANSI colors
|
|
14
|
+
def render(content)
|
|
15
|
+
return content if content.nil? || content.empty?
|
|
16
|
+
|
|
17
|
+
# Get current theme colors
|
|
18
|
+
theme = ThemeManager.current_theme
|
|
19
|
+
|
|
20
|
+
# Configure tty-markdown colors based on current theme
|
|
21
|
+
# tty-markdown uses Pastel internally, we can configure symbols
|
|
22
|
+
parsed = TTY::Markdown.parse(content,
|
|
23
|
+
colors: theme_colors,
|
|
24
|
+
width: TTY::Screen.width - 4 # Leave some margin
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
parsed
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
# Fallback to plain content if rendering fails
|
|
30
|
+
content
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if content looks like markdown
|
|
34
|
+
# @param content [String] Content to check
|
|
35
|
+
# @return [Boolean] true if content appears to be markdown
|
|
36
|
+
def markdown?(content)
|
|
37
|
+
return false if content.nil? || content.empty?
|
|
38
|
+
|
|
39
|
+
# Check for common markdown patterns
|
|
40
|
+
content.match?(/^#+ /) || # Headers
|
|
41
|
+
content.match?(/```/) || # Code blocks
|
|
42
|
+
content.match?(/^\s*[-*+] /) || # Unordered lists
|
|
43
|
+
content.match?(/^\s*\d+\. /) || # Ordered lists
|
|
44
|
+
content.match?(/\[.+\]\(.+\)/) || # Links
|
|
45
|
+
content.match?(/^\s*> /) || # Blockquotes
|
|
46
|
+
content.match?(/\*\*.+\*\*/) || # Bold
|
|
47
|
+
content.match?(/`.+`/) || # Inline code
|
|
48
|
+
content.match?(/^\s*\|.+\|/) || # Tables
|
|
49
|
+
content.match?(/^---+$/) # Horizontal rules
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Get theme-aware colors for markdown rendering
|
|
55
|
+
# @return [Hash] Color configuration for tty-markdown
|
|
56
|
+
def theme_colors
|
|
57
|
+
theme = ThemeManager.current_theme
|
|
58
|
+
|
|
59
|
+
# Map our theme colors to tty-markdown's expected format
|
|
60
|
+
{
|
|
61
|
+
# Headers use info color (cyan/blue)
|
|
62
|
+
header: theme.colors[:info],
|
|
63
|
+
# Code blocks use dim color
|
|
64
|
+
code: theme.colors[:thinking],
|
|
65
|
+
# Links use success color (green)
|
|
66
|
+
link: theme.colors[:success],
|
|
67
|
+
# Lists use default text color
|
|
68
|
+
list: :bright_white,
|
|
69
|
+
# Strong/bold use bright white
|
|
70
|
+
strong: :bright_white,
|
|
71
|
+
# Emphasis/italic use white
|
|
72
|
+
em: :white,
|
|
73
|
+
# Note/blockquote use dim color
|
|
74
|
+
note: theme.colors[:thinking],
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -171,14 +171,33 @@ module Clacky
|
|
|
171
171
|
buffer.force_encoding('UTF-8')
|
|
172
172
|
|
|
173
173
|
# Keep reading available characters
|
|
174
|
+
loop_count = 0
|
|
175
|
+
empty_checks = 0
|
|
176
|
+
|
|
174
177
|
loop do
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
# Check if there's data available immediately
|
|
179
|
+
has_data = IO.select([$stdin], nil, nil, 0)
|
|
180
|
+
|
|
181
|
+
if has_data
|
|
182
|
+
next_char = $stdin.getc
|
|
183
|
+
break unless next_char
|
|
184
|
+
|
|
185
|
+
next_char = next_char.force_encoding('UTF-8') if next_char.encoding != Encoding::UTF_8
|
|
186
|
+
buffer << next_char
|
|
187
|
+
loop_count += 1
|
|
188
|
+
empty_checks = 0 # Reset empty check counter
|
|
189
|
+
else
|
|
190
|
+
# No immediate data, but wait a bit to see if more is coming
|
|
191
|
+
# This handles the case where paste data arrives in chunks
|
|
192
|
+
empty_checks += 1
|
|
193
|
+
if empty_checks == 1
|
|
194
|
+
# First empty check - wait 10ms for more data
|
|
195
|
+
sleep 0.01
|
|
196
|
+
else
|
|
197
|
+
# Second empty check - really no more data
|
|
198
|
+
break
|
|
199
|
+
end
|
|
200
|
+
end
|
|
182
201
|
end
|
|
183
202
|
|
|
184
203
|
# If we buffered multiple characters or newlines, treat as rapid input (paste)
|