# 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 # 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 if styles = Style.code_index[arg] styles.first else Style.new(:code=>arg) end elsif style = Style.list[arg] style 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($1) elsif arg.to_s.downcase =~ /^on_rgb_([a-f0-9]{6})$/ Style.rgb($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 if !style.list @code_index ||= {} @code_index[style.code] ||= [] @code_index[style.code].reject!{|indexed_style| indexed_style.name == style.name} @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 { |name, 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) ? '%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 if style = list[name] style else parts = rgb_parts(hex) new(:name=>name, :code=>"\e[38;5;#{rgb_number(parts)}m", :rgb=>parts) end 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.inject(0) {|kode, part| kode*6 + (part/256.0*6.0).floor} 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).include?(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 = $1 + ($2.to_i + options[:increment]).to_s + $3 end new_rgb = options[:rgb] || @rgb self.class.new(self.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 = @rgb == [0,0,0] ? [128, 128, 128] : @rgb.map {|color| color==0 ? 0 : [color+128,255].min } find_style(new_name) or variant(new_name, :increment=>60, :rgb=>new_rgb) end def find_style(name) self.class.list[name] end end end