Mini Shell
# frozen_string_literal: true
class Reline::Face
SGR_PARAMETERS = {
foreground: {
black: 30,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
white: 37,
bright_black: 90,
gray: 90,
bright_red: 91,
bright_green: 92,
bright_yellow: 93,
bright_blue: 94,
bright_magenta: 95,
bright_cyan: 96,
bright_white: 97
},
background: {
black: 40,
red: 41,
green: 42,
yellow: 43,
blue: 44,
magenta: 45,
cyan: 46,
white: 47,
bright_black: 100,
gray: 100,
bright_red: 101,
bright_green: 102,
bright_yellow: 103,
bright_blue: 104,
bright_magenta: 105,
bright_cyan: 106,
bright_white: 107,
},
style: {
reset: 0,
bold: 1,
faint: 2,
italicized: 3,
underlined: 4,
slowly_blinking: 5,
blinking: 5,
rapidly_blinking: 6,
negative: 7,
concealed: 8,
crossed_out: 9
}
}.freeze
class Config
ESSENTIAL_DEFINE_NAMES = %i(default enhanced scrollbar).freeze
RESET_SGR = "\e[0m".freeze
def initialize(name, &block)
@definition = {}
block.call(self)
ESSENTIAL_DEFINE_NAMES.each do |name|
@definition[name] ||= { style: :reset, escape_sequence: RESET_SGR }
end
end
attr_reader :definition
def define(name, **values)
values[:escape_sequence] = format_to_sgr(values.to_a).freeze
@definition[name] = values
end
def reconfigure
@definition.each_value do |values|
values.delete(:escape_sequence)
values[:escape_sequence] = format_to_sgr(values.to_a).freeze
end
end
def [](name)
@definition.dig(name, :escape_sequence) or raise ArgumentError, "unknown face: #{name}"
end
private
def sgr_rgb(key, value)
return nil unless rgb_expression?(value)
if Reline::Face.truecolor?
sgr_rgb_truecolor(key, value)
else
sgr_rgb_256color(key, value)
end
end
def sgr_rgb_truecolor(key, value)
case key
when :foreground
"38;2;"
when :background
"48;2;"
end + value[1, 6].scan(/../).map(&:hex).join(";")
end
def sgr_rgb_256color(key, value)
# 256 colors are
# 0..15: standard colors, hight intensity colors
# 16..232: 216 colors (R, G, B each 6 steps)
# 233..255: grayscale colors (24 steps)
# This methods converts rgb_expression to 216 colors
rgb = value[1, 6].scan(/../).map(&:hex)
# Color steps are [0, 95, 135, 175, 215, 255]
r, g, b = rgb.map { |v| v <= 95 ? v / 48 : (v - 35) / 40 }
color = (16 + 36 * r + 6 * g + b)
case key
when :foreground
"38;5;#{color}"
when :background
"48;5;#{color}"
end
end
def format_to_sgr(ordered_values)
sgr = "\e[" + ordered_values.map do |key_value|
key, value = key_value
case key
when :foreground, :background
case value
when Symbol
SGR_PARAMETERS[key][value]
when String
sgr_rgb(key, value)
end
when :style
[ value ].flatten.map do |style_name|
SGR_PARAMETERS[:style][style_name]
end.then do |sgr_parameters|
sgr_parameters.include?(nil) ? nil : sgr_parameters
end
end.then do |rendition_expression|
unless rendition_expression
raise ArgumentError, "invalid SGR parameter: #{value.inspect}"
end
rendition_expression
end
end.join(';') + "m"
sgr == RESET_SGR ? RESET_SGR : RESET_SGR + sgr
end
def rgb_expression?(color)
color.respond_to?(:match?) and color.match?(/\A#[0-9a-fA-F]{6}\z/)
end
end
private_constant :SGR_PARAMETERS, :Config
def self.truecolor?
@force_truecolor || %w[truecolor 24bit].include?(ENV['COLORTERM'])
end
def self.force_truecolor
@force_truecolor = true
@configs&.each_value(&:reconfigure)
end
def self.[](name)
@configs[name]
end
def self.config(name, &block)
@configs ||= {}
@configs[name] = Config.new(name, &block)
end
def self.configs
@configs.transform_values(&:definition)
end
def self.load_initial_configs
config(:default) do |conf|
conf.define :default, style: :reset
conf.define :enhanced, style: :reset
conf.define :scrollbar, style: :reset
end
config(:completion_dialog) do |conf|
conf.define :default, foreground: :bright_white, background: :gray
conf.define :enhanced, foreground: :black, background: :white
conf.define :scrollbar, foreground: :white, background: :gray
end
end
def self.reset_to_initial_configs
@configs = {}
load_initial_configs
end
end
Zerion Mini Shell 1.0