# coding: utf-8 #-- # originally color_scheme.rb # # Created by Richard LeBer on 2011-06-27. # Copyright 2011. All rights reserved # # This is Free Software. See LICENSE and COPYING for details class HighLine #:nodoc: # Creates a style using {.find_or_create_style} or # {.find_or_create_style_list} # @param args [Array] style properties # @return [Style] def self.Style(*args) args = args.compact.flatten if args.size == 1 find_or_create_style(args.first) else find_or_create_style_list(*args) end end # Search for a Style with the given properties and return it. # If there's no matched Style, it creates one. # You can pass a Style, String or a Hash. # @param arg [Style, String, Hash] style properties # @return [Style] found or creted Style def self.find_or_create_style(arg) if arg.is_a?(Style) Style.list[arg.name] || Style.index(arg) elsif arg.is_a?(::String) && arg =~ /^\e\[/ # arg is a code styles = Style.code_index[arg] if styles styles.first else Style.new(code: arg) end elsif Style.list[arg] Style.list[arg] elsif HighLine.color_scheme && HighLine.color_scheme[arg] HighLine.color_scheme[arg] elsif arg.is_a?(Hash) Style.new(arg) elsif arg.to_s.downcase =~ /^rgb_([a-f0-9]{6})$/ Style.rgb(Regexp.last_match(1)) elsif arg.to_s.downcase =~ /^on_rgb_([a-f0-9]{6})$/ Style.rgb(Regexp.last_match(1)).on else raise NameError, "#{arg.inspect} is not a defined Style" end end # Find a Style list or create a new one. # @param args [Array] an Array of Symbols of each style # that will be on the style list. # @return [Style] Style list # @example Creating a Style list of the combined RED and BOLD styles. # style_list = HighLine.find_or_create_style_list(:red, :bold) def self.find_or_create_style_list(*args) name = args Style.list[name] || Style.new(list: args) end # ANSI styles to be used by HighLine. class Style # Index the given style. # Uses @code_index (Hash) as repository. # @param style [Style] # @return [Style] the given style def self.index(style) if style.name @styles ||= {} @styles[style.name] = style end unless style.list @code_index ||= {} @code_index[style.code] ||= [] @code_index[style.code].reject! do |indexed_style| indexed_style.name == style.name end @code_index[style.code] << style end style end # Clear all custom Styles, restoring the Style index to # builtin styles only. # @return [void] def self.clear_index # reset to builtin only styles @styles = list.select { |_name, style| style.builtin } @code_index = {} @styles.each_value { |style| index(style) } end # Converts all given color codes to hexadecimal and # join them in a single string. If any given color code # is already a String, doesn't perform any convertion. # # @param colors [Array] color codes # @return [String] all color codes joined # @example # HighLine::Style.rgb_hex(9, 10, "11") # => "090a11" def self.rgb_hex(*colors) colors.map do |color| color.is_a?(Numeric) ? format("%02x", color) : color.to_s end.join end # Split an rgb code string into its 3 numerical compounds. # @param hex [String] rgb code string like "010F0F" # @return [Array] numerical compounds like [1, 15, 15] # @example # HighLine::Style.rgb_parts("090A0B") # => [9, 10, 11] def self.rgb_parts(hex) hex.scan(/../).map { |part| part.to_i(16) } end # Search for or create a new Style from the colors provided. # @param colors (see .rgb_hex) # @return [Style] a Style with the rgb colors provided. # @example Creating a new Style based on rgb code # rgb_style = HighLine::Style.rgb(9, 10, 11) # # rgb_style.name # => :rgb_090a0b # rgb_style.code # => "\e[38;5;16m" # rgb_style.rgb # => [9, 10, 11] # def self.rgb(*colors) hex = rgb_hex(*colors) name = ("rgb_" + hex).to_sym style = list[name] return style if style parts = rgb_parts(hex) new(name: name, code: "\e[38;5;#{rgb_number(parts)}m", rgb: parts) end # Returns the rgb number to be used as escape code on ANSI terminals. # @param parts [Array] three numerical codes for red, green # and blue # @return [Numeric] to be used as escape code on ANSI terminals def self.rgb_number(*parts) parts = parts.flatten 16 + parts.reduce(0) do |kode, part| kode * 6 + (part / 256.0 * 6.0).floor end end # From an ANSI number (color escape code), craft an 'rgb_hex' code of it # @param ansi_number [Integer] ANSI escape code # @return [String] all color codes joined as {.rgb_hex} def self.ansi_rgb_to_hex(ansi_number) raise "Invalid ANSI rgb code #{ansi_number}" unless (16..231).cover?(ansi_number) parts = (ansi_number - 16). to_s(6). rjust(3, "0"). scan(/./). map { |d| (d.to_i * 255.0 / 6.0).ceil } rgb_hex(*parts) end # @return [Hash] list of all cached Styles def self.list @styles ||= {} end # @return [Hash] list of all cached Style codes def self.code_index @code_index ||= {} end # Remove any ANSI color escape sequence of the given String. # @param string [String] # @return [String] def self.uncolor(string) string.gsub(/\e\[\d+(;\d+)*m/, "") end # Style name # @return [Symbol] the name of the Style attr_reader :name # When a compound Style, returns a list of its components. # @return [Array] components of a Style list attr_reader :list # @return [Array] the three numerical rgb codes. Ex: [10, 12, 127] attr_accessor :rgb # @return [Boolean] true if the Style is builtin or not. attr_accessor :builtin # Single color/styles have :name, :code, :rgb (possibly), :builtin # Compound styles have :name, :list, :builtin # # @param defn [Hash] options for the Style to be created. def initialize(defn = {}) @definition = defn @name = defn[:name] @code = defn[:code] @rgb = defn[:rgb] @list = defn[:list] @builtin = defn[:builtin] if @rgb hex = self.class.rgb_hex(@rgb) @name ||= "rgb_" + hex elsif @list @name ||= @list end self.class.index self unless defn[:no_index] end # Duplicate current Style using the same definition used to create it. # @return [Style] duplicated Style def dup self.class.new(@definition) end # @return [Hash] the definition used to create this Style def to_hash @definition end # Uses the Style definition to add ANSI color escape codes # to a a given String # @param string [String] to be colored # @return [String] an ANSI colored string def color(string) code + string + HighLine::CLEAR end # @return [String] all codes of the Style list joined together # (if a Style list) # @return [String] the Style code def code if @list @list.map { |element| HighLine.Style(element).code }.join else @code end end # @return [Integer] the RED component of the rgb code def red @rgb && @rgb[0] end # @return [Integer] the GREEN component of the rgb code def green @rgb && @rgb[1] end # @return [Integer] the BLUE component of the rgb code def blue @rgb && @rgb[2] end # Duplicate Style with some minor changes # @param new_name [Symbol] # @param options [Hash] Style attributes to be changed # @return [Style] new Style with changed attributes def variant(new_name, options = {}) raise "Cannot create a variant of a style list (#{inspect})" if @list new_code = options[:code] || code if options[:increment] raise "Unexpected code in #{inspect}" unless new_code =~ /^(.*?)(\d+)(.*)/ new_code = Regexp.last_match(1) + (Regexp.last_match(2).to_i + options[:increment]).to_s + Regexp.last_match(3) end new_rgb = options[:rgb] || @rgb self.class.new(to_hash.merge(name: new_name, code: new_code, rgb: new_rgb)) end # Uses the color as background and return a new style. # @return [Style] def on new_name = ("on_" + @name.to_s).to_sym self.class.list[new_name] ||= variant(new_name, increment: 10) end # @return [Style] a brighter version of this Style def bright create_bright_variant(:bright) end # @return [Style] a lighter version of this Style def light create_bright_variant(:light) end private def create_bright_variant(variant_name) raise "Cannot create a #{name} variant of a style list (#{inspect})" if @list new_name = ("#{variant_name}_" + @name.to_s).to_sym new_rgb = if @rgb == [0, 0, 0] [128, 128, 128] else @rgb.map { |color| color.zero? ? 0 : [color + 128, 255].min } end find_style(new_name) || variant(new_name, increment: 60, rgb: new_rgb) end def find_style(name) self.class.list[name] end end end