├── .ruby-gemset ├── .ruby-version ├── .rspec ├── lib ├── pastel │ ├── version.rb │ ├── color_resolver.rb │ ├── decorator_chain.rb │ ├── detached.rb │ ├── alias_importer.rb │ ├── ansi.rb │ ├── delegator.rb │ └── color.rb └── pastel.rb ├── assets ├── pastel_logo.png └── screenshot.png ├── Rakefile ├── tasks ├── console.rake ├── coverage.rake └── spec.rake ├── .gitignore ├── spec ├── unit │ ├── color │ │ ├── styles_spec.rb │ │ ├── colored_spec.rb │ │ ├── valid_spec.rb │ │ ├── new_spec.rb │ │ ├── code_spec.rb │ │ ├── equal_spec.rb │ │ ├── alias_color_spec.rb │ │ ├── strip_spec.rb │ │ └── decorate_spec.rb │ ├── respond_to_spec.rb │ ├── alias_color_spec.rb │ ├── alias_importer_spec.rb │ ├── delegator_spec.rb │ ├── decorator_chain_spec.rb │ ├── detach_spec.rb │ └── new_spec.rb └── spec_helper.rb ├── Gemfile ├── benchmarks ├── speed.rb └── nesting_speed.rb ├── .travis.yml ├── pastel.gemspec ├── LICENSE.txt ├── examples └── palette.rb ├── CHANGELOG.md └── README.md /.ruby-gemset: -------------------------------------------------------------------------------- 1 | pastel 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --warnings 4 | -------------------------------------------------------------------------------- /lib/pastel/version.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | module Pastel 4 | VERSION = "0.5.1" 5 | end 6 | -------------------------------------------------------------------------------- /assets/pastel_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/pastel/master/assets/pastel_logo.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/pastel/master/assets/screenshot.png -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "bundler/gem_tasks" 4 | 5 | FileList['tasks/**/*.rake'].each(&method(:import)) 6 | 7 | desc 'Run all specs' 8 | task ci: %w[ spec ] 9 | -------------------------------------------------------------------------------- /tasks/console.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | desc 'Load gem inside irb console' 4 | task :console do 5 | require 'irb' 6 | require 'irb/completion' 7 | require File.join(__FILE__, '../../lib/pastel') 8 | ARGV.clear 9 | IRB.start 10 | end 11 | -------------------------------------------------------------------------------- /tasks/coverage.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | desc 'Measure code coverage' 4 | task :coverage do 5 | begin 6 | original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' 7 | Rake::Task['spec'].invoke 8 | ensure 9 | ENV['COVERAGE'] = original 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | -------------------------------------------------------------------------------- /spec/unit/color/styles_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color do 6 | 7 | subject(:color) { described_class.new(enabled: true) } 8 | 9 | it "exposes all available style ANSI codes" do 10 | expect(color.styles[:red]).to eq(31) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'rake', '~> 10.4.2' 7 | gem 'rspec', '~> 3.3.0' 8 | gem 'yard', '~> 0.8.7' 9 | gem 'benchmark-ips', '~> 2.0.0' 10 | end 11 | 12 | group :metrics do 13 | gem 'coveralls', '~> 0.8.2' 14 | gem 'simplecov', '~> 0.10.0' 15 | gem 'yardstick', '~> 0.9.9' 16 | end 17 | -------------------------------------------------------------------------------- /benchmarks/speed.rb: -------------------------------------------------------------------------------- 1 | require 'pastel' 2 | require 'benchmark/ips' 3 | 4 | pastel = Pastel.new 5 | 6 | Benchmark.ips do |bench| 7 | bench.config(time: 5, warmup: 2) 8 | 9 | bench.report('color decorate') do 10 | pastel.decorate('string', :red, :on_green, :bold) 11 | end 12 | 13 | bench.report('dsl styling') do 14 | pastel.red.on_green.bold('string') 15 | end 16 | 17 | bench.compare! 18 | end 19 | 20 | # color decorate: 14K/s 21 | # dsl styling: 10K/s 22 | -------------------------------------------------------------------------------- /spec/unit/color/colored_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '.colored?' do 6 | subject(:color) { described_class.new(enabled: true) } 7 | 8 | it "checks if string has color codes" do 9 | string = "foo\e[31mbar\e[0m" 10 | expect(color.colored?(string)).to eq(true) 11 | end 12 | 13 | it "checks that string doesn't contain color codes" do 14 | string = "foo\nbar" 15 | expect(color.colored?(string)).to eq(false) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without yard benchmarks 3 | script: "bundle exec rake ci" 4 | rvm: 5 | - 1.9.3 6 | - 2.0 7 | - 2.1 8 | - 2.2 9 | - ruby-head 10 | matrix: 11 | include: 12 | - rvm: jruby-19mode 13 | - rvm: jruby-20mode 14 | - rvm: jruby-21mode 15 | - rvm: jruby-head 16 | - rvm: rbx-2 17 | allow_failures: 18 | - rvm: ruby-head 19 | - rvm: jruby-head 20 | - rvm: jruby-20mode 21 | - rvm: jruby-21mode 22 | fast_finish: true 23 | branches: 24 | only: master 25 | -------------------------------------------------------------------------------- /spec/unit/respond_to_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel, '.respond_to?' do 6 | subject(:pastel) { described_class.new(enabled: true) } 7 | 8 | it "responds correctly to color method" do 9 | expect(pastel.respond_to?(:decorate)).to eq(true) 10 | end 11 | 12 | it "responds correctly to color property" do 13 | expect(pastel.respond_to?(:red)).to eq(true) 14 | end 15 | 16 | it "responds correctly to unkown method" do 17 | expect(pastel.respond_to?(:unknown)).to eq(false) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/color/valid_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '.valid?' do 6 | it "detects valid colors" do 7 | color = described_class.new 8 | expect(color.valid?(:red, :on_green, :bold)).to eq(true) 9 | end 10 | 11 | it "detects valid color aliases" do 12 | color = described_class.new 13 | color.alias_color(:funky, :red) 14 | expect(color.valid?(:funky)).to eq(true) 15 | end 16 | 17 | it "detects invalid color" do 18 | color = described_class.new 19 | expect(color.valid?(:red, :unknown)).to eq(false) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /benchmarks/nesting_speed.rb: -------------------------------------------------------------------------------- 1 | require 'pastel' 2 | require 'benchmark/ips' 3 | 4 | pastel = Pastel.new 5 | 6 | Benchmark.ips do |bench| 7 | bench.config(time: 5, warmup: 2) 8 | 9 | bench.report('regular nesting') do 10 | pastel.red.on_green('Unicorns' + 11 | pastel.green.on_red('will ', 'dominate' + pastel.yellow('the world!'))) 12 | end 13 | 14 | bench.report('block nesting') do 15 | pastel.red.on_green('Unicorns') do 16 | green.on_red('will ', 'dominate') do 17 | yellow('the world!') 18 | end 19 | end 20 | end 21 | 22 | bench.compare! 23 | end 24 | 25 | # regular nesting: 2800/s 26 | # block nesting: 2600/s 27 | -------------------------------------------------------------------------------- /spec/unit/color/new_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '.new' do 6 | it "is immutable" do 7 | expect(described_class.new).to be_frozen 8 | end 9 | 10 | it "allows to disable coloring" do 11 | color = described_class.new(enabled: false) 12 | 13 | expect(color.enabled?).to eq(false) 14 | expect(color.decorate("Unicorn", :red)).to eq("Unicorn") 15 | end 16 | 17 | it "invokes screen dependency to check color support" do 18 | allow(TTY::Screen).to receive(:color?).and_return(true) 19 | color = described_class.new 20 | 21 | expect(color.enabled?).to eq(true) 22 | expect(TTY::Screen).to have_received(:color?) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/unit/color/code_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '.code' do 6 | let(:string) { "This is a \e[1m\e[34mbold blue text\e[0m" } 7 | 8 | subject(:color) { described_class.new(enabled: true) } 9 | 10 | it 'finds single code' do 11 | expect(color.code(:black)).to eq([30]) 12 | end 13 | 14 | it 'finds more than one code' do 15 | expect(color.code(:black, :green)).to eq([30, 32]) 16 | end 17 | 18 | it "doesn't find code" do 19 | expect { color.code(:unkown) }.to raise_error(ArgumentError) 20 | end 21 | 22 | it "finds alias code" do 23 | color.alias_color(:funky, :red) 24 | expect(color.code(:funky)).to eq(color.code(:red)) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/pastel/color_resolver.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | module Pastel 4 | # Contains logic for resolving styles applied to component 5 | # 6 | # Used internally by {Delegator}. 7 | # 8 | # @api private 9 | class ColorResolver 10 | attr_reader :color 11 | 12 | # Initialize ColorResolver 13 | # 14 | # @param [Color] color 15 | # 16 | # @api private 17 | def initialize(color = Color.new) 18 | @color = color 19 | end 20 | 21 | # Resolve uncolored string 22 | # 23 | # @api private 24 | def resolve(base, unprocessed_string) 25 | if base.to_a.last == :detach 26 | Detached.new(color, *base.to_a[0...-1]) 27 | else 28 | color.decorate(unprocessed_string, *base) 29 | end 30 | end 31 | end # ColorResolver 32 | end # Pastel 33 | -------------------------------------------------------------------------------- /tasks/spec.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | desc 'Run all specs' 7 | RSpec::Core::RakeTask.new(:spec) do |task| 8 | task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb' 9 | end 10 | 11 | namespace :spec do 12 | desc 'Run unit specs' 13 | RSpec::Core::RakeTask.new(:unit) do |task| 14 | task.pattern = 'spec/unit{,/*/**}/*_spec.rb' 15 | end 16 | 17 | desc 'Run integration specs' 18 | RSpec::Core::RakeTask.new(:integration) do |task| 19 | task.pattern = 'spec/integration{,/*/**}/*_spec.rb' 20 | end 21 | end 22 | 23 | rescue LoadError 24 | %w[spec spec:unit spec:integration].each do |name| 25 | task name do 26 | $stderr.puts "In order to run #{name}, do `gem install rspec`" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/alias_color_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel, '.alias_color' do 6 | 7 | subject(:pastel) { described_class.new(enabled: true) } 8 | 9 | it "aliases color" do 10 | pastel.alias_color(:funky, :red) 11 | expect(pastel.funky('unicorn')).to eq("\e[31municorn\e[0m") 12 | end 13 | 14 | it "aliases color and combines with regular ones" do 15 | pastel.alias_color(:funky, :red) 16 | expect(pastel.funky.on_green('unicorn')).to eq("\e[31;42municorn\e[0m") 17 | end 18 | 19 | it "reads aliases from the environment" do 20 | color_aliases = "funky=red" 21 | allow(ENV).to receive(:[]).with('PASTEL_COLORS_ALIASES'). 22 | and_return(color_aliases) 23 | described_class.new(enabled: true) 24 | expect(pastel.valid?(:funky)).to eq(true) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/unit/color/equal_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '#==' do 6 | it "is true with the same enabled and eachline attributes" do 7 | expect(Pastel::Color.new(enabled: false, eachline: "\n")). 8 | to eq(Pastel::Color.new(enabled: false, eachline: "\n")) 9 | end 10 | 11 | it "is false with different enabled attribute" do 12 | expect(Pastel::Color.new(enabled: true, eachline: "\n")). 13 | not_to eq(Pastel::Color.new(enabled: false, eachline: "\n")) 14 | end 15 | 16 | it "is false with different eachline attribute" do 17 | expect(Pastel::Color.new(enabled: false, eachline: "\n")). 18 | not_to eq(Pastel::Color.new(enabled: false, eachline: "\r\n")) 19 | end 20 | 21 | it "is false with non-color" do 22 | expect(Pastel::Color.new(enabled: true)).not_to eq(:other) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/pastel/decorator_chain.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | module Pastel 4 | # Collects a list of decorators for styling a string 5 | # 6 | # @api private 7 | class DecoratorChain 8 | include Enumerable 9 | include Equatable 10 | 11 | def initialize(decorators = []) 12 | @decorators = decorators 13 | end 14 | 15 | # Add decorator 16 | # 17 | # @api public 18 | def add(decorator) 19 | self.class.new(decorators + [decorator]) 20 | end 21 | 22 | # Iterate over list of decorators 23 | # 24 | # @api public 25 | def each(&block) 26 | decorators.each(&block) 27 | end 28 | 29 | # Create an empty decorator chain 30 | # 31 | # @return [DecoratorChain] 32 | # 33 | # @api public 34 | def self.empty 35 | new([]) 36 | end 37 | 38 | protected 39 | 40 | attr_reader :decorators 41 | end # DecoratorChain 42 | end # Patel 43 | -------------------------------------------------------------------------------- /pastel.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'pastel/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "pastel" 8 | spec.version = Pastel::VERSION 9 | spec.authors = ["Piotr Murach"] 10 | spec.email = [""] 11 | spec.summary = %q{Terminal strings styling with intuitive and clean API.} 12 | spec.description = %q{Terminal strings styling with intuitive and clean API.} 13 | spec.homepage = "https://github.com/peter-murach/pastel" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "equatable", "~> 0.5" 22 | spec.add_dependency "tty-screen", "~> 0.4" 23 | 24 | spec.add_development_dependency "bundler", "~> 1.6" 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/alias_importer_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::AliasImporter, '.import' do 6 | let(:color) { spy(:color, alias_color: true) } 7 | let(:output) { StringIO.new } 8 | 9 | subject(:importer) { described_class.new(color, output) } 10 | 11 | it "imports aliases from environment" do 12 | color_aliases = "funky=red,base=bright_yellow" 13 | allow(ENV).to receive(:[]).with('PASTEL_COLORS_ALIASES'). 14 | and_return(color_aliases) 15 | 16 | importer.import 17 | 18 | expect(color).to have_received(:alias_color).twice 19 | end 20 | 21 | it "fails to import incorrectly formatted colors" do 22 | color_aliases = "funky red,base=bright_yellow" 23 | allow(ENV).to receive(:[]).with('PASTEL_COLORS_ALIASES'). 24 | and_return(color_aliases) 25 | 26 | importer.import 27 | 28 | output.rewind 29 | expect(output.string).to eq("Bad color mapping `funky red`\n") 30 | expect(color).to have_received(:alias_color).with(:base, :bright_yellow) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/pastel.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'forwardable' 4 | require 'equatable' 5 | require 'tty-screen' 6 | 7 | require 'pastel/ansi' 8 | require 'pastel/alias_importer' 9 | require 'pastel/color' 10 | require 'pastel/color_resolver' 11 | require 'pastel/delegator' 12 | require 'pastel/detached' 13 | require 'pastel/decorator_chain' 14 | require 'pastel/version' 15 | 16 | module Pastel 17 | # Raised when the style attribute is not supported 18 | InvalidAttributeNameError = Class.new(::ArgumentError) 19 | 20 | # Raised when the color alias is not supported 21 | InvalidAliasNameError = Class.new(::ArgumentError) 22 | 23 | # Create Pastel chainable API 24 | # 25 | # @example 26 | # pastel = Pastel.new enabled: true 27 | # 28 | # @return [Delegator] 29 | # 30 | # @api public 31 | def new(options = {}) 32 | color = Color.new(options) 33 | importer = AliasImporter.new(color) 34 | importer.import 35 | resolver = ColorResolver.new(color) 36 | Delegator.for(resolver, DecoratorChain.empty) 37 | end 38 | 39 | module_function :new 40 | end # Pastel 41 | -------------------------------------------------------------------------------- /lib/pastel/detached.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Pastel 4 | # A class representing detached color 5 | class Detached 6 | include Equatable 7 | 8 | # Initialize a detached object 9 | # 10 | # @param [Pastel::Color] color 11 | # the color instance 12 | # @param [Array[Symbol]] styles 13 | # the styles to be applied 14 | # 15 | # @api private 16 | def initialize(color, *styles) 17 | @color = color 18 | @styles = styles.dup 19 | freeze 20 | end 21 | 22 | # Decorate the values corresponding to styles 23 | # 24 | # @example 25 | # 26 | # @param [String] value 27 | # the stirng to decorate with styles 28 | # 29 | # @return [String] 30 | # 31 | # @api public 32 | def call(*args) 33 | value = args.join 34 | @color.decorate(value, *styles) 35 | end 36 | alias_method :[], :call 37 | 38 | # @api public 39 | def to_proc 40 | self 41 | end 42 | 43 | private 44 | 45 | # @api private 46 | attr_reader :styles 47 | end # Detached 48 | end # Pastel 49 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Piotr Murach 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/pastel/alias_importer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Pastel 4 | # A class responsible for importing color aliases 5 | class AliasImporter 6 | # Create alias importer 7 | # 8 | # @example 9 | # importer = Pastel::AliasImporter.new(Pastel::Color.new) 10 | # 11 | # @api public 12 | def initialize(color, output = $stderr) 13 | @color = color 14 | @output = output 15 | end 16 | 17 | # Import aliases from the environment 18 | # 19 | # @example 20 | # importer = Pastel::AliasImporter.new(Pastel::Color.new) 21 | # importer.import 22 | # 23 | # @return [nil] 24 | # 25 | # @api public 26 | def import 27 | color_aliases = ENV['PASTEL_COLORS_ALIASES'] 28 | return unless color_aliases 29 | color_aliases.split(',').each do |color_alias| 30 | new_color, old_color = color_alias.split('=').map(&:to_sym) 31 | if !new_color || !old_color 32 | output.puts "Bad color mapping `#{color_alias}`" 33 | else 34 | color.alias_color(new_color, old_color) 35 | end 36 | end 37 | end 38 | 39 | protected 40 | 41 | attr_reader :color, :output 42 | end # AliasImporter 43 | end # Pastel 44 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | if RUBY_VERSION > '1.9' and (ENV['COVERAGE'] || ENV['TRAVIS']) 4 | require 'simplecov' 5 | require 'coveralls' 6 | 7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 8 | SimpleCov::Formatter::HTMLFormatter, 9 | Coveralls::SimpleCov::Formatter 10 | ] 11 | 12 | SimpleCov.start do 13 | command_name 'spec' 14 | add_filter 'spec' 15 | end 16 | end 17 | 18 | require 'pastel' 19 | 20 | RSpec.configure do |config| 21 | config.expect_with :rspec do |expectations| 22 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 23 | end 24 | 25 | config.mock_with :rspec do |mocks| 26 | mocks.verify_partial_doubles = true 27 | end 28 | 29 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 30 | config.disable_monkey_patching! 31 | 32 | # This setting enables warnings. It's recommended, but in some cases may 33 | # be too noisy due to issues in dependencies. 34 | config.warnings = true 35 | 36 | if config.files_to_run.one? 37 | config.default_formatter = 'doc' 38 | end 39 | 40 | config.profile_examples = 2 41 | 42 | config.order = :random 43 | 44 | Kernel.srand config.seed 45 | end 46 | -------------------------------------------------------------------------------- /examples/palette.rb: -------------------------------------------------------------------------------- 1 | require_relative 'lib/pastel' 2 | 3 | pastel = Pastel.new 4 | 5 | puts pastel.bold('bold ') + ' ' + pastel.dim('dim ') + ' ' + pastel.italic('italic ') + ' ' + pastel.underline('underline') + ' ' + pastel.inverse('inverse ') + ' ' + pastel.strikethrough('strikethrough') 6 | 7 | puts pastel.red('red ') + ' ' + pastel.green('green ') + ' ' + pastel.yellow('yellow ') + ' ' + pastel.blue('blue ') + ' ' + pastel.magenta('magenta ') + ' ' + pastel.cyan('cyan ') + ' ' + pastel.white('white') 8 | 9 | puts pastel.bright_red('red ') + ' ' + pastel.bright_green('green ') + ' ' + pastel.bright_yellow('yellow ') + ' ' + pastel.bright_blue('blue ') + ' ' + pastel.bright_magenta('magenta ') + ' ' + pastel.bright_cyan('cyan ') + ' ' + pastel.bright_white('white') 10 | 11 | 12 | puts pastel.on_red('on_red') + ' ' + pastel.on_green('on_green') + ' ' + pastel.on_yellow('on_yellow') + ' ' + pastel.on_blue('on_blue') + ' ' + pastel.on_magenta('on_magenta') + ' ' + pastel.on_cyan('on_cyan') + ' ' + pastel.on_white('on_white') 13 | 14 | puts pastel.on_bright_red('on_red') + ' ' + pastel.on_bright_green('on_green') + ' ' + pastel.on_bright_yellow('on_yellow') + ' ' + pastel.on_bright_blue('on_blue') + ' ' + pastel.on_bright_magenta('on_magenta') + ' ' + pastel.on_bright_cyan('on_cyan') + ' ' + pastel.on_bright_white('on_white') 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.5.1 (Sept 18, 2015) 2 | 3 | * Add ability to call detached instance with array access 4 | 5 | 0.5.0 (Sept 13, 2015) 6 | 7 | * Add external dependency to check for color support 8 | * Add #colored? to check if string has color escape codes 9 | * Add #eachline option to allow coloring of multiline strings 10 | * Further refine #strip method accuracy 11 | * Fix redefining inspect method 12 | * Fix string representation for pastel instance 13 | 14 | 0.4.0 (November 22, 2014) 15 | 16 | * Fix Delegator#respond_to method to correctly report existence of methods 17 | * Add ability to #detach color combination for later reuse 18 | * Add ability to nest styles with blocks 19 | 20 | 0.3.0 (November 8, 2014) 21 | 22 | * Add ability to alias colors through #alias_color method 23 | * Add ability to alias colors through the environment variable 24 | * Improve performance of Pastel::Color styles and lookup methods 25 | * Fix bug concerned with lack of escaping for nested styles 26 | 27 | 0.2.1 (October 13, 2014) 28 | 29 | * Fix issue #1 with unitialize dependency 30 | 31 | 0.2.0 (October 12, 2014) 32 | 33 | * Change gemspec to include equatable as dependency 34 | * Add #supports? to Color to check for terminal color support 35 | * Add ability to force color support through :enabled option 36 | * Change Delegator to stop creating instances and improve performance 37 | -------------------------------------------------------------------------------- /spec/unit/delegator_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Delegator do 6 | 7 | it "returns delegator for color without argument" do 8 | pastel = Pastel.new(enabled: true) 9 | expect(pastel.red).to be_a(Pastel::Delegator) 10 | end 11 | 12 | describe ".inspect" do 13 | it "inspects delegator styles chain" do 14 | chain = ['red', 'on_green'] 15 | delegator = described_class.new(:resolver, chain) 16 | allow(delegator).to receive(:styles).and_return({red: 31, on_green: 42}) 17 | expect(delegator.inspect).to eq("#") 18 | end 19 | end 20 | 21 | describe ".respond_to_missing?" do 22 | context 'for a method defined on' do 23 | it "returns true" do 24 | resolver = double(:resolver) 25 | chain = double(:chain) 26 | decorator = described_class.new(resolver, chain) 27 | expect(decorator.method(:styles)).not_to be_nil 28 | end 29 | end 30 | 31 | context "for an undefined method " do 32 | it "returns false" do 33 | resolver = double(:resolver, color: true) 34 | chain = double(:chain) 35 | decorator = described_class.new(resolver, chain) 36 | expect { decorator.method(:unknown) }.to raise_error(NameError) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/unit/color/alias_color_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '.alias_color' do 6 | 7 | subject(:color) { described_class.new(enabled: true) } 8 | 9 | it 'aliases non existent color' do 10 | expect { 11 | color.alias_color(:funky, :unknown) 12 | }.to raise_error(Pastel::InvalidAttributeNameError) 13 | end 14 | 15 | it 'aliases color with invalid name' do 16 | expect { 17 | color.alias_color('some name', :red) 18 | }.to raise_error(Pastel::InvalidAliasNameError, /Invalid alias name/) 19 | end 20 | 21 | it 'aliases standard color' do 22 | expect { 23 | color.alias_color(:red, :red) 24 | }.to raise_error(Pastel::InvalidAliasNameError, /alias standard color/) 25 | end 26 | 27 | it 'aliases color :red to :funky' do 28 | color.alias_color(:funky, :red) 29 | expect(color.valid?(:funky)).to eq(true) 30 | expect(color.code(:funky)).to eq([31]) 31 | expect(color.lookup(:funky)).to eq("\e[31m") 32 | end 33 | 34 | it "has global aliases" do 35 | color_foo = described_class.new(enabled: true) 36 | color_bar = described_class.new(enabled: true) 37 | color_foo.alias_color(:foo, :red) 38 | color_bar.alias_color(:bar, :red) 39 | expect(color_foo.valid?(:foo)).to eq(true) 40 | expect(color_foo.valid?(:bar)).to eq(true) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/pastel/ansi.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Pastel 4 | # Mixin that provides ANSI codes 5 | module ANSI 6 | ATTRIBUTES = { 7 | clear: 0, 8 | reset: 0, 9 | bold: 1, 10 | dark: 2, 11 | dim: 2, 12 | italic: 3, 13 | underline: 4, 14 | underscore: 4, 15 | inverse: 7, 16 | hidden: 8, 17 | strikethrough: 9, 18 | 19 | black: 30, 20 | red: 31, 21 | green: 32, 22 | yellow: 33, 23 | blue: 34, 24 | magenta: 35, 25 | cyan: 36, 26 | white: 37, 27 | 28 | on_black: 40, 29 | on_red: 41, 30 | on_green: 42, 31 | on_yellow: 43, 32 | on_blue: 44, 33 | on_magenta: 45, 34 | on_cyan: 46, 35 | on_white: 47, 36 | 37 | bright_black: 90, 38 | bright_red: 91, 39 | bright_green: 92, 40 | bright_yellow: 93, 41 | bright_blue: 94, 42 | bright_magenta: 95, 43 | bright_cyan: 96, 44 | bright_white: 97, 45 | 46 | on_bright_black: 100, 47 | on_bright_red: 101, 48 | on_bright_green: 102, 49 | on_bright_yellow: 103, 50 | on_bright_blue: 104, 51 | on_bright_magenta: 105, 52 | on_bright_cyan: 106, 53 | on_bright_white: 107 54 | } 55 | end # ANSI 56 | end # Pastel 57 | -------------------------------------------------------------------------------- /spec/unit/decorator_chain_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::DecoratorChain do 6 | it "is enumerable" do 7 | expect(described_class.new).to be_a(Enumerable) 8 | end 9 | 10 | it "is equatable" do 11 | expect(described_class.new).to be_a(Equatable) 12 | end 13 | 14 | describe ".each" do 15 | it "yields each decorator" do 16 | first = double('first') 17 | second = double('second') 18 | chain = described_class.new.add(first).add(second) 19 | yielded = [] 20 | 21 | expect { 22 | chain.each { |decorator| yielded << decorator } 23 | }.to change { yielded }.from([]).to([first, second]) 24 | end 25 | end 26 | 27 | describe ".==" do 28 | it "is equivalent with the same decorator" do 29 | expect(described_class.new.add(:foo).add(:bar)). 30 | to eq(described_class.new.add(:foo).add(:bar)) 31 | end 32 | 33 | it "is not equivalent with different decorator" do 34 | expect(described_class.new.add(:foo).add(:bar)). 35 | not_to eq(described_class.new.add(:foo).add(:baz)) 36 | end 37 | 38 | it "is not equivalent to another type" do 39 | expect(described_class.new.add(:foo).add(:bar)). 40 | not_to eq(:other) 41 | end 42 | end 43 | 44 | describe ".inspect" do 45 | it "displays object information" do 46 | expect(described_class.new.inspect).to match(/decorators=\[\]/) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/unit/detach_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel, '.detach' do 6 | 7 | subject(:pastel) { described_class.new(enabled: true) } 8 | 9 | it "creates detached instance" do 10 | error = pastel.red.bold.detach 11 | expect(error).to be_a(Pastel::Detached) 12 | end 13 | 14 | it "ensures instance is immutable" do 15 | error = pastel.red.detach 16 | expect(error.frozen?).to be(true) 17 | end 18 | 19 | it "detaches colors combination" do 20 | error = pastel.red.bold.detach 21 | expect(error.call('unicorn')).to eq("\e[31;1municorn\e[0m") 22 | expect(error.call('error')).to eq("\e[31;1merror\e[0m") 23 | end 24 | 25 | it "allows array like access" do 26 | error = pastel.red.bold.detach 27 | expect(error['unicorn']).to eq("\e[31;1municorn\e[0m") 28 | end 29 | 30 | it "allows alternative call invocation" do 31 | error = pastel.red.bold.detach 32 | expect(error.('unicorn')).to eq("\e[31;1municorn\e[0m") 33 | end 34 | 35 | it "calls detached colors with no arguments" do 36 | warning = pastel.yellow.detach 37 | expect(warning.call('')).to eq('') 38 | end 39 | 40 | it "inspects detached colors" do 41 | warning = pastel.yellow.bold.detach 42 | expect(warning.inspect).to eq('#') 43 | end 44 | 45 | it "accepts multiple strings" do 46 | error = pastel.red.bold.detach 47 | expect(error.call('Unicorns', ' run ', 'wild')). 48 | to eq("\e[31;1mUnicorns run wild\e[0m") 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/unit/color/strip_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '.strip' do 6 | 7 | subject(:color) { described_class.new(enabled: true) } 8 | 9 | it 'strips ansi color from string' do 10 | string = "This is a \e[1m\e[34mbold blue text\e[0m" 11 | expect(color.strip(string)).to eq('This is a bold blue text') 12 | end 13 | 14 | it "strips partial ansi color" do 15 | string = "foo\e[1mbar" 16 | expect(color.strip(string)).to eq('foobar') 17 | end 18 | 19 | it 'preserves movement characters' do 20 | # [176A - move cursor up n lines 21 | expect(color.strip("foo\e[176Abar")).to eq('foobar') 22 | end 23 | 24 | it 'strips reset/setfg/setbg/italics/strike/underline sequence' do 25 | string = "\x1b[0;33;49;3;9;4mfoo\x1b[0m" 26 | expect(color.strip(string)).to eq("foo") 27 | end 28 | 29 | it 'strips octal in encapsulating brackets' do 30 | string = "\[\033[01;32m\]u@h \[\033[01;34m\]W $ \[\033[00m\]" 31 | expect(color.strip(string)).to eq('u@h W $ ') 32 | end 33 | 34 | it 'strips octal codes without brackets' do 35 | string = "\033[01;32mu@h \033[01;34mW $ \033[00m" 36 | expect(color.strip(string)).to eq('u@h W $ ') 37 | end 38 | 39 | it 'strips octal with multiple colors' do 40 | string = "\e[3;0;0;tfoo\e[8;50;0t" 41 | expect(color.strip(string)).to eq('foo') 42 | end 43 | 44 | it "strips multiple colors delimited by :" do 45 | string = "\e[31:44:4mfoo\e[0m" 46 | expect(color.strip(string)).to eq('foo') 47 | end 48 | 49 | it 'strips control codes' do 50 | string = "WARN. \x1b[1m&\x1b[0m ERR. \x1b[7m&\x1b[0m" 51 | expect(color.strip(string)).to eq('WARN. & ERR. &') 52 | end 53 | 54 | it 'strips escape bytes' do 55 | string = "This is a \e[1m\e[34mbold blue text\e[0m" 56 | expect(color.strip(string)).to eq("This is a bold blue text") 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/unit/color/decorate_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel::Color, '.decorate' do 6 | let(:string) { 'string' } 7 | 8 | subject(:color) { described_class.new(enabled: true) } 9 | 10 | it "doesn't output styling when disabled" do 11 | color = described_class.new(enabled: false) 12 | expect(color.decorate('foo', :red)).to eq('foo') 13 | end 14 | 15 | it "doesn't apply styling to empty string" do 16 | expect(color.decorate('')).to eq('') 17 | end 18 | 19 | it 'applies green text to string' do 20 | expect(color.decorate(string, :green)).to eq("\e[32m#{string}\e[0m") 21 | end 22 | 23 | it 'applies red text background to string' do 24 | expect(color.decorate(string, :on_red)).to eq("\e[41m#{string}\e[0m") 25 | end 26 | 27 | it 'applies style and color to string' do 28 | expect(color.decorate(string, :bold, :green)).to eq("\e[1;32m#{string}\e[0m") 29 | end 30 | 31 | it 'applies style, color and background to string' do 32 | text = color.decorate(string, :bold, :green, :on_blue) 33 | expect(text).to eq("\e[1;32;44m#{string}\e[0m") 34 | end 35 | 36 | it "applies styles to nested text" do 37 | decorated = color.decorate(string + color.decorate(string, :red) + string, :green) 38 | expect(decorated).to eq("\e[32m#{string}\e[31m#{string}\e[32m#{string}\e[0m") 39 | end 40 | 41 | it "decorates multiline string as regular by default" do 42 | string = "foo\nbar\nbaz" 43 | expect(color.decorate(string, :red)).to eq("\e[31mfoo\nbar\nbaz\e[0m") 44 | end 45 | 46 | it "allows to decorate each line separately" do 47 | string = "foo\nbar\nbaz" 48 | color = described_class.new(enabled: true, eachline: "\n") 49 | expect(color.decorate(string, :red)).to eq([ 50 | "\e[31mfoo\e[0m", 51 | "\e[31mbar\e[0m", 52 | "\e[31mbaz\e[0m" 53 | ].join("\n")) 54 | end 55 | 56 | it 'errors for unknown color' do 57 | expect { 58 | color.decorate(string, :crimson) 59 | }.to raise_error(Pastel::InvalidAttributeNameError) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/pastel/delegator.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | module Pastel 4 | # Wrapes the {DecoratorChain} to allow for easy resolution 5 | # of string coloring. 6 | # 7 | # @api private 8 | class Delegator 9 | extend Forwardable 10 | include Equatable 11 | 12 | def_delegators '@resolver.color', :valid?, :styles, :strip, :decorate, 13 | :enabled?, :colored?, :alias_color 14 | 15 | # Create Delegator 16 | # 17 | # Used internally by {Pastel} 18 | # 19 | # @param [ColorResolver] resolver 20 | # 21 | # @param [DecoratorChain] base 22 | # 23 | # @api private 24 | def initialize(resolver, base) 25 | @resolver = resolver 26 | @base = base 27 | end 28 | 29 | # @api public 30 | def self.for(resolver, base) 31 | new(resolver, base) 32 | end 33 | 34 | remove_method :inspect 35 | 36 | # Object string representation 37 | # 38 | # @return [String] 39 | # 40 | # @api 41 | def inspect 42 | "#" 43 | end 44 | alias_method :to_s, :inspect 45 | 46 | protected 47 | 48 | attr_reader :base 49 | 50 | attr_reader :resolver 51 | 52 | # Wrap colors 53 | # 54 | # @api private 55 | def wrap(base) 56 | self.class.new(resolver, base) 57 | end 58 | 59 | def method_missing(method_name, *args, &block) 60 | new_base = base.add(method_name) 61 | delegator = wrap(new_base) 62 | if args.empty? && !(method_name.to_sym == :detach) 63 | delegator 64 | else 65 | string = args.join 66 | string << evaluate_block(&block) if block_given? 67 | resolver.resolve(new_base, string) 68 | end 69 | end 70 | 71 | def respond_to_missing?(name, include_all = false) 72 | resolver.color.respond_to?(name, include_all) || valid?(name) || super 73 | end 74 | 75 | # Evaluate color block 76 | # 77 | # @api private 78 | def evaluate_block(&block) 79 | delegator = self.class.new(resolver, DecoratorChain.empty) 80 | delegator.instance_eval(&block) 81 | end 82 | end # Delegator 83 | end # Pastel 84 | -------------------------------------------------------------------------------- /spec/unit/new_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pastel do 6 | 7 | subject(:pastel) { described_class.new(enabled: true) } 8 | 9 | describe 'coloring string' do 10 | it "doesn't apply styles to empty string" do 11 | expect(pastel.red('')).to eq('') 12 | end 13 | 14 | it "colors string" do 15 | expect(pastel.red("unicorn")).to eq("\e[31municorn\e[0m") 16 | end 17 | 18 | it "allows to specify variable number of arguments" do 19 | expect(pastel.red("unicorn", "running")).to eq("\e[31municornrunning\e[0m") 20 | end 21 | 22 | it "combines colored strings with regular ones" do 23 | expect(pastel.red("Unicorns") + ' will rule ' + pastel.green('the World!')). 24 | to eq("\e[31mUnicorns\e[0m will rule \e[32mthe World!\e[0m") 25 | end 26 | 27 | it "composes two color strings " do 28 | expect(pastel.red.on_green("unicorn")).to eq("\e[31;42municorn\e[0m") 29 | end 30 | 31 | it "composes three color strings" do 32 | expect(pastel.red.on_green.underline("unicorn")). 33 | to eq("\e[31;42;4municorn\e[0m") 34 | end 35 | 36 | it "combines colored composed strings with regular ones" do 37 | expect(pastel.red.on_green("Unicorns") + ' will rule ' + 38 | pastel.green.on_red('the World!')). 39 | to eq("\e[31;42mUnicorns\e[0m will rule \e[32;41mthe World!\e[0m") 40 | end 41 | 42 | it "allows one level nesting" do 43 | expect(pastel.red("Unicorn" + pastel.blue("rule!"))). 44 | to eq("\e[31mUnicorn\e[34mrule!\e[0m") 45 | end 46 | 47 | it "allows to nest mixed styles" do 48 | expect(pastel.red("Unicorn" + pastel.green.on_yellow.underline('running') + '!')). 49 | to eq("\e[31mUnicorn\e[32;43;4mrunning\e[31m!\e[0m") 50 | end 51 | 52 | it "allows for deep nesting" do 53 | expect(pastel.red('r' + pastel.green('g' + pastel.yellow('y') + 'g') + 'r')). 54 | to eq("\e[31mr\e[32mg\e[33my\e[32mg\e[31mr\e[0m") 55 | end 56 | 57 | it "allows for variable nested arguments" do 58 | expect(pastel.red('r', pastel.green('g'), 'r')). 59 | to eq("\e[31mr\e[32mg\e[31mr\e[0m") 60 | end 61 | 62 | it "allows to nest styles within block" do 63 | string = pastel.red.on_green('Unicorns' + 64 | pastel.green.on_red('will ', 'dominate' + pastel.yellow('the world!'))) 65 | 66 | expect(pastel.red.on_green('Unicorns') do 67 | green.on_red('will ', 'dominate') do 68 | yellow('the world!') 69 | end 70 | end).to eq(string) 71 | end 72 | 73 | it "raises error when chained with unrecognized color" do 74 | expect { 75 | pastel.unknown.on_red('unicorn') 76 | }.to raise_error(Pastel::InvalidAttributeNameError) 77 | end 78 | 79 | it "raises error when doesn't recognize color" do 80 | expect { 81 | pastel.unknown('unicorn') 82 | }.to raise_error(Pastel::InvalidAttributeNameError) 83 | end 84 | end 85 | 86 | describe '.valid?' do 87 | it "when valid returns true" do 88 | expect(pastel.valid?(:red)).to eq(true) 89 | end 90 | 91 | it "returns false when invalid" do 92 | expect(pastel.valid?(:unknown)).to eq(false) 93 | end 94 | end 95 | 96 | describe '.colored?' do 97 | it "checks if string is colored" do 98 | expect(pastel.colored?("\e[31mfoo\e[0m")).to eq(true) 99 | end 100 | end 101 | 102 | describe 'options passed in' do 103 | it "receives enabled option" do 104 | pastel = described_class.new(enabled: false) 105 | expect(pastel.enabled?).to eq(false) 106 | expect(pastel.red('Unicorn', pastel.green('!'))).to eq('Unicorn!') 107 | end 108 | 109 | it "sets eachline option" do 110 | pastel = described_class.new(enabled: true, eachline: "\n") 111 | expect(pastel.red("foo\nbar")).to eq("\e[31mfoo\e[0m\n\e[31mbar\e[0m") 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/pastel/color.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Pastel 4 | # A class responsible for coloring strings. 5 | class Color 6 | include Equatable 7 | include ANSI 8 | 9 | ALIASES = {} 10 | 11 | ANSI_REGEX = /(\[)?\033(\[)?[:;?\d]*[\dA-Za-z](\])?/.freeze 12 | 13 | attr_reader :enabled 14 | alias_method :enabled?, :enabled 15 | 16 | attr_reader :eachline 17 | 18 | # Initialize a Terminal Color 19 | # 20 | # @api public 21 | def initialize(options = {}) 22 | @enabled = options.fetch(:enabled) { TTY::Screen.color? } 23 | @eachline = options.fetch(:eachline) { false } 24 | freeze 25 | end 26 | 27 | # Disable coloring of this terminal session 28 | # 29 | # @api public 30 | def disable! 31 | @enabled = false 32 | end 33 | 34 | # Apply ANSI color to the given string. 35 | # 36 | # @param [String] string 37 | # text to add ANSI strings 38 | # 39 | # @param [Array[Symbol]] colors 40 | # the color names 41 | # 42 | # @example 43 | # color.decorate "text", :yellow, :on_green, :underline 44 | # 45 | # @return [String] 46 | # the colored string 47 | # 48 | # @api public 49 | def decorate(string, *colors) 50 | return string if string.empty? || !enabled 51 | 52 | ansi_colors = lookup(*colors) 53 | ansi_string = wrap_eachline(string, ansi_colors) 54 | ansi_string = nest_color(collapse_reset(ansi_string), ansi_colors) 55 | ansi_string 56 | end 57 | 58 | # Reset sequence 59 | # 60 | # @api public 61 | def clear 62 | lookup(:clear) 63 | end 64 | 65 | # Wraps eachline with clear character 66 | # 67 | # @param [String] string 68 | # string to wrap with multiline characters 69 | # 70 | # @param [String] ansi_colors 71 | # colors to apply to string 72 | # 73 | # @return [String] 74 | # 75 | # @api private 76 | def wrap_eachline(string, ansi_colors) 77 | if eachline 78 | string.split(eachline).map do |line| 79 | "#{ansi_colors}#{line}#{clear}" 80 | end.join(eachline) 81 | else 82 | "#{ansi_colors}#{string}#{clear}" 83 | end 84 | end 85 | 86 | # Collapse reset 87 | # 88 | # @param [String] string 89 | # the string to remove duplicates from 90 | # 91 | # @return [String] 92 | # 93 | # @api private 94 | def collapse_reset(string) 95 | ansi_string = string.dup 96 | if ansi_string =~ /(#{Regexp.quote(clear)}){2,}/ 97 | ansi_string.gsub!(/(#{Regexp.quote(clear)}){2,}/, clear) 98 | end 99 | ansi_string 100 | end 101 | 102 | # Nest color 103 | # 104 | # @param [String] string 105 | # the string to decorate 106 | # 107 | # @param [String] ansi_colors 108 | # the ansi colors to apply 109 | # 110 | # @return [String] 111 | # 112 | # @api private 113 | def nest_color(string, ansi_colors) 114 | ansi_string = string.dup 115 | matches = ansi_string.scan(/#{Regexp.quote(clear)}/) 116 | if matches.length > 1 && !eachline 117 | ansi_string.sub!(/#{Regexp.quote(clear)}/, ansi_colors) 118 | end 119 | ansi_string 120 | end 121 | private :collapse_reset, :nest_color 122 | 123 | # Strip ANSI color codes from a string. 124 | # 125 | # Only ANSI color codes are removed, not movement codes or 126 | # other escapes sequences are stripped. 127 | # 128 | # @param [Array[String]] strings 129 | # a string or array of strings to sanitize 130 | # 131 | # @example 132 | # strip("foo\e[1mbar\e[0m") # => "foobar" 133 | # 134 | # @return [String] 135 | # 136 | # @api public 137 | def strip(*strings) 138 | modified = strings.map { |string| string.dup.gsub(ANSI_REGEX, '') } 139 | modified.size == 1 ? modified[0] : modified 140 | end 141 | 142 | # Check if string has color escape codes 143 | # 144 | # @param [String] string 145 | # the string to check for color strings 146 | # 147 | # @return [Boolean] 148 | # true when string contains color codes, false otherwise 149 | # 150 | # @api public 151 | def colored?(string) 152 | !ANSI_REGEX.match(string).nil? 153 | end 154 | 155 | # Return raw color code without embeding it into a string. 156 | # 157 | # @return [Array[String]] 158 | # ANSI escape codes 159 | # 160 | # @api public 161 | def code(*colors) 162 | attribute = [] 163 | colors.each do |color| 164 | value = ANSI::ATTRIBUTES[color] || ALIASES[color] 165 | if value 166 | attribute << value 167 | else 168 | validate(color) 169 | end 170 | end 171 | attribute 172 | end 173 | 174 | # Find the escape code for color attribute. 175 | # 176 | # @param [Symbol,String] colors 177 | # the color name(s) to lookup 178 | # 179 | # @return [String] 180 | # the ANSI code(s) 181 | # 182 | # @api private 183 | def lookup(*colors) 184 | attribute = code(*colors) 185 | "\e[#{attribute.join(';')}m" 186 | end 187 | 188 | # Expose all ANSI color names and their codes 189 | # 190 | # @return [Hash[Symbol]] 191 | # 192 | # @api public 193 | def styles 194 | ANSI::ATTRIBUTES.merge(ALIASES) 195 | end 196 | 197 | # List all available style names 198 | # 199 | # @return [Array[Symbol]] 200 | # 201 | # @api public 202 | def style_names 203 | styles.keys 204 | end 205 | 206 | # Check if provided colors are known colors 207 | # 208 | # @param [Array[Symbol,String]] 209 | # the list of colors to check 210 | # 211 | # @example 212 | # valid?(:red) # => true 213 | # 214 | # @return [Boolean] 215 | # true if all colors are valid, false otherwise 216 | # 217 | # @api public 218 | def valid?(*colors) 219 | colors.all? { |color| style_names.include?(color.to_sym) } 220 | end 221 | 222 | # Define a new color alias 223 | # 224 | # @param [String] alias_name 225 | # the color alias to define 226 | # @param [String] color 227 | # the color the alias will correspond to 228 | # 229 | # @return [String] 230 | # the standard color value of the alias 231 | # 232 | # @api public 233 | def alias_color(alias_name, color) 234 | validate(color) 235 | 236 | if !(alias_name.to_s =~ /^[\w]+$/) 237 | fail InvalidAliasNameError, "Invalid alias name `#{alias_name}`" 238 | elsif ANSI::ATTRIBUTES[alias_name] 239 | fail InvalidAliasNameError, 240 | "Cannot alias standard color `#{alias_name}`" 241 | end 242 | 243 | ALIASES[alias_name.to_sym] = ANSI::ATTRIBUTES[color] 244 | color 245 | end 246 | 247 | private 248 | 249 | # @api private 250 | def validate(*colors) 251 | return if valid?(*colors) 252 | fail InvalidAttributeNameError, 'Bad style or unintialized constant, ' \ 253 | " valid styles are: #{style_names.join(', ')}." 254 | end 255 | end # Color 256 | end # TTY 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | pastel logo 3 |
4 | # Pastel 5 | [![Gem Version](https://badge.fury.io/rb/pastel.svg)][gem] 6 | [![Build Status](https://secure.travis-ci.org/peter-murach/pastel.svg?branch=master)][travis] 7 | [![Code Climate](https://codeclimate.com/github/peter-murach/pastel/badges/gpa.svg)][codeclimate] 8 | [![Coverage Status](https://coveralls.io/repos/peter-murach/pastel/badge.svg)][coverage] 9 | [![Inline docs](http://inch-ci.org/github/peter-murach/tty.svg?branch=master)][inchpages] 10 | 11 | [gem]: http://badge.fury.io/rb/pastel 12 | [travis]: http://travis-ci.org/peter-murach/pastel 13 | [codeclimate]: https://codeclimate.com/github/peter-murach/pastel 14 | [coverage]: https://coveralls.io/r/peter-murach/pastel 15 | [inchpages]: http://inch-ci.org/github/peter-murach/pastel 16 | 17 | > Terminal output styling with intuitive and clean API that doesn't monkey patch String class. 18 | 19 | **Pastel** is minimal and focused to work in all terminal emulators. 20 | 21 | ![screenshot](https://github.com/peter-murach/pastel/raw/master/assets/screenshot.png) 22 | 23 | **Pastel** provides independent coloring component for [TTY](https://github.com/peter-murach/tty) toolkit. 24 | 25 | ## Features 26 | 27 | * Doesn't monkey patch `String` 28 | * Intuitive and expressive API 29 | * Minimal and focused to work on all terminal emulators 30 | * Auto-detection of color support 31 | * Allows nested styles 32 | * Performant 33 | 34 | ## Installation 35 | 36 | Add this line to your application's Gemfile: 37 | 38 | gem 'pastel' 39 | 40 | And then execute: 41 | 42 | $ bundle 43 | 44 | Or install it yourself as: 45 | 46 | $ gem install pastel 47 | 48 | ## Contents 49 | 50 | * [1. Usage](#1-usage) 51 | * [2. Interface](#2-interface) 52 | * [2.1 Color](#21-color) 53 | * [2.2 Decorate](#22-decorate) 54 | * [2.3 Detach](#23-detach) 55 | * [2.4 Strip](#24-strip) 56 | * [2.5 Styles](#25-styles) 57 | * [2.6 Valid?](#26-valid) 58 | * [2.7 Colored?](#27-colored) 59 | * [2.8 Enabled?](#28-enabled) 60 | * [2.9 Eachline](#29-eachline) 61 | * [2.10 Alias Color](#30-alias-color) 62 | * [3. Supported Colors](#3-supported-colors) 63 | * [4. Environment](#4-environment) 64 | 65 | ## 1 Usage 66 | 67 | **Pastel** provides a simple, minimal and intuitive API for styling your strings: 68 | 69 | ```ruby 70 | pastel = Pastel.new 71 | 72 | pastel.red('Unicorns!') 73 | ``` 74 | 75 | You can compose multiple styles through chainable API: 76 | 77 | ```ruby 78 | pastel.red.on_green.bold('Unicorns!') 79 | ``` 80 | 81 | It allows you to combine styled strings with unstyled ones: 82 | 83 | ```ruby 84 | pastel.red('Unicorns') + ' will rule ' + pastel.green('the World!') 85 | ``` 86 | 87 | It supports variable number of arguments: 88 | 89 | ```ruby 90 | pastel.red('Unicorns', 'are', 'running', 'everywhere!') 91 | ``` 92 | 93 | You can also nest styles as follows: 94 | 95 | ```ruby 96 | pastel.red('Unicorns ', pastel.on_green('everywhere!')) 97 | ``` 98 | 99 | Nesting is smart enough to know where one color ends and another one starts: 100 | 101 | ```ruby 102 | pastel.red('Unicorns ' + pastel.green('everywhere') + pastel.on_yellow('!')) 103 | ``` 104 | 105 | You can also nest styles inside blocks: 106 | 107 | ```ruby 108 | pastel.red.on_green('Unicorns') { 109 | green.on_red('will ', 'dominate') { 110 | yellow('the world!') 111 | } 112 | } 113 | ``` 114 | 115 | When dealing with multiline strings you can set `eachline` option(more info see [eachline](#29-eachline)): 116 | 117 | ``` 118 | pastel = Pastel.new(eachline: "\n") 119 | ``` 120 | 121 | You can also predefine needed styles and reuse them: 122 | 123 | ```ruby 124 | error = pastel.red.bold.detach 125 | warning = pastel.yellow.detach 126 | 127 | puts error.('Error!') 128 | puts warning.('Warning') 129 | ``` 130 | 131 | ## 2 Interface 132 | 133 | ### 2.1 Color 134 | 135 | pastel.`[....](string, [string...])` 136 | 137 | Color styles are invoked as method calls with a string argument. A given color can take any number of strings as arguments. Then it returns a colored string which isn't printed out to terminal. You need to print it yourself if you need to. This is done so that you can save it as a string, pass to something else, send it to a file handle and so on. 138 | 139 | ```ruby 140 | pastel.red('Unicorns ', pastel.bold.underline('everywhere'), '!') 141 | ``` 142 | 143 | Please refer to [3. Supported Colors](#3-supported-colors) section for full list of supported styles. 144 | 145 | ### 2.2 Decorate 146 | 147 | This method is a lower level string styling call that takes as the first argument the string to style followed by any number of color attributes, and returns string wrapped in styles. 148 | 149 | ```ruby 150 | pastel.decorate('Unicorn', :green, :on_blue, :bold) 151 | ``` 152 | 153 | This method will be useful in situations where colors are provided as a list of parameters that have been generated dynamically. 154 | 155 | ### 2.3 Detach 156 | 157 | The `detach` method allows to keep all the associated colors with the detached instance for later reference. This method is useful when detached colors are being reused frequently and thus shorthand version is preferred. The detached object can be invoked using `call` method or it's shorthand `.()`, as well as array like access `[]`. For example, the following are equivalent examples of detaching colors: 158 | 159 | ```ruby 160 | notice = pastel.blue.bold.detach 161 | 162 | notice.call('Unicorns running') 163 | notice.('Unicorns running') 164 | notice['Unicorns running'] 165 | ``` 166 | 167 | ### 2.4 Strip 168 | 169 | Strip only color sequence characters from the provided strings and preserve any movement codes or other escape sequences. The return value will be either array of modified strings or a single string. The arguments are not modified. 170 | 171 | ```ruby 172 | pastel.strip("\e[1m\e[34mbold blue text\e[0m") # => "bold blue text" 173 | ``` 174 | 175 | ### 2.5 Styles 176 | 177 | To get a full list of supported styles with the corresponding color codes do: 178 | 179 | ```ruby 180 | pastel.styles 181 | ``` 182 | 183 | ### 2.6 Valid? 184 | 185 | Determine whether a color or a list of colors are valid. `valid?` takes one or more attribute strings or symbols and returns true if all attributes are known and false otherwise. 186 | 187 | ```ruby 188 | pastel.valid?(:red, :blue) # => true 189 | pastel.valid?(:unicorn) # => false 190 | ``` 191 | 192 | ### 2.7 Colored? 193 | 194 | In order to determine if string has color escape codes use `colored?` like so 195 | 196 | ```ruby 197 | pastel.colored?("\e[31mcolorful\e[0m") # => true 198 | ``` 199 | 200 | ### 2.8 Enabled? 201 | 202 | In order to detect if your terminal supports coloring do: 203 | 204 | ```ruby 205 | pastel.enabled? # => false 206 | ``` 207 | 208 | In cases when the color support is not provided no styling will be applied to the colored string. Moreover, you can force **Pastel** to always print out string with coloring switched on: 209 | 210 | ```ruby 211 | pastel = Pastel.new(enabled: true) 212 | pastel.enabled? # => false 213 | ``` 214 | 215 | ### 2.9 Eachline 216 | 217 | Normally **Pastel** colors string by putting color codes at the beginning and end of the string, but if you provide `eachline` option set to some string, that string will be considered the line delimiter. Consequently, each line will be separately colored with escape sequence and reset code at the end. This option is desirable if the output string contains newlines and you're using background colors. Since color code that spans more than one line is often interpreted by terminal as providing background for all the lines that follow. This in turn may cause programs such as pagers to spill the colors throughout the text. In most cases you will want to set `eachline` to `\n` character like so: 218 | 219 | ```ruby 220 | pastel = Pastel.new(eachline: "\n") 221 | pastel.red("foo\nbar") # => "\e[31mfoo\e[0m\n\e[31mbar\e[0m" 222 | ``` 223 | 224 | ### 2.10 Alias Color 225 | 226 | In order to setup an alias for the standard color do: 227 | 228 | ```ruby 229 | pastel.alias_color(:funky, :red) 230 | ``` 231 | 232 | From that point forward, `:funky` alias can be passed to `decorate`, `valid?` with the same meaning as standard color: 233 | 234 | ```ruby 235 | pastel.funky.on_green('unicorn') # => will use :red color 236 | ``` 237 | 238 | This method allows you to give more meaningful names to existing colors. 239 | 240 | You can also use the `PASTEL_COLORS_ALIASES` environment variable (see [Environment](#4-environment)) to specify aliases. 241 | 242 | Note: Aliases are global and affect all callers in the same process. 243 | 244 | ## 3 Supported Colors 245 | 246 | **Pastel** works with terminal emulators that support minimum sixteen colors. It provides `16` basic colors and `8` styles with further `16` bright color pairs. The corresponding bright color is obtained by prepending the `bright` to the normal color name. For example, color `red` will have `bright_red` as its pair. 247 | 248 | The variant with `on_` prefix will style the text background color. 249 | 250 | The foreground colors: 251 | 252 | * `black` 253 | * `red` 254 | * `green` 255 | * `yellow` 256 | * `blue` 257 | * `magenta` 258 | * `cyan` 259 | * `white` 260 | * `bright_black` 261 | * `bright_red` 262 | * `bright_green` 263 | * `bright_yellow` 264 | * `bright_blue` 265 | * `bright_magenta` 266 | * `bright_cyan` 267 | * `bright_white` 268 | 269 | The background colors: 270 | 271 | * `on_black` 272 | * `on_red` 273 | * `on_green` 274 | * `on_yellow` 275 | * `on_blue` 276 | * `on_magenta` 277 | * `on_cyan` 278 | * `on_white` 279 | * `on_bright_black` 280 | * `on_bright_red` 281 | * `on_bright_green` 282 | * `on_bright_yellow` 283 | * `on_bright_blue` 284 | * `on_bright_magenta` 285 | * `on_bright_cyan` 286 | * `on_bright_white` 287 | 288 | Generic styles: 289 | 290 | * `clear` 291 | * `bold` 292 | * `dim` 293 | * `italic` 294 | * `underline` 295 | * `inverse` 296 | * `hidden` 297 | * `strikethrough` 298 | 299 | ## 4 Environment 300 | 301 | ### 4.1 PASTEL_COLORS_ALIASES 302 | 303 | This environment variable allows you to specify custom color aliases at runtime that will be understood by **Pastel**. The environment variable is read and used when the instance of **Pastel** is created. You can also use `alias_color` to create aliases. 304 | 305 | Only alphanumeric and `_` are allowed in the alias names with the following format: 306 | 307 | ```ruby 308 | PASTEL_COLORS_ALIASES='newcolor_1=red,newcolor_2=on_green' 309 | ``` 310 | 311 | ## Contributing 312 | 313 | 1. Fork it ( https://github.com/peter-murach/pastel/fork ) 314 | 2. Create your feature branch (`git checkout -b my-new-feature`) 315 | 3. Commit your changes (`git commit -am 'Add some feature'`) 316 | 4. Push to the branch (`git push origin my-new-feature`) 317 | 5. Create a new Pull Request 318 | 319 | ## Copyright 320 | 321 | Copyright (c) 2014-2015 Piotr Murach. See LICENSE for further details. 322 | --------------------------------------------------------------------------------