Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a new class Reline::Face to configure character attributes #552

Merged
merged 34 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1224863
Reine::Face
hasumikin Jun 12, 2023
17114b9
fix test_yamatanooroti
hasumikin Jun 13, 2023
e033f88
Define singleton methods to make accessors to attributes of a face
hasumikin Jun 15, 2023
629db24
s/display/foreground/
hasumikin Oct 10, 2023
a993e52
s/default/default_style/ && s/normal_line/default/ && s/enhanced_line…
hasumikin Oct 10, 2023
cab307f
fix typo
hasumikin Oct 10, 2023
e4d61dd
FaceConfig.new now takes keyword arguments
hasumikin Oct 10, 2023
0e7acfb
Update lib/reline/face.rb
hasumikin Oct 14, 2023
b442c57
Update test/reline/test_face.rb
hasumikin Oct 14, 2023
389ff41
Fix to correspond to frozen_string_literal
hasumikin Oct 14, 2023
c3f92f1
Face::FaceConfig -> Face::Config
hasumikin Oct 14, 2023
d652fc0
Merge branch 'master' into reline_face
hasumikin Oct 14, 2023
1550519
ref https://github.com/ruby/reline/pull/552#pullrequestreview-1677282576
hasumikin Oct 14, 2023
6e4b4e9
delete unused ivar
hasumikin Oct 14, 2023
e8eb85b
ref https://github.com/ruby/reline/pull/552#discussion_r1358783723
hasumikin Oct 14, 2023
7ea200e
insert "\e[0m" into all SGR
hasumikin Oct 16, 2023
26cb115
tiny fix
hasumikin Oct 23, 2023
9286c59
ESSENTIAL_DEFINE_NAMES
hasumikin Oct 23, 2023
f001348
Change to Hash-accessor style
hasumikin Oct 23, 2023
28197be
Cache array method call in local variable
hasumikin Oct 24, 2023
efb75ed
Tests for Face configuration variations
hasumikin Nov 2, 2023
e7a6d56
resolve https://github.com/ruby/reline/pull/552#pullrequestreview-171…
hasumikin Nov 3, 2023
5a53084
amend to
hasumikin Nov 3, 2023
67a8497
check invalid SGR parameter in :style
hasumikin Nov 3, 2023
c02205c
The order of define values should be preserved
hasumikin Nov 3, 2023
728ca2d
Update test/reline/test_face.rb
hasumikin Nov 4, 2023
cde902e
Update test/reline/test_face.rb
hasumikin Nov 4, 2023
23d2360
Add methods: load_initial_config and reset_to_initial_config. And tea…
hasumikin Nov 5, 2023
2eee3b0
omission in amending "style: :default" to "style: :reset"
hasumikin Nov 5, 2023
76dd7b0
refs https://github.com/ruby/reline/issues/598
hasumikin Nov 6, 2023
1ce4818
Fix link
hasumikin Nov 6, 2023
7c5133c
amend method name
hasumikin Nov 6, 2023
a33109f
Merge branch 'master' into reline_face
hasumikin Nov 6, 2023
544f3d1
Update lib/reline/face.rb
hasumikin Nov 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,3 +12,4 @@ end
gem 'bundler'
gem 'rake'
gem 'test-unit'
gem 'test-unit-rr'
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_config

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
hasumikin marked this conversation as resolved.
Show resolved Hide resolved
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;"
hasumikin marked this conversation as resolved.
Show resolved Hide resolved
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_config
config(:default) do |conf|
conf.define :default, style: :reset
conf.define :enhanced, style: :reset
conf.define :scrollbar, style: :reset
end
Comment on lines +141 to +145
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this config required for? It does not appear to be used.
Sorry if I have missed it.

Copy link
Collaborator Author

@hasumikin hasumikin Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In IRB, configs[:default] is used for all character output except the completion dialog.
Actually, :enhanced and :scrollbar are not used.
I was also unsure whether to keep them or not, but even if they were there, IRB wouldn't malfunction so I left it. But should they be deleted?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like IRB's document dialog (which does not specify face yet) use it.

Copy link
Member

@ima1zumi ima1zumi Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for letting me know.
Does it mean that a default value is used if the face argument is not provided in Reline::DialogRenderInfo.new?

Like this code https://github.com/ruby/irb/blob/v1.8.3/lib/irb/input-method.rb#L382

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/ruby/reline/pull/552/files#diff-082d93e1b70331eead4465512d8e0f47b9a91734b7c899d5929168c620cf8025R65-R67

As a result of ensuring that the absence of :enhanced and :scrollbar attributes are not broken even if the user forgets the scrollbar attribute in the completion_dialog config, enhanced and scrollbar attributes are automatically created in the :default config, too.
This is kind of a compromise with the existing implementation, where the concept of "completion_dialog" basically belongs to the IRB, but the SGR settings are set on the Reline side.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I overlooked tompng's comment.
default config is used here https://github.com/ruby/reline/pull/552/files#diff-9703d9141e08b96c2d616930b207873b52dc9eb68b81223818fe250cdb0e927fR834

Totally I made confusing this thread, lol

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I understood.

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_config
@configs = {}
load_initial_config
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
Loading