Skip to content

Commit

Permalink
Introduce a new class Reline::Face to configure character attributes (#…
Browse files Browse the repository at this point in the history
…552)

* Reine::Face

* fix test_yamatanooroti

* Define singleton methods to make accessors to attributes of a face

* s/display/foreground/

* s/default/default_style/ && s/normal_line/default/ && s/enhanced_line/enhanced/

* fix typo

* FaceConfig.new now takes keyword arguments

* Update lib/reline/face.rb

Co-authored-by: Stan Lo <stan001212@gmail.com>

* Update test/reline/test_face.rb

Co-authored-by: Stan Lo <stan001212@gmail.com>

* Fix to correspond to frozen_string_literal

* Face::FaceConfig -> Face::Config

* ref #552 (review)

* delete unused ivar

* ref #552 (comment)

* insert "\e[0m" into all SGR

* tiny fix

* ESSENTIAL_DEFINE_NAMES

ref #552 (comment)

* Change to Hash-accessor style

- Reline::Face[:completion_dialog].enhanced ->
  Reline::Face[:completion_dialog][:enhanced]
- Reline::Face.configs shows all defined values

* Cache array method call in local variable

* Tests for Face configuration variations

* resolve #552 (review)

* amend  to

* check invalid SGR parameter in :style

* The order of define values should be preserved

* Update test/reline/test_face.rb

Co-authored-by: Stan Lo <stan001212@gmail.com>

* Update test/reline/test_face.rb

Co-authored-by: Stan Lo <stan001212@gmail.com>

* Add methods: load_initial_config and reset_to_initial_config. And teardown in tests

* omission in amending "style: :default" to "style: :reset"

* refs #598

* Fix link

* amend method name

* Update lib/reline/face.rb

Co-authored-by: ima1zumi <52617472+ima1zumi@users.noreply.github.com>

---------

Co-authored-by: Stan Lo <stan001212@gmail.com>
Co-authored-by: ima1zumi <52617472+ima1zumi@users.noreply.github.com>
  • Loading branch information
3 people committed Nov 6, 2023
1 parent e094be0 commit fdc1d3b
Show file tree
Hide file tree
Showing 11 changed files with 590 additions and 83 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ end
gem 'bundler'
gem 'rake'
gem 'test-unit'
gem 'test-unit-rr'

gem 'racc'
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ end

See also: [test/reline/yamatanooroti/multiline_repl](https://github.com/ruby/reline/blob/master/test/reline/yamatanooroti/multiline_repl)

## Documentation

### Reline::Face

You can modify the text color and text decorations in your terminal emulator.
See [doc/reline/face.md](./doc/reline/face.md)

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/reline.
Expand Down
108 changes: 108 additions & 0 deletions doc/reline/face.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Face

With the `Reline::Face` class, you can modify the text color and text decorations in your terminal emulator.
This is primarily used to customize the appearance of the method completion dialog in IRB.

## Usage

### ex: Change the background color of the completion dialog cyan to blue

```ruby
Reline::Face.config(:completion_dialog) do |conf|
conf.define :default, foreground: :white, background: :blue
# ^^^^^ `:cyan` by default
conf.define :enhanced, foreground: :white, background: :magenta
conf.define :scrollbar, foreground: :white, background: :blue
end
```

If you provide the above code to an IRB session in some way, you can apply the configuration.
It's generally done by writing it in `.irbrc`.

Regarding `.irbrc`, please refer to the following link: [https://docs.ruby-lang.org/en/master/IRB.html](https://docs.ruby-lang.org/en/master/IRB.html)

## Available parameters

`Reline::Face` internally creates SGR (Select Graphic Rendition) code according to the block parameter of `Reline::Face.config` method.

| Key | Value | SGR Code (numeric part following "\e[")|
|:------------|:------------------|-----:|
| :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 |

- The value for `:style` can be both a Symbol and an Array
```ruby
# Single symbol
conf.define :default, style: :bold
# Array
conf.define :default, style: [:bold, :negative]
```
- The availability of specific SGR codes depends on your terminal emulator
- You can specify a hex color code to `:foreground` and `:background` color like `foreground: "#FF1020"`. Its availability also depends on your terminal emulator

## Debugging

You can see the current Face configuration by `Reline::Face.configs` method

Example:

```ruby
irb(main):001:0> Reline::Face.configs
=>
{:default=>
{:default=>{:style=>:reset, :escape_sequence=>"\e[0m"},
:enhanced=>{:style=>:reset, :escape_sequence=>"\e[0m"},
:scrollbar=>{:style=>:reset, :escape_sequence=>"\e[0m"}},
:completion_dialog=>
{:default=>{:foreground=>:white, :background=>:cyan, :escape_sequence=>"\e[0m\e[37;46m"},
:enhanced=>{:foreground=>:white, :background=>:magenta, :escape_sequence=>"\e[0m\e[37;45m"},
: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

14 changes: 6 additions & 8 deletions lib/reline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require 'reline/line_editor'
require 'reline/history'
require 'reline/terminfo'
require 'reline/face'
require 'rbconfig'

module Reline
Expand Down Expand Up @@ -36,10 +37,8 @@ def match?(other)
DialogRenderInfo = Struct.new(
:pos,
:contents,
:bg_color,
:pointer_bg_color,
:fg_color,
:pointer_fg_color,
:face,
:bg_color, # For the time being, this line should stay here for the compatibility with IRB.
:width,
:height,
:scrollbar,
Expand Down Expand Up @@ -260,10 +259,7 @@ def get_screen_size
contents: result,
scrollbar: true,
height: [15, preferred_dialog_height].min,
bg_color: 46,
pointer_bg_color: 45,
fg_color: 37,
pointer_fg_color: 37
face: :completion_dialog
)
}
Reline::DEFAULT_DIALOG_CONTEXT = Array.new
Expand Down Expand Up @@ -606,4 +602,6 @@ def self.update_iogate
io
end

Reline::Face.load_initial_configs

Reline::HISTORY = Reline::History.new(Reline.core.config)
157 changes: 157 additions & 0 deletions lib/reline/face.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# 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 [](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)
case key
when :foreground
"38;2;"
when :background
"48;2;"
end + value[1, 6].scan(/../).map(&:hex).join(";")
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.[](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: :white, background: :cyan
conf.define :enhanced, foreground: :white, background: :magenta
conf.define :scrollbar, foreground: :white, background: :cyan
end
end

def self.reset_to_initial_configs
@configs = {}
load_initial_configs
end
end
23 changes: 10 additions & 13 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -831,27 +831,24 @@ def add_dialog_proc(name, p, context = nil)
dialog.column = 0
dialog.width = @screen_size.last
end
face = Reline::Face[dialog_render_info.face || :default]
scrollbar_sgr = face[:scrollbar]
default_sgr = face[:default]
enhanced_sgr = face[:enhanced]
dialog.contents = contents.map.with_index do |item, i|
if i == pointer
fg_color = dialog_render_info.pointer_fg_color
bg_color = dialog_render_info.pointer_bg_color
else
fg_color = dialog_render_info.fg_color
bg_color = dialog_render_info.bg_color
end
line_sgr = i == pointer ? enhanced_sgr : default_sgr
str_width = dialog.width - (scrollbar_pos.nil? ? 0 : @block_elem_width)
str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width)
colored_content = "\e[#{bg_color}m\e[#{fg_color}m#{str}"
colored_content = "#{line_sgr}#{str}"
if scrollbar_pos
color_seq = "\e[37m"
if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height)
colored_content + color_seq + @full_block
colored_content + scrollbar_sgr + @full_block
elsif scrollbar_pos <= (i * 2) and (i * 2) < (scrollbar_pos + bar_height)
colored_content + color_seq + @upper_half_block
colored_content + scrollbar_sgr + @upper_half_block
elsif scrollbar_pos <= (i * 2 + 1) and (i * 2) < (scrollbar_pos + bar_height)
colored_content + color_seq + @lower_half_block
colored_content + scrollbar_sgr + @lower_half_block
else
colored_content + color_seq + ' ' * @block_elem_width
colored_content + scrollbar_sgr + ' ' * @block_elem_width
end
else
colored_content
Expand Down
1 change: 1 addition & 0 deletions test/reline/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

require 'reline'
require 'test/unit'
require 'test/unit/rr'

begin
require 'rbconfig'
Expand Down
Loading

0 comments on commit fdc1d3b

Please sign in to comment.