openclacky 0.5.6 → 0.6.1
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 +71 -0
- data/docs/ui2-architecture.md +124 -0
- data/lib/clacky/agent.rb +376 -346
- data/lib/clacky/agent_config.rb +1 -7
- data/lib/clacky/cli.rb +167 -398
- data/lib/clacky/client.rb +68 -36
- data/lib/clacky/gitignore_parser.rb +26 -12
- data/lib/clacky/model_pricing.rb +6 -2
- data/lib/clacky/session_manager.rb +6 -2
- data/lib/clacky/tools/glob.rb +66 -10
- data/lib/clacky/tools/grep.rb +6 -122
- data/lib/clacky/tools/run_project.rb +10 -5
- data/lib/clacky/tools/safe_shell.rb +149 -20
- data/lib/clacky/tools/shell.rb +3 -51
- 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 +4 -4
- data/lib/clacky/tools/web_search.rb +40 -28
- data/lib/clacky/ui2/README.md +214 -0
- data/lib/clacky/ui2/components/base_component.rb +163 -0
- data/lib/clacky/ui2/components/common_component.rb +98 -0
- data/lib/clacky/ui2/components/inline_input.rb +187 -0
- data/lib/clacky/ui2/components/input_area.rb +1124 -0
- data/lib/clacky/ui2/components/message_component.rb +80 -0
- data/lib/clacky/ui2/components/output_area.rb +112 -0
- data/lib/clacky/ui2/components/todo_area.rb +130 -0
- data/lib/clacky/ui2/components/tool_component.rb +106 -0
- data/lib/clacky/ui2/components/welcome_banner.rb +103 -0
- data/lib/clacky/ui2/layout_manager.rb +437 -0
- data/lib/clacky/ui2/line_editor.rb +201 -0
- data/lib/clacky/ui2/markdown_renderer.rb +80 -0
- data/lib/clacky/ui2/screen_buffer.rb +257 -0
- data/lib/clacky/ui2/theme_manager.rb +68 -0
- data/lib/clacky/ui2/themes/base_theme.rb +85 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +58 -0
- data/lib/clacky/ui2/themes/minimal_theme.rb +52 -0
- data/lib/clacky/ui2/ui_controller.rb +778 -0
- data/lib/clacky/ui2/view_renderer.rb +177 -0
- data/lib/clacky/ui2.rb +37 -0
- data/lib/clacky/utils/file_ignore_helper.rb +126 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +1 -6
- metadata +53 -6
- data/lib/clacky/ui/banner.rb +0 -155
- data/lib/clacky/ui/enhanced_prompt.rb +0 -786
- data/lib/clacky/ui/formatter.rb +0 -209
- data/lib/clacky/ui/statusbar.rb +0 -96
|
@@ -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
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-screen"
|
|
4
|
+
require "io/console"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
module UI2
|
|
8
|
+
# ScreenBuffer manages terminal screen state and provides low-level rendering primitives
|
|
9
|
+
class ScreenBuffer
|
|
10
|
+
attr_reader :width, :height
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@width = TTY::Screen.width
|
|
14
|
+
@height = TTY::Screen.height
|
|
15
|
+
@buffer = []
|
|
16
|
+
@last_input_time = nil
|
|
17
|
+
@rapid_input_threshold = 0.01 # 10ms threshold for detecting paste-like rapid input
|
|
18
|
+
setup_resize_handler
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Move cursor to specific position (0-indexed)
|
|
22
|
+
# @param row [Integer] Row position
|
|
23
|
+
# @param col [Integer] Column position
|
|
24
|
+
def move_cursor(row, col)
|
|
25
|
+
print "\e[#{row + 1};#{col + 1}H"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Clear entire screen
|
|
29
|
+
def clear_screen
|
|
30
|
+
print "\e[2J"
|
|
31
|
+
move_cursor(0, 0)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Clear current line
|
|
35
|
+
def clear_line
|
|
36
|
+
print "\e[2K"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Clear from cursor to end of line
|
|
40
|
+
def clear_to_eol
|
|
41
|
+
print "\e[K"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Hide cursor
|
|
45
|
+
def hide_cursor
|
|
46
|
+
print "\e[?25l"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Show cursor
|
|
50
|
+
def show_cursor
|
|
51
|
+
print "\e[?25h"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Save cursor position
|
|
55
|
+
def save_cursor
|
|
56
|
+
print "\e[s"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Restore cursor position
|
|
60
|
+
def restore_cursor
|
|
61
|
+
print "\e[u"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Enable alternative screen buffer (like vim/less)
|
|
65
|
+
def enable_alt_screen
|
|
66
|
+
print "\e[?1049h"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Disable alternative screen buffer
|
|
70
|
+
def disable_alt_screen
|
|
71
|
+
print "\e[?1049l"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Set scroll region (DECSTBM - DEC Set Top and Bottom Margins)
|
|
75
|
+
# Content written in this region will scroll, content outside will stay fixed
|
|
76
|
+
# @param top [Integer] Top row (1-indexed)
|
|
77
|
+
# @param bottom [Integer] Bottom row (1-indexed)
|
|
78
|
+
def set_scroll_region(top, bottom)
|
|
79
|
+
print "\e[#{top};#{bottom}r"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Reset scroll region to full screen
|
|
83
|
+
def reset_scroll_region
|
|
84
|
+
print "\e[r"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Scroll the scroll region up by n lines
|
|
88
|
+
# @param n [Integer] Number of lines to scroll
|
|
89
|
+
def scroll_up(n = 1)
|
|
90
|
+
print "\e[#{n}S"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Scroll the scroll region down by n lines
|
|
94
|
+
# @param n [Integer] Number of lines to scroll
|
|
95
|
+
def scroll_down(n = 1)
|
|
96
|
+
print "\e[#{n}T"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get current screen dimensions
|
|
100
|
+
def update_dimensions
|
|
101
|
+
@width = TTY::Screen.width
|
|
102
|
+
@height = TTY::Screen.height
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Enable raw mode (disable line buffering)
|
|
106
|
+
def enable_raw_mode
|
|
107
|
+
$stdin.raw!
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Disable raw mode
|
|
111
|
+
def disable_raw_mode
|
|
112
|
+
$stdin.cooked!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Read a single character without echo
|
|
116
|
+
# @param timeout [Float] Timeout in seconds (nil for blocking)
|
|
117
|
+
# @return [String, nil] Character or nil if timeout
|
|
118
|
+
def read_char(timeout: nil)
|
|
119
|
+
if timeout
|
|
120
|
+
return nil unless IO.select([$stdin], nil, nil, timeout)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
$stdin.getc
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Read a key including special keys (arrows, etc.)
|
|
127
|
+
# @param timeout [Float] Timeout in seconds
|
|
128
|
+
# @return [Symbol, String, Hash, nil] Key symbol, character, or { type: :rapid_input, text: String }
|
|
129
|
+
def read_key(timeout: nil)
|
|
130
|
+
$stdin.set_encoding('UTF-8')
|
|
131
|
+
|
|
132
|
+
current_time = Time.now.to_f
|
|
133
|
+
is_rapid_input = @last_input_time && (current_time - @last_input_time) < @rapid_input_threshold
|
|
134
|
+
@last_input_time = current_time
|
|
135
|
+
|
|
136
|
+
char = read_char(timeout: timeout)
|
|
137
|
+
return nil unless char
|
|
138
|
+
|
|
139
|
+
# Ensure character is UTF-8 encoded
|
|
140
|
+
char = char.force_encoding('UTF-8') if char.is_a?(String) && char.encoding != Encoding::UTF_8
|
|
141
|
+
|
|
142
|
+
# Handle escape sequences for special keys
|
|
143
|
+
if char == "\e"
|
|
144
|
+
# Non-blocking read for escape sequence
|
|
145
|
+
char2 = read_char(timeout: 0.01)
|
|
146
|
+
return :escape unless char2
|
|
147
|
+
|
|
148
|
+
if char2 == "["
|
|
149
|
+
char3 = read_char(timeout: 0.01)
|
|
150
|
+
case char3
|
|
151
|
+
when "A" then return :up_arrow
|
|
152
|
+
when "B" then return :down_arrow
|
|
153
|
+
when "C" then return :right_arrow
|
|
154
|
+
when "D" then return :left_arrow
|
|
155
|
+
when "H" then return :home
|
|
156
|
+
when "F" then return :end
|
|
157
|
+
when "Z" then return :shift_tab
|
|
158
|
+
when "3"
|
|
159
|
+
char4 = read_char(timeout: 0.01)
|
|
160
|
+
return :delete if char4 == "~"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Check if there are more characters available (for rapid input detection)
|
|
166
|
+
has_more_input = IO.select([$stdin], nil, nil, 0)
|
|
167
|
+
|
|
168
|
+
# If this is rapid input or there are more characters available
|
|
169
|
+
if is_rapid_input || has_more_input
|
|
170
|
+
buffer = char.to_s.dup
|
|
171
|
+
buffer.force_encoding('UTF-8')
|
|
172
|
+
|
|
173
|
+
# Keep reading available characters
|
|
174
|
+
loop_count = 0
|
|
175
|
+
empty_checks = 0
|
|
176
|
+
|
|
177
|
+
loop do
|
|
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
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# If we buffered multiple characters or newlines, treat as rapid input (paste)
|
|
204
|
+
if buffer.length > 1 || buffer.include?("\n") || buffer.include?("\r")
|
|
205
|
+
# Remove any trailing \r or \n from rapid input buffer
|
|
206
|
+
cleaned_buffer = buffer.gsub(/[\r\n]+\z/, '')
|
|
207
|
+
return { type: :rapid_input, text: cleaned_buffer } if cleaned_buffer.length > 0
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Single character, continue to normal handling
|
|
211
|
+
char = buffer[0] if buffer.length == 1
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Handle control characters
|
|
215
|
+
case char
|
|
216
|
+
when "\r" then :enter
|
|
217
|
+
when "\n" then :newline # Shift+Enter sends \n
|
|
218
|
+
when "\u007F", "\b" then :backspace
|
|
219
|
+
when "\u0001" then :ctrl_a
|
|
220
|
+
when "\u0002" then :ctrl_b
|
|
221
|
+
when "\u0003" then :ctrl_c
|
|
222
|
+
when "\u0004" then :ctrl_d
|
|
223
|
+
when "\u0005" then :ctrl_e
|
|
224
|
+
when "\u0006" then :ctrl_f
|
|
225
|
+
when "\u000B" then :ctrl_k
|
|
226
|
+
when "\u000C" then :ctrl_l
|
|
227
|
+
when "\u0012" then :ctrl_r
|
|
228
|
+
when "\u0015" then :ctrl_u
|
|
229
|
+
when "\u0016" then :ctrl_v
|
|
230
|
+
when "\u0017" then :ctrl_w
|
|
231
|
+
else char
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Flush output
|
|
236
|
+
def flush
|
|
237
|
+
$stdout.flush
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
# Setup handler for terminal resize (SIGWINCH)
|
|
243
|
+
def setup_resize_handler
|
|
244
|
+
Signal.trap("WINCH") do
|
|
245
|
+
update_dimensions
|
|
246
|
+
@resize_callback&.call(@width, @height)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Register callback for resize events
|
|
251
|
+
# @param block [Proc] Callback to execute on resize
|
|
252
|
+
def on_resize(&block)
|
|
253
|
+
@resize_callback = block
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "themes/base_theme"
|
|
4
|
+
require_relative "themes/hacker_theme"
|
|
5
|
+
require_relative "themes/minimal_theme"
|
|
6
|
+
|
|
7
|
+
module Clacky
|
|
8
|
+
module UI2
|
|
9
|
+
# ThemeManager handles theme registration and switching
|
|
10
|
+
class ThemeManager
|
|
11
|
+
class << self
|
|
12
|
+
def instance
|
|
13
|
+
@instance ||= new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Delegate methods to instance
|
|
17
|
+
def current_theme
|
|
18
|
+
instance.current_theme
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_theme(name)
|
|
22
|
+
instance.set_theme(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def available_themes
|
|
26
|
+
instance.available_themes
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def register_theme(name, theme_class)
|
|
30
|
+
instance.register_theme(name, theme_class)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
@themes = {}
|
|
36
|
+
@current_theme = nil
|
|
37
|
+
register_default_themes
|
|
38
|
+
set_theme(:hacker)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current_theme
|
|
42
|
+
@current_theme
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set_theme(name)
|
|
46
|
+
name = name.to_sym
|
|
47
|
+
raise ArgumentError, "Unknown theme: #{name}" unless @themes.key?(name)
|
|
48
|
+
|
|
49
|
+
@current_theme = @themes[name].new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def available_themes
|
|
53
|
+
@themes.keys
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def register_theme(name, theme_class)
|
|
57
|
+
@themes[name.to_sym] = theme_class
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def register_default_themes
|
|
63
|
+
register_theme(:hacker, Themes::HackerTheme)
|
|
64
|
+
register_theme(:minimal, Themes::MinimalTheme)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Themes
|
|
8
|
+
# BaseTheme defines the abstract interface for all themes
|
|
9
|
+
# Subclasses MUST define SYMBOLS and COLORS constants
|
|
10
|
+
class BaseTheme
|
|
11
|
+
def initialize
|
|
12
|
+
@pastel = Pastel.new
|
|
13
|
+
validate_theme_definition!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get all symbols defined by this theme
|
|
17
|
+
# @return [Hash] Symbol definitions
|
|
18
|
+
def symbols
|
|
19
|
+
self.class::SYMBOLS
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get all colors defined by this theme
|
|
23
|
+
# @return [Hash] Color definitions
|
|
24
|
+
def colors
|
|
25
|
+
self.class::COLORS
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get symbol for a specific key
|
|
29
|
+
# @param key [Symbol] Symbol key
|
|
30
|
+
# @return [String] Symbol string
|
|
31
|
+
def symbol(key)
|
|
32
|
+
symbols[key] || "[??]"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get symbol color for a specific key
|
|
36
|
+
# @param key [Symbol] Color key
|
|
37
|
+
# @return [Symbol] Pastel color method name
|
|
38
|
+
def symbol_color(key)
|
|
39
|
+
colors.dig(key, 0) || :white
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get text color for a specific key
|
|
43
|
+
# @param key [Symbol] Color key
|
|
44
|
+
# @return [Symbol] Pastel color method name
|
|
45
|
+
def text_color(key)
|
|
46
|
+
colors.dig(key, 1) || :white
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Format symbol with its color
|
|
50
|
+
# @param key [Symbol] Symbol key (e.g., :user, :assistant)
|
|
51
|
+
# @return [String] Colored symbol
|
|
52
|
+
def format_symbol(key)
|
|
53
|
+
@pastel.public_send(symbol_color(key), symbol(key))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Format text with color for given key
|
|
57
|
+
# @param text [String] Text to format
|
|
58
|
+
# @param key [Symbol] Color key (e.g., :user, :assistant)
|
|
59
|
+
# @return [String] Colored text
|
|
60
|
+
def format_text(text, key)
|
|
61
|
+
@pastel.public_send(text_color(key), text)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Theme name for display (subclasses should override)
|
|
65
|
+
# @return [String] Theme name
|
|
66
|
+
def name
|
|
67
|
+
raise NotImplementedError, "Subclass must implement #name method"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Validate that subclass has defined required constants
|
|
73
|
+
def validate_theme_definition!
|
|
74
|
+
unless self.class.const_defined?(:SYMBOLS)
|
|
75
|
+
raise NotImplementedError, "Theme #{self.class.name} must define SYMBOLS constant"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless self.class.const_defined?(:COLORS)
|
|
79
|
+
raise NotImplementedError, "Theme #{self.class.name} must define COLORS constant"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_theme"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Themes
|
|
8
|
+
# HackerTheme - Matrix/hacker-style with bracket symbols
|
|
9
|
+
class HackerTheme < BaseTheme
|
|
10
|
+
SYMBOLS = {
|
|
11
|
+
user: "[>>]",
|
|
12
|
+
assistant: "[<<]",
|
|
13
|
+
tool_call: "[=>]",
|
|
14
|
+
tool_result: "[<=]",
|
|
15
|
+
tool_denied: "[!!]",
|
|
16
|
+
tool_planned: "[??]",
|
|
17
|
+
tool_error: "[XX]",
|
|
18
|
+
thinking: "[..]",
|
|
19
|
+
working: "[..]",
|
|
20
|
+
success: "[OK]",
|
|
21
|
+
error: "[ER]",
|
|
22
|
+
warning: "[!!]",
|
|
23
|
+
info: "[--]",
|
|
24
|
+
task: "[##]",
|
|
25
|
+
progress: "[>>]",
|
|
26
|
+
file: "[F]",
|
|
27
|
+
command: "[C]",
|
|
28
|
+
cached: "[*]"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
COLORS = {
|
|
32
|
+
user: [:bright_blue, :blue],
|
|
33
|
+
assistant: [:bright_green, :white],
|
|
34
|
+
tool_call: [:bright_cyan, :cyan],
|
|
35
|
+
tool_result: [:cyan, :white],
|
|
36
|
+
tool_denied: [:bright_yellow, :yellow],
|
|
37
|
+
tool_planned: [:bright_blue, :blue],
|
|
38
|
+
tool_error: [:bright_red, :red],
|
|
39
|
+
thinking: [:dim, :dim],
|
|
40
|
+
working: [:bright_yellow, :yellow],
|
|
41
|
+
success: [:bright_green, :green],
|
|
42
|
+
error: [:bright_red, :red],
|
|
43
|
+
warning: [:bright_yellow, :yellow],
|
|
44
|
+
info: [:bright_white, :white],
|
|
45
|
+
task: [:bright_yellow, :white],
|
|
46
|
+
progress: [:bright_cyan, :cyan],
|
|
47
|
+
file: [:cyan, :white],
|
|
48
|
+
command: [:cyan, :white],
|
|
49
|
+
cached: [:cyan, :cyan]
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
def name
|
|
53
|
+
"hacker"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_theme"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Themes
|
|
8
|
+
# MinimalTheme - Clean, simple symbols
|
|
9
|
+
class MinimalTheme < BaseTheme
|
|
10
|
+
SYMBOLS = {
|
|
11
|
+
user: ">",
|
|
12
|
+
assistant: "<",
|
|
13
|
+
tool_call: "*",
|
|
14
|
+
tool_result: "-",
|
|
15
|
+
tool_denied: "!",
|
|
16
|
+
tool_planned: "?",
|
|
17
|
+
tool_error: "x",
|
|
18
|
+
thinking: ".",
|
|
19
|
+
working: ".",
|
|
20
|
+
success: "+",
|
|
21
|
+
error: "x",
|
|
22
|
+
warning: "!",
|
|
23
|
+
info: "-",
|
|
24
|
+
task: "#",
|
|
25
|
+
progress: ">"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
COLORS = {
|
|
29
|
+
user: [:blue, :blue],
|
|
30
|
+
assistant: [:green, :white],
|
|
31
|
+
tool_call: [:cyan, :cyan],
|
|
32
|
+
tool_result: [:white, :white],
|
|
33
|
+
tool_denied: [:yellow, :yellow],
|
|
34
|
+
tool_planned: [:blue, :blue],
|
|
35
|
+
tool_error: [:red, :red],
|
|
36
|
+
thinking: [:dim, :dim],
|
|
37
|
+
working: [:bright_yellow, :yellow],
|
|
38
|
+
success: [:green, :green],
|
|
39
|
+
error: [:red, :red],
|
|
40
|
+
warning: [:yellow, :yellow],
|
|
41
|
+
info: [:white, :white],
|
|
42
|
+
task: [:yellow, :white],
|
|
43
|
+
progress: [:cyan, :cyan]
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
def name
|
|
47
|
+
"minimal"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|