Module: Charming::UI::ColorSupport
- Defined in:
- lib/charming/presentation/ui/color_support.rb
Overview
ColorSupport detects the terminal’s color capability and downconverts colors so themes written in truecolor degrade gracefully on less-capable terminals.
Levels (best to worst): :truecolor, :color256, :color16, :none.
Detection honors NO_COLOR, then COLORTERM (truecolor/24bit), then TERM. ‘UI::ColorSupport.level = :color256` overrides detection (useful in tests and for user preference).
Constant Summary collapse
- LEVELS =
%i[none color16 color256 truecolor].freeze
- CUBE_LEVELS =
The 6-level RGB ramp used by the xterm 256-color cube (indices 16-231).
[0, 95, 135, 175, 215, 255].freeze
Class Method Summary collapse
-
.at_least?(required) ⇒ Boolean
True when the active level is at least required (e.g., ‘at_least?(:color256)`).
- .basic_index_for_code(code) ⇒ Object
-
.basic_palette ⇒ Object
The 16 basic ANSI colors as [SGR foreground code, RGB] pairs.
- .color_distance(a, b) ⇒ Object
-
.cube_index(r, g, b) ⇒ Object
Index in the 6x6x6 cube (16-231) closest to the RGB triple.
-
.detect(env) ⇒ Object
Detects the color capability from an environment hash.
-
.gray_index(r, g, b) ⇒ Object
Index in the grayscale ramp (232-255) closest to the RGB triple’s luminance.
-
.hex_components(hex) ⇒ Object
– conversion internals —————————————————-.
-
.hex_to_16(hex) ⇒ Object
Converts “#rrggbb” to the nearest of the 16 basic ANSI colors, returned as the SGR foreground code (30-37 or 90-97).
-
.hex_to_256(hex) ⇒ Object
Converts “#rrggbb” to the nearest xterm 256-color index (cube or grayscale ramp).
-
.index_to_16(index) ⇒ Object
Converts a 256-color index to the nearest basic ANSI SGR foreground code.
-
.index_to_rgb(index) ⇒ Object
The RGB triple a 256-color index renders as.
-
.level ⇒ Object
The active color level: the explicit override or the detected level (memoized).
-
.level=(value) ⇒ Object
Overrides the detected level (nil resets to auto-detection on next access).
- .nearest_cube_level(component) ⇒ Object
Class Method Details
.at_least?(required) ⇒ Boolean
True when the active level is at least required (e.g., ‘at_least?(:color256)`).
47 48 49 |
# File 'lib/charming/presentation/ui/color_support.rb', line 47 def at_least?(required) LEVELS.index(level) >= LEVELS.index(required) end |
.basic_index_for_code(code) ⇒ Object
124 125 126 |
# File 'lib/charming/presentation/ui/color_support.rb', line 124 def basic_index_for_code(code) (code < 90) ? code - 30 : code - 90 + 8 end |
.basic_palette ⇒ Object
The 16 basic ANSI colors as [SGR foreground code, RGB] pairs.
115 116 117 118 119 120 121 122 |
# File 'lib/charming/presentation/ui/color_support.rb', line 115 def basic_palette @basic_palette ||= { 30 => [0, 0, 0], 31 => [205, 49, 49], 32 => [13, 188, 121], 33 => [229, 229, 16], 34 => [36, 114, 200], 35 => [188, 63, 188], 36 => [17, 168, 205], 37 => [229, 229, 229], 90 => [102, 102, 102], 91 => [241, 76, 76], 92 => [35, 209, 139], 93 => [245, 245, 67], 94 => [59, 142, 234], 95 => [214, 112, 214], 96 => [41, 184, 219], 97 => [255, 255, 255] } end |
.color_distance(a, b) ⇒ Object
110 111 112 |
# File 'lib/charming/presentation/ui/color_support.rb', line 110 def color_distance(a, b) (a[0] - b[0])**2 + (a[1] - b[1])**2 + (a[2] - b[2])**2 end |
.cube_index(r, g, b) ⇒ Object
Index in the 6x6x6 cube (16-231) closest to the RGB triple.
82 83 84 |
# File 'lib/charming/presentation/ui/color_support.rb', line 82 def cube_index(r, g, b) 16 + (36 * nearest_cube_level(r)) + (6 * nearest_cube_level(g)) + nearest_cube_level(b) end |
.detect(env) ⇒ Object
Detects the color capability from an environment hash.
34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/charming/presentation/ui/color_support.rb', line 34 def detect(env) return :none if env["NO_COLOR"] && !env["NO_COLOR"].empty? return :truecolor if %w[truecolor 24bit].include?(env["COLORTERM"]) term = env["TERM"].to_s return :none if term.empty? || term == "dumb" return :truecolor if term.include?("direct") return :color256 if term.include?("256color") :color16 end |
.gray_index(r, g, b) ⇒ Object
Index in the grayscale ramp (232-255) closest to the RGB triple’s luminance.
87 88 89 90 91 |
# File 'lib/charming/presentation/ui/color_support.rb', line 87 def gray_index(r, g, b) gray = ((r + g + b) / 3.0).round step = ((gray - 8) / 10.0).round.clamp(0, 23) 232 + step end |
.hex_components(hex) ⇒ Object
– conversion internals —————————————————-
76 77 78 79 |
# File 'lib/charming/presentation/ui/color_support.rb', line 76 def hex_components(hex) digits = hex.to_s.delete_prefix("#") [digits[0..1].to_i(16), digits[2..3].to_i(16), digits[4..5].to_i(16)] end |
.hex_to_16(hex) ⇒ Object
Converts “#rrggbb” to the nearest of the 16 basic ANSI colors, returned as the SGR foreground code (30-37 or 90-97).
61 62 63 64 65 |
# File 'lib/charming/presentation/ui/color_support.rb', line 61 def hex_to_16(hex) rgb = hex_components(hex) best = basic_palette.min_by { |_code, basic_rgb| color_distance(rgb, basic_rgb) } best.first end |
.hex_to_256(hex) ⇒ Object
Converts “#rrggbb” to the nearest xterm 256-color index (cube or grayscale ramp).
52 53 54 55 56 57 |
# File 'lib/charming/presentation/ui/color_support.rb', line 52 def hex_to_256(hex) r, g, b = hex_components(hex) cube = cube_index(r, g, b) gray = gray_index(r, g, b) (color_distance([r, g, b], index_to_rgb(cube)) <= color_distance([r, g, b], index_to_rgb(gray))) ? cube : gray end |
.index_to_16(index) ⇒ Object
Converts a 256-color index to the nearest basic ANSI SGR foreground code.
68 69 70 71 72 |
# File 'lib/charming/presentation/ui/color_support.rb', line 68 def index_to_16(index) rgb = index_to_rgb(index) best = basic_palette.min_by { |_code, basic_rgb| color_distance(rgb, basic_rgb) } best.first end |
.index_to_rgb(index) ⇒ Object
The RGB triple a 256-color index renders as.
98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/charming/presentation/ui/color_support.rb', line 98 def index_to_rgb(index) if index >= 232 gray = 8 + (index - 232) * 10 [gray, gray, gray] elsif index >= 16 offset = index - 16 [CUBE_LEVELS[offset / 36], CUBE_LEVELS[(offset / 6) % 6], CUBE_LEVELS[offset % 6]] else basic_palette.find { |code, _| basic_index_for_code(code) == index }&.last || [0, 0, 0] end end |
.level ⇒ Object
The active color level: the explicit override or the detected level (memoized).
22 23 24 |
# File 'lib/charming/presentation/ui/color_support.rb', line 22 def level @level ||= detect(ENV) end |
.level=(value) ⇒ Object
Overrides the detected level (nil resets to auto-detection on next access).
27 28 29 30 31 |
# File 'lib/charming/presentation/ui/color_support.rb', line 27 def level=(value) raise ArgumentError, "unknown color level: #{value.inspect}" if value && !LEVELS.include?(value) @level = value end |
.nearest_cube_level(component) ⇒ Object
93 94 95 |
# File 'lib/charming/presentation/ui/color_support.rb', line 93 def nearest_cube_level(component) CUBE_LEVELS.each_index.min_by { |index| (CUBE_LEVELS[index] - component).abs } end |