From 90b49024c0d5fe8fe60942b96dcbd1f610042f1b Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Mon, 13 Nov 2023 21:42:25 +0900 Subject: [PATCH] [ruby/reline] Fallback to 256color if COLORTERM != truecolor (https://github.com/ruby/reline/pull/604) * Fallback to 256color if COLORTERM != truecolor * Add Reline::Face.force_truecolor to force truecolor without COLORTERM env https://github.com/ruby/reline/commit/090e1e4df0 --- doc/reline/face.md | 9 ++++-- lib/reline/face.rb | 42 +++++++++++++++++++++++++++ test/reline/test_face.rb | 61 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/doc/reline/face.md b/doc/reline/face.md index cf3bb69440..1fa916123b 100644 --- a/doc/reline/face.md +++ b/doc/reline/face.md @@ -102,7 +102,10 @@ irb(main):001:0> Reline::Face.configs :scrollbar=>{:foreground=>:white, :background=>:cyan, :escape_sequence=>"\e[0m\e[37;46m"}}} ``` -## Backlog - -- Support for 256-color terminal emulator. Fallback hex color code such as "#FF1020" to 256 colors +## 256-Color and TrueColor +Reline will automatically detect if your terminal emulator supports truecolor with `ENV['COLORTERM] in 'truecolor' | '24bit'`. When this env is not set, Reline will fallback to 256-color. +If your terminal emulator supports truecolor but does not set COLORTERM env, add this line to `.irbrc`. +```ruby +Reline::Face.force_truecolor +``` diff --git a/lib/reline/face.rb b/lib/reline/face.rb index b78f3b1ca5..e18ec957e8 100644 --- a/lib/reline/face.rb +++ b/lib/reline/face.rb @@ -74,6 +74,13 @@ class Reline::Face @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 @@ -82,6 +89,14 @@ class Reline::Face 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;" @@ -90,6 +105,24 @@ class Reline::Face 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 @@ -124,6 +157,15 @@ class Reline::Face 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 diff --git a/test/reline/test_face.rb b/test/reline/test_face.rb index 14da4f6d65..8fa2be8fa4 100644 --- a/test/reline/test_face.rb +++ b/test/reline/test_face.rb @@ -5,12 +5,19 @@ require_relative 'helper' class Reline::Face::Test < Reline::TestCase RESET_SGR = "\e[0m" + def setup + @colorterm_backup = ENV['COLORTERM'] + ENV['COLORTERM'] = 'truecolor' + end + def teardown Reline::Face.reset_to_initial_configs + ENV['COLORTERM'] = @colorterm_backup end class WithInsufficientSetupTest < self def setup + super Reline::Face.config(:my_insufficient_config) do |face| end @face = Reline::Face[:my_insufficient_config] @@ -37,6 +44,7 @@ class Reline::Face::Test < Reline::TestCase class WithSetupTest < self def setup + super Reline::Face.config(:my_config) do |face| face.define :default, foreground: :blue face.define :enhanced, foreground: "#FF1020", background: :black, style: [:bold, :underlined] @@ -148,9 +156,15 @@ class Reline::Face::Test < Reline::TestCase class ConfigTest < self def setup + super @config = Reline::Face.const_get(:Config).new(:my_config) { } end + def teardown + super + Reline::Face.instance_variable_set(:@force_truecolor, nil) + end + def test_rgb? assert_equal true, @config.send(:rgb_expression?, "#FFFFFF") end @@ -190,9 +204,54 @@ class Reline::Face::Test < Reline::TestCase ) end - def test_sgr_rgb + def test_truecolor + ENV['COLORTERM'] = 'truecolor' + assert_equal true, Reline::Face.truecolor? + ENV['COLORTERM'] = '24bit' + assert_equal true, Reline::Face.truecolor? + ENV['COLORTERM'] = nil + assert_equal false, Reline::Face.truecolor? + Reline::Face.force_truecolor + assert_equal true, Reline::Face.truecolor? + end + + def test_sgr_rgb_truecolor + ENV['COLORTERM'] = 'truecolor' assert_equal "38;2;255;255;255", @config.send(:sgr_rgb, :foreground, "#ffffff") assert_equal "48;2;18;52;86", @config.send(:sgr_rgb, :background, "#123456") end + + def test_sgr_rgb_256color + ENV['COLORTERM'] = nil + assert_equal '38;5;231', @config.send(:sgr_rgb, :foreground, '#ffffff') + assert_equal '48;5;16', @config.send(:sgr_rgb, :background, '#000000') + # Color steps are [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] + assert_equal '38;5;24', @config.send(:sgr_rgb, :foreground, '#005f87') + assert_equal '38;5;67', @config.send(:sgr_rgb, :foreground, '#5f87af') + assert_equal '48;5;110', @config.send(:sgr_rgb, :background, '#87afd7') + assert_equal '48;5;153', @config.send(:sgr_rgb, :background, '#afd7ff') + # Boundary values are [0x30, 0x73, 0x9b, 0xc3, 0xeb] + assert_equal '38;5;24', @config.send(:sgr_rgb, :foreground, '#2f729a') + assert_equal '38;5;67', @config.send(:sgr_rgb, :foreground, '#30739b') + assert_equal '48;5;110', @config.send(:sgr_rgb, :background, '#9ac2ea') + assert_equal '48;5;153', @config.send(:sgr_rgb, :background, '#9bc3eb') + end + + def test_force_truecolor_reconfigure + ENV['COLORTERM'] = nil + + Reline::Face.config(:my_config) do |face| + face.define :default, foreground: '#005f87' + face.define :enhanced, background: '#afd7ff' + end + + assert_equal "\e[0m\e[38;5;24m", Reline::Face[:my_config][:default] + assert_equal "\e[0m\e[48;5;153m", Reline::Face[:my_config][:enhanced] + + Reline::Face.force_truecolor + + assert_equal "\e[0m\e[38;2;0;95;135m", Reline::Face[:my_config][:default] + assert_equal "\e[0m\e[48;2;175;215;255m", Reline::Face[:my_config][:enhanced] + end end end