# lib/tryouts/console.rb
require 'pathname'
class Tryouts
module Console
# ANSI escape sequence numbers for text attributes
unless defined? ATTRIBUTES
ATTRIBUTES = {
normal: 0,
bright: 1,
dim: 2,
underline: 4,
blink: 5,
reverse: 7,
hidden: 8,
default: 0,
}.freeze
end
# ANSI escape sequence numbers for text colours
unless defined? COLOURS
COLOURS = {
black: 30,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
white: 37,
default: 39,
random: 30 + rand(10).to_i,
}.freeze
end
# ANSI escape sequence numbers for background colours
unless defined? BGCOLOURS
BGCOLOURS = {
black: 40,
red: 41,
green: 42,
yellow: 43,
blue: 44,
magenta: 45,
cyan: 46,
white: 47,
default: 49,
random: 40 + rand(10).to_i,
}.freeze
end
module InstanceMethods
def bright
Console.bright(self)
end
def underline
Console.underline(self)
end
def reverse
Console.reverse(self)
end
def color(col)
Console.color(col, self)
end
def att(col)
Console.att(col, self)
end
def bgcolor(col)
Console.bgcolor(col, self)
end
end
class << self
def bright(str, io = $stdout)
str = [style(ATTRIBUTES[:bright], io: io), str, default_style(io)].join
str.extend Console::InstanceMethods
str
end
def underline(str, io = $stdout)
str = [style(ATTRIBUTES[:underline], io: io), str, default_style(io)].join
str.extend Console::InstanceMethods
str
end
def reverse(str, io = $stdout)
str = [style(ATTRIBUTES[:reverse], io: io), str, default_style(io)].join
str.extend Console::InstanceMethods
str
end
def color(col, str, io = $stdout)
str = [style(COLOURS[col], io: io), str, default_style(io)].join
str.extend Console::InstanceMethods
str
end
def att(name, str, io = $stdout)
str = [style(ATTRIBUTES[name], io: io), str, default_style(io)].join
str.extend Console::InstanceMethods
str
end
def bgcolor(col, str, io = $stdout)
str = [style(ATTRIBUTES[col], io: io), str, default_style(io)].join
str.extend Console::InstanceMethods
str
end
def style(*att, io: nil)
# Only output ANSI codes if colors are supported
target_io = io || $stdout
# Explicit color control via environment variables
# FORCE_COLOR/CLICOLOR_FORCE override NO_COLOR
return "\e[%sm" % att.join(';') if ENV['FORCE_COLOR'] || ENV['CLICOLOR_FORCE']
return '' if ENV['NO_COLOR']
# Check if we're outputting to a real TTY
tty_output = (target_io.respond_to?(:tty?) && target_io.tty?) ||
($stdout.respond_to?(:tty?) && $stdout.tty?) ||
($stderr.respond_to?(:tty?) && $stderr.tty?)
# If we have a real TTY, always use colors
return "\e[%sm" % att.join(';') if tty_output
# For environments like Claude Code where TTY detection fails but we want colors
# Check if output appears to be redirected to a file/pipe
if ENV['TERM'] && ENV['TERM'] != 'dumb'
# Check if stdout/stderr look like they're redirected using file stats
begin
stdout_stat = $stdout.stat
stderr_stat = $stderr.stat
# If either stdout or stderr looks like a regular file or pipe, disable colors
stdout_redirected = stdout_stat.file? || stdout_stat.pipe?
stderr_redirected = stderr_stat.file? || stderr_stat.pipe?
# Enable colors if neither appears redirected
return "\e[%sm" % att.join(';') unless stdout_redirected || stderr_redirected
rescue StandardError
# If stat fails, fall back to enabling colors with TERM set
return "\e[%sm" % att.join(';')
end
end
# Default: no colors
''
end
def default_style(io = $stdout)
style(ATTRIBUTES[:default], COLOURS[:default], BGCOLOURS[:default], io: io)
end
# Converts an absolute file path to a path relative to the current working
# directory. This simplifies logging and error reporting by showing
# only the relevant parts of file paths instead of lengthy absolute paths.
#
def pretty_path(file)
return nil if file.nil?
file = File.expand_path(file) # be absolutely sure
basepath = Dir.pwd
Pathname.new(file).relative_path_from(basepath).to_s
end
end
end
end