Module: Tng::UI::Theme

Defined in:
lib/tng/ui/theme.rb

Constant Summary collapse

ICONS =

Icons - consistent across all UI components

{
  # Status icons
  success: "",
  error: "",
  warning: "⚠️",
  info: "ℹ️",

  # Action icons
  rocket: "🚀",
  run: "",
  wave: "👋",
  stats: "📊",
  config: "📋",
  heart: "❤️",
  lightbulb: "💡",

  # Navigation icons
  back: "⬅️ ", # Added space after back arrow
  marker: "",
  bullet: ""
}.freeze
ICONS_ASCII =
{
  success: "[OK]",
  error: "[ERR]",
  warning: "[!]",
  info: "[i]",
  rocket: ">>",
  run: ">",
  wave: "hi",
  stats: "[stats]",
  config: "[cfg]",
  heart: "<3",
  lightbulb: "[tip]",
  back: "<- ",
  marker: ">",
  bullet: "-"
}.freeze
COLORS =

Colors - terminal-agnostic color scheme

{
  # Primary status colors
  success: :green,
  error: :red,
  warning: :yellow,
  info: :cyan,

  # Text hierarchy colors
  primary: :bright_white,
  secondary: :cyan,
  accent: :yellow,
  muted: :dim,

  # Border colors for boxes
  border_success: :green,
  border_error: :red,
  border_warning: :yellow,
  border_info: :cyan,
  border_primary: :cyan
}.freeze
BOX_STYLES =

Box styling constants

{
  default: {
    style: {
      border: {
        type: :light,
        top_left: "",
        top_right: "",
        bottom_left: "",
        bottom_right: "",
        top: "",
        bottom: "",
        left: "",
        right: ""
      }
    },
    padding: [1, 2],
    max_width: 100
  },
  success: {
    style: {
      border: {
        type: :light,
        top_left: "",
        top_right: "",
        bottom_left: "",
        bottom_right: "",
        top: "",
        bottom: "",
        left: "",
        right: ""
      },
      fg: :green
    },
    padding: [1, 2],
    max_width: 100
  },
  warning: {
    style: {
      border: {
        type: :light,
        top_left: "",
        top_right: "",
        bottom_left: "",
        bottom_right: "",
        top: "",
        bottom: "",
        left: "",
        right: ""
      },
      fg: :yellow
    },
    padding: [1, 2],
    max_width: 100
  }
}.freeze

Class Method Summary collapse

Class Method Details

.adaptive_color(color_key) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/tng/ui/theme.rb', line 204

def adaptive_color(color_key)
  base_color = base_color(color_key)

  # Adjust colors based on terminal background for optimal visibility
  if detect_terminal_background == :light
    # Light terminal adjustments
    case base_color
    when :dim
      :black
    when :bright_white
      :black  # White text invisible on light backgrounds
    when :cyan
      :blue   # Cyan can be hard to read on light backgrounds
    when :yellow
      :red    # Yellow invisible on light backgrounds
    else
      base_color
    end
  else
    # Dark terminal adjustments - optimized for macOS Terminal
    case base_color
    when :dim
      # macOS Terminal renders :white better than :bright_white
      :white
    when :black
      :white # Black text invisible on dark backgrounds
    when :cyan
      # macOS Terminal cyan can be hard to read, use blue
      :blue
    when :yellow
      # macOS Terminal yellow can be faded, use brighter alternatives
      :red
    when :bright_white
      # Force to regular white for better macOS Terminal compatibility
      :white
    else
      base_color
    end
  end
end

.base_color(name) ⇒ Object



253
254
255
# File 'lib/tng/ui/theme.rb', line 253

def base_color(name)
  COLORS[name] || :white
end

.box_style(name) ⇒ Object



257
258
259
# File 'lib/tng/ui/theme.rb', line 257

def box_style(name)
  BOX_STYLES[name] || BOX_STYLES[:default]
end

.calculate_box_width(terminal_width) ⇒ Object



261
262
263
# File 'lib/tng/ui/theme.rb', line 261

def calculate_box_width(terminal_width)
  [terminal_width - 4, BOX_STYLES[:default][:max_width]].min
end

.center_box(box_content, box_width, terminal_width) ⇒ Object



265
266
267
268
269
270
# File 'lib/tng/ui/theme.rb', line 265

def center_box(box_content, box_width, terminal_width)
  lines = box_content.split("\n")
  padding = (terminal_width - box_width) / 2
  padding = 0 if padding.negative?
  lines.map { |line| (" " * padding) + line }.join("\n")
end

.center_text(text, terminal_width) ⇒ Object



272
273
274
275
276
277
278
# File 'lib/tng/ui/theme.rb', line 272

def center_text(text, terminal_width)
  # Remove ANSI color codes for length calculation
  clean_text = text.gsub(/\e\[[0-9;]*m/, "")
  padding = (terminal_width - clean_text.length) / 2
  padding = 0 if padding.negative?
  (" " * padding) + text
end

.color(name) ⇒ Object



249
250
251
# File 'lib/tng/ui/theme.rb', line 249

def color(name)
  adaptive_color(name)
end

.detect_terminal_backgroundObject



130
131
132
133
134
135
136
137
# File 'lib/tng/ui/theme.rb', line 130

def detect_terminal_background
  # Return cached result if available
  return @background_cache if @background_cache

  # Get background color and cache the result
  @background_cache = get_background_color
  @background_cache
end

.get_background_colorObject



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/tng/ui/theme.rb', line 144

def get_background_color
  return :dark unless $stdout.tty? && $stdin.tty? && interactive_session?

  print "\e]11;?\e\\"
  $stdout.flush

  require "timeout"
  begin
    response = ""
    Timeout.timeout(0.1) do
      response = $stdin.read_nonblock(100)
    end

    case response
    when %r{rgb:([0-9a-f]{4})/([0-9a-f]{4})/([0-9a-f]{4})}i
      r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
        hex.to_i(16) >> 8
      end
      brightness = (r * 0.299 + g * 0.587 + b * 0.114)
      return brightness > 128 ? :light : :dark
    when /rgb:([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i
      r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
        hex.to_i(16)
      end
      brightness = (r * 0.299 + g * 0.587 + b * 0.114)
      return brightness > 128 ? :light : :dark
    end
  rescue IO::WaitReadable, Errno::EAGAIN
    # No data available
  rescue Timeout::Error, StandardError
    # Fall back to environment detection
  end

  # Enhanced fallback detection
  case ENV["TERM_PROGRAM"]
  when "iTerm.app"
    ENV["ITERM_PROFILE"]&.downcase&.include?("light") ? :light : :dark
  when "Apple_Terminal"
    :light  # Terminal.app defaults to light
  when "vscode"
    :dark   # VS Code integrated terminal usually dark
  else
    # Check for common dark theme indicators
    if ENV["COLORTERM"] == "truecolor" || ENV["TERM"]&.include?("256")
      :dark
    else
      :light
    end
  end
end

.icon(name) ⇒ Object



139
140
141
142
# File 'lib/tng/ui/theme.rb', line 139

def icon(key, ascii: false)
  icons = ascii ? ICONS_ASCII : ICONS
  icons[key] || ""
end

.interactive_session?Boolean

Returns:



195
196
197
198
199
200
201
202
# File 'lib/tng/ui/theme.rb', line 195

def interactive_session?
  return false if defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.initialized?
  return false if ENV["BUNDLE_GEMFILE"]
  return false if $PROGRAM_NAME&.include?("bundle")
  return false if $PROGRAM_NAME&.include?("rails") && ARGV.any? { |arg| %w[server console runner].include?(arg) }

  ENV["TNG_CLI"] == "true" || $PROGRAM_NAME&.include?("tng")
end