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
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



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/tng/ui/theme.rb', line 182

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



231
232
233
# File 'lib/tng/ui/theme.rb', line 231

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

.box_style(name) ⇒ Object



235
236
237
# File 'lib/tng/ui/theme.rb', line 235

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

.calculate_box_width(terminal_width) ⇒ Object



239
240
241
# File 'lib/tng/ui/theme.rb', line 239

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



243
244
245
246
247
248
# File 'lib/tng/ui/theme.rb', line 243

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



250
251
252
253
254
255
256
# File 'lib/tng/ui/theme.rb', line 250

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



227
228
229
# File 'lib/tng/ui/theme.rb', line 227

def color(name)
  adaptive_color(name)
end

.detect_terminal_backgroundObject



113
114
115
116
117
118
119
120
# File 'lib/tng/ui/theme.rb', line 113

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



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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
# File 'lib/tng/ui/theme.rb', line 122

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



223
224
225
# File 'lib/tng/ui/theme.rb', line 223

def icon(name)
  ICONS[name] || ""
end

.interactive_session?Boolean

Returns:

  • (Boolean)


173
174
175
176
177
178
179
180
# File 'lib/tng/ui/theme.rb', line 173

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