tng 0.1.3 → 0.1.5

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.
@@ -1,6 +1,7 @@
1
1
  require "tty-box"
2
2
  require "pastel"
3
3
  require "tty-screen"
4
+ require_relative "theme"
4
5
 
5
6
  class ShowHelp
6
7
  attr_reader :pastel
@@ -17,73 +18,107 @@ class ShowHelp
17
18
 
18
19
  def content
19
20
  [
20
- @pastel.cyan.bold("TNG - LLM-Powered Rails Test Generator"),
21
- @pastel.dim("Version: #{@version}"),
21
+ @pastel.public_send(Tng::UI::Theme.color(:secondary)).bold("TNG - LLM-Powered Rails Test Generator"),
22
+ @pastel.public_send(Tng::UI::Theme.color(:muted), "Version: #{@version}"),
22
23
  "",
23
- @pastel.yellow.bold("Usage:"),
24
- @pastel.bright_white(" bundle exec tng") + @pastel.dim(" # Interactive mode"),
25
- @pastel.bright_white(" bundle exec tng --type=TYPE --file=FILE") + @pastel.dim(" # Direct mode"),
26
- @pastel.bright_white(" bundle exec tng -t TYPE -f FILE") + @pastel.dim(" # Short aliases"),
27
- @pastel.bright_white(" bundle exec tng -t=TYPE f=FILE") + @pastel.dim(" # Mixed format"),
24
+ @pastel.public_send(Tng::UI::Theme.color(:accent)).bold("Usage:"),
25
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
26
+ " bundle exec tng") + @pastel.public_send(Tng::UI::Theme.color(:muted),
27
+ " # Interactive mode"),
28
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
29
+ " bundle exec tng --type=TYPE --file=FILE") + @pastel.public_send(
30
+ Tng::UI::Theme.color(:muted), " # Direct mode"
31
+ ),
32
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
33
+ " bundle exec tng -t TYPE -f FILE") + @pastel.public_send(Tng::UI::Theme.color(:muted),
34
+ " # Short aliases"),
35
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
36
+ " bundle exec tng -t=TYPE f=FILE") + @pastel.public_send(Tng::UI::Theme.color(:muted),
37
+ " # Mixed format"),
28
38
  "",
29
- @pastel.yellow.bold("Examples:"),
30
- @pastel.green(" bundle exec tng -t c -f ping") + @pastel.dim(" # Controller test"),
31
- @pastel.green(" bundle exec tng -t=c f=ping") + @pastel.dim(" # Mixed format"),
32
- @pastel.green(" bundle exec tng -t m -f user") + @pastel.dim(" # Model test"),
33
- @pastel.green(" bundle exec tng --type=controller --file=users_controller"),
34
- @pastel.green(" bundle exec tng -t c -f \"admin/users_controller\"") + @pastel.dim(" # Namespaced"),
39
+ @pastel.public_send(Tng::UI::Theme.color(:accent)).bold("Examples:"),
40
+ @pastel.public_send(Tng::UI::Theme.color(:success),
41
+ " bundle exec tng -t c -f ping") + @pastel.public_send(Tng::UI::Theme.color(:muted),
42
+ " # Controller test"),
43
+ @pastel.public_send(Tng::UI::Theme.color(:success),
44
+ " bundle exec tng -t=c f=ping") + @pastel.public_send(Tng::UI::Theme.color(:muted),
45
+ " # Mixed format"),
46
+ @pastel.public_send(Tng::UI::Theme.color(:success),
47
+ " bundle exec tng -t m -f user") + @pastel.public_send(Tng::UI::Theme.color(:muted),
48
+ " # Model test"),
49
+ @pastel.public_send(Tng::UI::Theme.color(:success),
50
+ " bundle exec tng --type=controller --file=users_controller"),
51
+ @pastel.public_send(Tng::UI::Theme.color(:success),
52
+ " bundle exec tng -t c -f \"admin/users_controller\"") + @pastel.public_send(Tng::UI::Theme.color(:muted),
53
+ " # Namespaced"),
35
54
  "",
36
- @pastel.yellow.bold("Options:"),
37
- @pastel.bright_white(" -t, --type=TYPE") + @pastel.dim(" Test type (controller, model)"),
38
- @pastel.bright_white(" -f, --file=FILE") + @pastel.dim(" File name (without .rb extension)"),
39
- @pastel.bright_white(" -m, --method=METHOD") + @pastel.dim(" Method name (for per-method tests)"),
40
- @pastel.bright_white(" -h, --help") + @pastel.dim(" Show this help message"),
55
+ @pastel.public_send(Tng::UI::Theme.color(:accent)).bold("Options:"),
56
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
57
+ " -t, --type=TYPE") + @pastel.public_send(Tng::UI::Theme.color(:muted),
58
+ " Test type (controller, model)"),
59
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
60
+ " -f, --file=FILE") + @pastel.public_send(Tng::UI::Theme.color(:muted),
61
+ " File name (without .rb extension)"),
62
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
63
+ " -m, --method=METHOD") + @pastel.public_send(Tng::UI::Theme.color(:muted),
64
+ " Method name (for per-method tests)"),
65
+ @pastel.public_send(Tng::UI::Theme.color(:primary),
66
+ " -h, --help") + @pastel.public_send(Tng::UI::Theme.color(:muted),
67
+ " Show this help message"),
41
68
  "",
42
- @pastel.yellow.bold("Type Aliases:"),
43
- @pastel.dim(" • Controllers: ") + @pastel.cyan("c, controller"),
44
- @pastel.dim(" • Models: ") + @pastel.cyan("m, mo, model"),
69
+ @pastel.public_send(Tng::UI::Theme.color(:accent)).bold("Type Aliases:"),
70
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
71
+ " #{Tng::UI::Theme.icon(:bullet)} Controllers: ") + @pastel.public_send(Tng::UI::Theme.color(:secondary),
72
+ "c, controller"),
73
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
74
+ " #{Tng::UI::Theme.icon(:bullet)} Models: ") + @pastel.public_send(
75
+ Tng::UI::Theme.color(:secondary), "m, mo, model"
76
+ ),
45
77
  "",
46
- @pastel.yellow.bold("File Format:"),
47
- @pastel.dim(" • Controllers: ") + @pastel.green("users_controller, api/v1/posts_controller"),
48
- @pastel.dim(" • Models: ") + @pastel.green("user, blog/post"),
49
- @pastel.dim(" Don't include .rb extension"),
78
+ @pastel.public_send(Tng::UI::Theme.color(:accent)).bold("File Format:"),
79
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
80
+ " #{Tng::UI::Theme.icon(:bullet)} Controllers: ") + @pastel.public_send(Tng::UI::Theme.color(:success),
81
+ "users_controller, api/v1/posts_controller"),
82
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
83
+ " #{Tng::UI::Theme.icon(:bullet)} Models: ") + @pastel.public_send(
84
+ Tng::UI::Theme.color(:success), "user, blog/post"
85
+ ),
86
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
87
+ " #{Tng::UI::Theme.icon(:bullet)} Don't include .rb extension"),
50
88
  "",
51
- @pastel.magenta.bold("Pro Tips:"),
52
- @pastel.dim(" • Mix formats: ") + @pastel.cyan("-t=c f=ping") + @pastel.dim(" works perfectly"),
53
- @pastel.dim(" • Use filtering in interactive mode for large projects"),
54
- @pastel.dim(" • All formats are equivalent - use what feels natural"),
89
+ @pastel.public_send(Tng::UI::Theme.color(:accent)).bold("Pro Tips:"),
90
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
91
+ " #{Tng::UI::Theme.icon(:bullet)} Mix formats: ") + @pastel.public_send(Tng::UI::Theme.color(:secondary),
92
+ "-t=c f=ping") + @pastel.public_send(
93
+ Tng::UI::Theme.color(:muted), " works perfectly"
94
+ ),
95
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
96
+ " #{Tng::UI::Theme.icon(:bullet)} Use filtering in interactive mode for large projects"),
97
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
98
+ " #{Tng::UI::Theme.icon(:bullet)} All formats are equivalent - use what feels natural"),
55
99
  "",
56
- @pastel.cyan.bold("Happy testing! ") + @pastel.yellow("★")
100
+ @pastel.public_send(Tng::UI::Theme.color(:secondary)).bold("Happy testing! ") + @pastel.public_send(
101
+ Tng::UI::Theme.color(:accent), "★"
102
+ )
57
103
  ].join("\n")
58
104
  end
59
105
 
60
106
  def render
61
107
  # Use a reasonable width that matches other UI components
62
- box_width = [@terminal_width - 4, 80].min
63
- box_width = 60 if box_width < 60
108
+ box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
109
+ box_width = [box_width, 80].min # Allow wider for help content
110
+ style = Tng::UI::Theme.box_style(:default)
64
111
 
65
112
  box = TTY::Box.frame(
66
113
  title: { top_left: " TNG Help ", bottom_right: " v#{@version} " },
67
- style: {
68
- fg: :bright_white,
69
- border: { fg: :cyan },
70
- title: { fg: :bright_cyan }
71
- },
72
- padding: [1, 2],
114
+ style: style[:style],
115
+ padding: style[:padding],
73
116
  align: :left,
74
117
  width: box_width
75
118
  ) do
76
119
  content
77
120
  end
78
121
 
79
- center_box(box, box_width)
80
- end
81
-
82
- private
83
-
84
- def center_box(box_string, box_width)
85
- padding = (@terminal_width - box_width) / 2
86
- padding = 0 if padding.negative?
87
- box_string.lines.map { |line| (" " * padding) + line.chomp }.join("\n")
122
+ Tng::UI::Theme.center_box(box, box_width, @terminal_width)
88
123
  end
89
124
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pastel"
4
+ require_relative "theme"
4
5
 
5
6
  class SystemStatusDisplay
6
7
  def initialize(pastel, args)
@@ -11,47 +12,117 @@ class SystemStatusDisplay
11
12
  def display(status)
12
13
  case status[:status]
13
14
  when :ok
14
- puts @pastel.green("✅ System operational") unless @args[:type] && @args[:file]
15
+ unless @args[:type] && @args[:file]
16
+ puts @pastel.public_send(Tng::UI::Theme.color(:success),
17
+ "#{Tng::UI::Theme.icon(:success)} System operational")
18
+ end
15
19
  true
16
20
  when :version_mismatch
17
- puts @pastel.red.bold("❌ Version Mismatch!")
18
- puts @pastel.yellow("Your gem version: #{status[:gem_version]}")
19
- puts @pastel.yellow("Required version: #{status[:current_version]}")
20
- puts
21
- puts @pastel.bright_white("Please update your gem:")
22
- puts @pastel.cyan(" gem update tng")
23
- puts @pastel.dim(" or")
24
- puts @pastel.cyan(" bundle update tng")
21
+ display_status_message(
22
+ title: "Version Mismatch",
23
+ icon: Tng::UI::Theme.icon(:error),
24
+ color: Tng::UI::Theme.color(:error),
25
+ content: [
26
+ "Your gem version: #{status[:gem_version]}",
27
+ "Required version: #{status[:current_version]}",
28
+ "",
29
+ "Quick fix:",
30
+ "• gem update tng",
31
+ "• bundle update tng"
32
+ ]
33
+ )
25
34
  false
26
35
  when :base_url_mismatch
27
- puts @pastel.red.bold("❌ Endpoint URL Mismatch!")
28
- puts @pastel.yellow("Your configured URL: #{status[:user_base_url]}")
29
- puts @pastel.yellow("Server's current URL: #{status[:server_base_url]}")
30
- puts
31
- puts @pastel.bright_white("Please update your configuration:")
32
- puts @pastel.cyan(" Edit config/initializers/tng.rb")
33
- puts @pastel.cyan(" Set: config.base_url = '#{status[:server_base_url]}'")
34
- puts
35
- puts @pastel.dim("This ensures you're connecting to the correct endpoint.")
36
+ display_status_message(
37
+ title: "URL Mismatch",
38
+ icon: Tng::UI::Theme.icon(:error),
39
+ color: Tng::UI::Theme.color(:error),
40
+ content: [
41
+ "Your URL: #{status[:user_base_url]}",
42
+ "Server URL: #{status[:server_base_url]}",
43
+ "",
44
+ "Quick fix:",
45
+ "• Edit config/initializers/tng.rb",
46
+ "• Set: config.base_url = '#{status[:server_base_url]}'"
47
+ ]
48
+ )
36
49
  false
37
50
  when :service_down
38
- puts @pastel.red.bold("❌ Service Unavailable")
39
- puts @pastel.dim(status[:message])
40
- puts
41
- puts @pastel.yellow("The TNG service is currently down.")
42
- puts @pastel.dim("Please try again later or contact support.")
51
+ display_status_message(
52
+ title: "Service Unavailable",
53
+ icon: Tng::UI::Theme.icon(:error),
54
+ color: Tng::UI::Theme.color(:error),
55
+ content: [
56
+ status[:message] || "TNG service is currently down",
57
+ "",
58
+ "Please try again later or contact support"
59
+ ]
60
+ )
43
61
  false
44
62
  when :error
45
- puts @pastel.red.bold("❌ Connection Error")
46
- puts @pastel.dim(status[:message])
47
- puts
48
- puts @pastel.yellow("Unable to connect to TNG service.")
49
- puts @pastel.dim("Please check your internet connection and try again.")
63
+ display_status_message(
64
+ title: "Connection Error",
65
+ icon: Tng::UI::Theme.icon(:error),
66
+ color: Tng::UI::Theme.color(:error),
67
+ content: [
68
+ status[:message] || "Unable to connect to TNG service",
69
+ "",
70
+ "Please check your internet connection and try again"
71
+ ]
72
+ )
50
73
  false
51
74
  else
52
- puts @pastel.red.bold("❌ Unknown Error")
53
- puts @pastel.dim(status[:message] || "An unknown error occurred")
75
+ display_status_message(
76
+ title: "Unknown Error",
77
+ icon: Tng::UI::Theme.icon(:error),
78
+ color: Tng::UI::Theme.color(:error),
79
+ content: [status[:message] || "An unknown error occurred"]
80
+ )
54
81
  false
55
82
  end
56
83
  end
84
+
85
+ private
86
+
87
+ def display_status_message(title:, icon:, color:, content:)
88
+ require "tty-box"
89
+ require "tty-screen"
90
+
91
+ terminal_width = begin
92
+ TTY::Screen.width
93
+ rescue StandardError
94
+ 80
95
+ end
96
+
97
+ formatted_content = content.map do |line|
98
+ case line
99
+ when /^•/
100
+ @pastel.public_send(Tng::UI::Theme.color(:muted), line)
101
+ when /^Quick fix:/
102
+ @pastel.public_send(Tng::UI::Theme.color(:secondary), line)
103
+ when ""
104
+ line
105
+ else
106
+ @pastel.public_send(Tng::UI::Theme.color(:warning), line)
107
+ end
108
+ end.join("\n")
109
+
110
+ box_width = Tng::UI::Theme.calculate_box_width(terminal_width)
111
+
112
+ status_box = TTY::Box.frame(
113
+ title: { top_left: " #{title} " },
114
+ style: {
115
+ fg: :bright_white,
116
+ border: { fg: color },
117
+ title: { fg: color == Tng::UI::Theme.color(:error) ? :bright_red : color }
118
+ },
119
+ padding: [1, 2],
120
+ width: box_width
121
+ ) do
122
+ formatted_content
123
+ end
124
+
125
+ puts Tng::UI::Theme.center_box(status_box, box_width, terminal_width)
126
+ puts
127
+ end
57
128
  end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tng
4
+ module UI
5
+ module Theme
6
+ # Icons - consistent across all UI components
7
+ ICONS = {
8
+ # Status icons
9
+ success: "✅",
10
+ error: "❌",
11
+ warning: "⚠️",
12
+ info: "ℹ️",
13
+
14
+ # Action icons
15
+ rocket: "🚀",
16
+ wave: "👋",
17
+ stats: "📊",
18
+ config: "📋",
19
+ heart: "❤️",
20
+ lightbulb: "💡",
21
+
22
+ # Navigation icons
23
+ back: "⬅️ ", # Added space after back arrow
24
+ marker: "▶",
25
+ bullet: "•"
26
+ }.freeze
27
+
28
+ # Colors - terminal-agnostic color scheme
29
+ COLORS = {
30
+ # Primary status colors
31
+ success: :green,
32
+ error: :red,
33
+ warning: :yellow,
34
+ info: :cyan,
35
+
36
+ # Text hierarchy colors
37
+ primary: :bright_white,
38
+ secondary: :cyan,
39
+ accent: :yellow,
40
+ muted: :dim,
41
+
42
+ # Border colors for boxes
43
+ border_success: :green,
44
+ border_error: :red,
45
+ border_warning: :yellow,
46
+ border_info: :cyan,
47
+ border_primary: :cyan
48
+ }.freeze
49
+
50
+ # Box styling constants
51
+ BOX_STYLES = {
52
+ default: {
53
+ style: {
54
+ border: {
55
+ type: :light,
56
+ top_left: "┌",
57
+ top_right: "┐",
58
+ bottom_left: "└",
59
+ bottom_right: "┘",
60
+ top: "─",
61
+ bottom: "─",
62
+ left: "│",
63
+ right: "│"
64
+ }
65
+ },
66
+ padding: [1, 2],
67
+ max_width: 100
68
+ },
69
+ success: {
70
+ style: {
71
+ border: {
72
+ type: :light,
73
+ top_left: "┌",
74
+ top_right: "┐",
75
+ bottom_left: "└",
76
+ bottom_right: "┘",
77
+ top: "─",
78
+ bottom: "─",
79
+ left: "│",
80
+ right: "│"
81
+ },
82
+ fg: :green
83
+ },
84
+ padding: [1, 2],
85
+ max_width: 100
86
+ },
87
+ warning: {
88
+ style: {
89
+ border: {
90
+ type: :light,
91
+ top_left: "┌",
92
+ top_right: "┐",
93
+ bottom_left: "└",
94
+ bottom_right: "┘",
95
+ top: "─",
96
+ bottom: "─",
97
+ left: "│",
98
+ right: "│"
99
+ },
100
+ fg: :yellow
101
+ },
102
+ padding: [1, 2],
103
+ max_width: 100
104
+ }
105
+ }.freeze
106
+
107
+ # Helper methods for consistent styling
108
+ module_function
109
+
110
+ def detect_terminal_background
111
+ # Manual override for TNG (highest priority)
112
+ case ENV["TNG_TERMINAL_THEME"]&.downcase
113
+ when "light"
114
+ :light
115
+ when "dark"
116
+ :dark
117
+ else
118
+ # Skip background detection if explicitly disabled
119
+ return :dark if ENV["TNG_DISABLE_BACKGROUND_DETECTION"] == "true"
120
+
121
+ get_background_color
122
+ end
123
+ end
124
+
125
+ def get_background_color
126
+ # Don't attempt background detection if not in an interactive terminal
127
+ # or during Rails startup/loading
128
+ return :dark unless $stdout.tty? && $stdin.tty? && interactive_session?
129
+
130
+ print "\e]11;?\e\\"
131
+ $stdout.flush
132
+
133
+ require "timeout"
134
+ begin
135
+ response = ""
136
+ Timeout.timeout(0.1) do
137
+ response = $stdin.read_nonblock(100)
138
+ end
139
+
140
+ case response
141
+ when %r{rgb:([0-9a-f]{4})/([0-9a-f]{4})/([0-9a-f]{4})}i
142
+ r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
143
+ hex.to_i(16) >> 8
144
+ end
145
+ brightness = (r * 0.299 + g * 0.587 + b * 0.114)
146
+ return brightness > 128 ? :light : :dark
147
+ when /rgb:([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i
148
+ r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
149
+ hex.to_i(16)
150
+ end
151
+ brightness = (r * 0.299 + g * 0.587 + b * 0.114)
152
+ return brightness > 128 ? :light : :dark
153
+ end
154
+ rescue IO::WaitReadable, Errno::EAGAIN
155
+ # No data available
156
+ rescue Timeout::Error, StandardError
157
+ # Fall back to environment detection
158
+ end
159
+
160
+ # Enhanced fallback detection
161
+ case ENV["TERM_PROGRAM"]
162
+ when "iTerm.app"
163
+ ENV["ITERM_PROFILE"]&.downcase&.include?("light") ? :light : :dark
164
+ when "Apple_Terminal"
165
+ :light # Terminal.app defaults to light
166
+ when "vscode"
167
+ :dark # VS Code integrated terminal usually dark
168
+ else
169
+ # Check for common dark theme indicators
170
+ if ENV["COLORTERM"] == "truecolor" || ENV["TERM"]&.include?("256")
171
+ :dark
172
+ else
173
+ :light
174
+ end
175
+ end
176
+ end
177
+
178
+ def interactive_session?
179
+ # Skip background detection during Rails startup, bundler operations, etc.
180
+ return false if defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.initializing?
181
+ return false if ENV["BUNDLE_GEMFILE"] && !ENV["TNG_FORCE_BACKGROUND_DETECTION"]
182
+ return false if $PROGRAM_NAME&.include?("bundle")
183
+ return false if $PROGRAM_NAME&.include?("rails") && ARGV.any? { |arg| %w[server console runner].include?(arg) }
184
+
185
+ # Only allow in truly interactive contexts
186
+ ENV["TNG_CLI"] == "true" || $PROGRAM_NAME&.include?("tng")
187
+ end
188
+
189
+ def adaptive_color(color_key)
190
+ base_color = base_color(color_key)
191
+
192
+ # Adjust colors based on terminal background for optimal visibility
193
+ if detect_terminal_background == :light
194
+ # Light terminal adjustments
195
+ case base_color
196
+ when :dim
197
+ :black
198
+ when :bright_white
199
+ :black # White text invisible on light backgrounds
200
+ when :cyan
201
+ :blue # Cyan can be hard to read on light backgrounds
202
+ when :yellow
203
+ :red # Yellow invisible on light backgrounds
204
+ else
205
+ base_color
206
+ end
207
+ else
208
+ # Dark terminal adjustments - optimized for macOS Terminal
209
+ case base_color
210
+ when :dim
211
+ # macOS Terminal renders :white better than :bright_white
212
+ :white
213
+ when :black
214
+ :white # Black text invisible on dark backgrounds
215
+ when :cyan
216
+ # macOS Terminal cyan can be hard to read, use blue
217
+ :blue
218
+ when :yellow
219
+ # macOS Terminal yellow can be faded, use brighter alternatives
220
+ :red
221
+ when :bright_white
222
+ # Force to regular white for better macOS Terminal compatibility
223
+ :white
224
+ else
225
+ base_color
226
+ end
227
+ end
228
+ end
229
+
230
+ def icon(name)
231
+ ICONS[name] || ""
232
+ end
233
+
234
+ def color(name)
235
+ adaptive_color(name)
236
+ end
237
+
238
+ def base_color(name)
239
+ COLORS[name] || :white
240
+ end
241
+
242
+ def box_style(name)
243
+ BOX_STYLES[name] || BOX_STYLES[:default]
244
+ end
245
+
246
+ def calculate_box_width(terminal_width)
247
+ [terminal_width - 4, BOX_STYLES[:default][:max_width]].min
248
+ end
249
+
250
+ def center_box(box_content, box_width, terminal_width)
251
+ lines = box_content.split("\n")
252
+ padding = (terminal_width - box_width) / 2
253
+ padding = 0 if padding.negative?
254
+ lines.map { |line| (" " * padding) + line }.join("\n")
255
+ end
256
+
257
+ def center_text(text, terminal_width)
258
+ # Remove ANSI color codes for length calculation
259
+ clean_text = text.gsub(/\e\[[0-9;]*m/, "")
260
+ padding = (terminal_width - clean_text.length) / 2
261
+ padding = 0 if padding.negative?
262
+ (" " * padding) + text
263
+ end
264
+ end
265
+ end
266
+ end