tng 0.1.2 → 0.1.4

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,248 @@
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
+ get_background_color
119
+ end
120
+ end
121
+
122
+ def get_background_color
123
+ print "\e]11;?\e\\"
124
+ $stdout.flush
125
+
126
+ require "timeout"
127
+ begin
128
+ response = ""
129
+ Timeout.timeout(0.1) do
130
+ response = $stdin.read_nonblock(100)
131
+ end
132
+
133
+ case response
134
+ when %r{rgb:([0-9a-f]{4})/([0-9a-f]{4})/([0-9a-f]{4})}i
135
+ r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
136
+ hex.to_i(16) >> 8
137
+ end
138
+ brightness = (r * 0.299 + g * 0.587 + b * 0.114)
139
+ return brightness > 128 ? :light : :dark
140
+ when /rgb:([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i
141
+ r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
142
+ hex.to_i(16)
143
+ end
144
+ brightness = (r * 0.299 + g * 0.587 + b * 0.114)
145
+ return brightness > 128 ? :light : :dark
146
+ end
147
+ rescue IO::WaitReadable, Errno::EAGAIN
148
+ # No data available
149
+ rescue Timeout::Error, StandardError
150
+ # Fall back to environment detection
151
+ end
152
+
153
+ # Enhanced fallback detection
154
+ case ENV["TERM_PROGRAM"]
155
+ when "iTerm.app"
156
+ ENV["ITERM_PROFILE"]&.downcase&.include?("light") ? :light : :dark
157
+ when "Apple_Terminal"
158
+ :light # Terminal.app defaults to light
159
+ when "vscode"
160
+ :dark # VS Code integrated terminal usually dark
161
+ else
162
+ # Check for common dark theme indicators
163
+ if ENV["COLORTERM"] == "truecolor" || ENV["TERM"]&.include?("256")
164
+ :dark
165
+ else
166
+ :light
167
+ end
168
+ end
169
+ end
170
+
171
+ def adaptive_color(color_key)
172
+ base_color = base_color(color_key)
173
+
174
+ # Adjust colors based on terminal background for optimal visibility
175
+ if detect_terminal_background == :light
176
+ # Light terminal adjustments
177
+ case base_color
178
+ when :dim
179
+ :black
180
+ when :bright_white
181
+ :black # White text invisible on light backgrounds
182
+ when :cyan
183
+ :blue # Cyan can be hard to read on light backgrounds
184
+ when :yellow
185
+ :red # Yellow invisible on light backgrounds
186
+ else
187
+ base_color
188
+ end
189
+ else
190
+ # Dark terminal adjustments - optimized for macOS Terminal
191
+ case base_color
192
+ when :dim
193
+ # macOS Terminal renders :white better than :bright_white
194
+ :white
195
+ when :black
196
+ :white # Black text invisible on dark backgrounds
197
+ when :cyan
198
+ # macOS Terminal cyan can be hard to read, use blue
199
+ :blue
200
+ when :yellow
201
+ # macOS Terminal yellow can be faded, use brighter alternatives
202
+ :red
203
+ when :bright_white
204
+ # Force to regular white for better macOS Terminal compatibility
205
+ :white
206
+ else
207
+ base_color
208
+ end
209
+ end
210
+ end
211
+
212
+ def icon(name)
213
+ ICONS[name] || ""
214
+ end
215
+
216
+ def color(name)
217
+ adaptive_color(name)
218
+ end
219
+
220
+ def base_color(name)
221
+ COLORS[name] || :white
222
+ end
223
+
224
+ def box_style(name)
225
+ BOX_STYLES[name] || BOX_STYLES[:default]
226
+ end
227
+
228
+ def calculate_box_width(terminal_width)
229
+ [terminal_width - 4, BOX_STYLES[:default][:max_width]].min
230
+ end
231
+
232
+ def center_box(box_content, box_width, terminal_width)
233
+ lines = box_content.split("\n")
234
+ padding = (terminal_width - box_width) / 2
235
+ padding = 0 if padding.negative?
236
+ lines.map { |line| (" " * padding) + line }.join("\n")
237
+ end
238
+
239
+ def center_text(text, terminal_width)
240
+ # Remove ANSI color codes for length calculation
241
+ clean_text = text.gsub(/\e\[[0-9;]*m/, "")
242
+ padding = (terminal_width - clean_text.length) / 2
243
+ padding = 0 if padding.negative?
244
+ (" " * padding) + text
245
+ end
246
+ end
247
+ end
248
+ end