Class: Styles::Colors

Inherits:
Object
  • Object
show all
Defined in:
lib/styles/colors.rb

Overview

Basically a wrapper around Term::ANSIColor but also adds combination foreground and background colors (e.g. :red_on_white). Returns nil with an invalid color specification.

Constant Summary collapse

CSS_TO_ANSI_VALUES =

Map CSS-style value to ANSI code name, where they are different

{
  :line_through => :strikethrough
}.freeze
FOREGROUND_COLOR_VALUES =
[
  :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white
].freeze
BACKGROUND_COLOR_VALUES =
[
  :on_black, :on_red, :on_green, :on_yellow, :on_blue, :on_magenta, :on_cyan, :on_white
].freeze
TEXT_DECORATION_VALUES =
[:underline, :strikethrough, :blink].freeze
COLOR_VALUES =
(FOREGROUND_COLOR_VALUES + BACKGROUND_COLOR_VALUES).freeze
OTHER_STYLE_VALUES =
[:bold, :italic, :underline, :underscore, :blink, :strikethrough]
NEGATIVE_PSEUDO_VALUES =

Only :reset is available to represent the complete absence of color and styling. There are no fine-grained negative codes to just remove foreground color or just remove bold. Our API should provide these to allow these kind of fine-grained transitions to other color states.

[
  :no_fg_color, :no_bg_color, :no_bold, :no_italic, :no_text_decoration,
  :no_underline, :no_blink, :no_strikethrough
].freeze
VALID_VALUES =
(::Term::ANSIColor.attributes + [:none] + CSS_TO_ANSI_VALUES.keys).freeze
VALID_VALUES_AND_PSEUDO_VALUES =
(VALID_VALUES + NEGATIVE_PSEUDO_VALUES).freeze

Class Method Summary collapse

Class Method Details

.[](*colors) ⇒ Object Also known as: c

Retrieve color codes with the corresponding symbol. Can be basic colors like :red or “compound” colors specifying foreground and background colors like :red_on_blue.

Any number of colors can be specified, either as multiple arguments or in an array.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/styles/colors.rb', line 40

def self.[](*colors)
  colors.flatten!
  valid_colors = []
  colors.each do |color|
    if is_valid_basic_value? color
      valid_colors << (CSS_TO_ANSI_VALUES[color] || color)
    elsif color_parts = is_compound_color?(color)
      valid_colors += color_parts
    end
  end

  unless valid_colors.empty?
    valid_colors.uniq!
    valid_colors.sort!
    valid_colors.unshift(:reset) if valid_colors.delete(:reset)

    valid_colors.inject('') { |str, color| str += ansi_color.send(color) }
  end
end

.color(string, *colors) ⇒ Object

Apply any valid colors to a string and auto-reset (if any colors applied). Does not apply colors to an empty string.



66
67
68
69
70
71
72
73
74
75
# File 'lib/styles/colors.rb', line 66

def self.color(string, *colors)
  return string if string.nil? or string.empty?
  colors.flatten!
  colors.reject! { |col| col == :none || !VALID_VALUES.include?(col) }
  if colors.any?
    "#{colors.map { |col| c(col) }.join}#{string}#{c(:reset)}"
  else
    string
  end
end

.color_transition(before_colors, after_colors, hard = true) ⇒ Object

Produces a string of color codes to transition from one set of colors to another.

If hard is true then all foregound and background colors are reset before adding the after colors. In other words, no colors are allowed to continue, even if not replaced.

If hard is false then colors that are not explicitly replaced by new colors are not reset. This means that if there are foreground and background before colors and only a foreground after color then even though the foreground color is replaced by the new one the background color is allowed to continue and is not explicitly reset.

Regardless of whether all colors are reset, output of unnecessary codes is avoided. This means, for example, that if any before colors are replaced by new colors of the same category (foreground, background, underline, etc.) then there will never be an explicit reset because that would be redundant and merely add more characters.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/styles/colors.rb', line 142

def self.color_transition(before_colors, after_colors, hard=true)
  before_colors, after_colors = Array(before_colors), Array(after_colors)

  before_categories, after_categories = categorize(before_colors), categorize(after_colors)

  # Nothing to do if before and after colors are the same
  return '' if before_categories == after_categories

  transition = ''
  should_reset = false
  colors_to_apply = []

  # Explicit reset is necessary if we want a hard transition and all colors in all
  # categories are not replaced.
  if hard
    before_categories.each_pair do |cat, before_color|
      next if negative?(before_color)
      after_color = after_categories[cat]
      if !after_color || negative?(after_color)
        should_reset = true
        break
      end
    end
  end

  # If soft transition then the only time we need an explicit reset is when we have a color
  # in a category that is explicitly turned off with a negative value. This also applies
  # to hard transitions.
  unless should_reset
    after_categories.each_pair do |cat, after_color|
      before_color = before_categories[cat]
      if before_color && negative?(after_color) && !negative?(before_color)
        should_reset = true
        break
      end
    end
  end

  after_categories.each_pair do |cat, after_color|
    before_color = before_categories[cat]
    if !negative?(after_color)
      transition << c(after_color) unless before_color == after_color && !should_reset
    end
  end

  # If we are resetting but using a soft transition then all colors execept negated ones
  # need to be set again after the reset.
  if should_reset && !hard
    before_categories.values.each do |color|
      unless negative?(color) || after_categories.values.include?(negate(color))
        transition << c(color) unless after_categories.keys.include?(category(color))
      end
    end
  end

  transition.prepend(c(:reset)) if should_reset
  transition
end

.force_color(string, *colors) ⇒ Object

Apply any valid colors to a string and auto-reset (if any colors applied). If there are any resets in the middle of the string, reapply the colors after them.



79
80
81
82
83
84
85
86
87
88
89
# File 'lib/styles/colors.rb', line 79

def self.force_color(string, *colors)
  return string if string.nil? or string.empty?
  colors.flatten!
  colors.reject! { |col| col == :none || !VALID_VALUES.include?(col) }
  if colors.any?
    codes = colors.map { |col| c(col) }.join
    "#{codes}#{string.gsub(c(:reset), c(:reset) + codes)}#{c(:reset)}"
  else
    string
  end
end

.is_basic_color?(color) ⇒ Boolean

Returns:

  • (Boolean)


95
96
97
# File 'lib/styles/colors.rb', line 95

def self.is_basic_color?(color)
  COLOR_VALUES.include?(color)
end

.is_compound_color?(color) ⇒ Boolean

Returns an array of colors if the given symbol represents a compound color. Returns nil otherwise.

Returns:

  • (Boolean)


101
102
103
104
105
106
107
108
# File 'lib/styles/colors.rb', line 101

def self.is_compound_color?(color)
  if color.to_s =~ /(\w+)_on_(\w+)/
    colors = [$1.to_sym, "on_#{$2}".to_sym]
    if colors.all? { |c| COLOR_VALUES.include? c }
      colors
    end
  end
end

.line_substring_color_transitions(line_colors, substring_colors) ⇒ Object

Gives a pair of color codes for transitions into and out of a colored substring in the middle of a possibly differently colored line.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/styles/colors.rb', line 112

def self.line_substring_color_transitions(line_colors, substring_colors)
  line_colors, substring_colors = Array(line_colors), Array(substring_colors)

  implied_substring_colors = []
  line_colors.each do |line_col|
    cat = category(line_col)
    replaced = substring_colors.any? { |substr_col| category(substr_col) == cat}
    implied_substring_colors << line_col unless replaced
  end

  [
    color_transition(line_colors, substring_colors, false),
    color_transition(substring_colors + implied_substring_colors, line_colors)
  ]
end

.negative?(color) ⇒ Boolean

Returns:

  • (Boolean)


201
202
203
# File 'lib/styles/colors.rb', line 201

def self.negative?(color)
  NEGATIVE_PSEUDO_VALUES.include?(color)
end

.uncolor(string) ⇒ Object



205
206
207
# File 'lib/styles/colors.rb', line 205

def self.uncolor(string)
  ansi_color.uncolor(string)
end

.valid?(color) ⇒ Boolean

Returns:

  • (Boolean)


91
92
93
# File 'lib/styles/colors.rb', line 91

def self.valid?(color)
  is_valid_basic_value?(color) || is_compound_color?(color)
end