├── examples └── basic │ ├── libs │ └── chalk_box │ └── src │ └── basic.cr ├── src ├── chalk_box │ ├── version.cr │ ├── base.cr │ ├── colors.cr │ ├── styles.cr │ └── supports.cr └── chalk_box.cr ├── .gitignore ├── shard.yml ├── CHANGELOG.md ├── circle.yml ├── spec ├── chalk_box_spec.cr ├── spec_helper.cr ├── chalk_box │ ├── styles_spec.cr │ ├── base_spec.cr │ └── supports_spec.cr └── integration_spec.cr ├── README.md └── LICENSE /examples/basic/libs/chalk_box: -------------------------------------------------------------------------------- 1 | ../../../src -------------------------------------------------------------------------------- /src/chalk_box/version.cr: -------------------------------------------------------------------------------- 1 | module ChalkBox 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /src/chalk_box.cr: -------------------------------------------------------------------------------- 1 | require "./chalk_box/*" 2 | 3 | module ChalkBox 4 | def chalk 5 | Base.new 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | .crystal 4 | .shards 5 | 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in application that uses them 9 | /shard.lock 10 | 11 | -------------------------------------------------------------------------------- /examples/basic/src/basic.cr: -------------------------------------------------------------------------------- 1 | require "chalk_box" 2 | 3 | module Basic 4 | extend ChalkBox 5 | extend self 6 | 7 | def main 8 | puts chalk.green("green fields") 9 | end 10 | end 11 | 12 | Basic.main 13 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: chalk_box 2 | version: 0.2.0 3 | 4 | authors: 5 | - Everton Ribeiro 6 | 7 | license: Apache-2 8 | 9 | development_dependencies: 10 | minitest: 11 | github: ysbaddaden/minitest.cr 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## v0.2.0 - (2016-06-15) 7 | 8 | * Enhancements 9 | * [crystal] Upgrading to support only crystal `0.18.0`; 10 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | cache_directories: 3 | - lib 4 | pre: 5 | - curl http://dist.crystal-lang.org/apt/setup.sh | sudo bash 6 | - sudo apt-get install crystal -y 7 | - crystal --version 8 | - shards --version 9 | - shards install 10 | 11 | test: 12 | override: 13 | - crystal spec -v 14 | -------------------------------------------------------------------------------- /spec/chalk_box_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | spec_mod ChalkBox, includes: true do 4 | it "should not raise exception with default args" do 5 | if STDOUT.tty? # is default for supports 6 | expect(chalk.red("foo")).must_equal "\e[31mfoo\e[39m" 7 | else 8 | expect(chalk.red("foo")).must_equal "foo" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "../src/chalk_box" 2 | require "minitest/autorun" 3 | 4 | module ChalkBox::SpecHelpers 5 | end 6 | 7 | macro let(name, type, &block) 8 | @{{name.id}} : {{type.id}} 9 | let({{name.id}}) { {{yield}} } 10 | end 11 | 12 | macro spec_mod(mod, subject = true, includes = false, &block) 13 | describe {{mod.id}} do 14 | alias SubjectType = Nil | {{mod.id}} 15 | include ChalkBox::SpecHelpers 16 | {% if includes %} 17 | include {{mod.id}} 18 | {% end %} 19 | {% if subject %} 20 | let(:subject) { {{mod.id}} } 21 | {% end %} 22 | {{block.body}} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/chalk_box/styles_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | spec_mod ChalkBox::Styles, includes: true do 4 | it("should return ANSI escape codes") do 5 | expect(green.open).must_equal("\u001b[32m") 6 | expect(green.close).must_equal("\u001b[39m") 7 | expect(bgGreen.open).must_equal("\u001b[42m") 8 | expect(gray.open).must_equal(grey.open) 9 | end 10 | 11 | it("should group related codes into categories") do 12 | expect(color.magenta).must_equal(magenta) 13 | expect(bgColor.yellow).must_equal(bgYellow) 14 | expect(bgColor.bgYellow).must_equal(bgYellow) 15 | expect(modifier.bold).must_equal(bold) 16 | expect(modifier.underline.open).must_equal(Modifier.underline.open) 17 | expect(modifier.dim.open).must_equal(Modifier::Dim.open) 18 | end 19 | 20 | it("should return open if convert to String") do 21 | expect(magenta.to_s).must_equal(magenta.open) 22 | expect("#{yellow}").must_equal(yellow.open) 23 | expect("#{reset}").must_equal(reset) 24 | expect("#{yellow.reset}").must_equal(yellow.close) 25 | end 26 | 27 | it("should support reset") do 28 | expect(reset).must_equal("\u001b[0m") 29 | expect(color.reset).must_equal("\u001b[0m") 30 | expect(color.green.reset).must_equal(green.reset) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/integration_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | class FailedCommand < Exception 4 | getter stdout : String 5 | getter stderr : String 6 | 7 | def initialize(message, @stdout, @stderr) 8 | super message 9 | end 10 | end 11 | 12 | describe "Integration spec" do 13 | def run(command, env = nil, chdir = nil) 14 | output, error = IO::Memory.new, IO::Memory.new 15 | status = Process.run( 16 | "/bin/sh", input: IO::Memory.new(command), output: output, error: error, 17 | chdir: chdir, env: env 18 | ) 19 | 20 | if status.success? 21 | output.to_s 22 | else 23 | puts output.to_s 24 | puts error.to_s 25 | raise FailedCommand.new("command failed: #{command}", output.to_s, error.to_s) 26 | end 27 | end 28 | 29 | let(:chdir) { File.join(__DIR__, "../examples/basic") } 30 | 31 | it "should puts without colors" do 32 | output = run("crystal run src/basic.cr", chdir: chdir) 33 | expect(output).must_equal "green fields\n" 34 | end 35 | 36 | it "should puts with colors if forced with args" do 37 | command = "crystal run src/basic.cr -- --color" 38 | output = run(command, chdir: chdir) 39 | chalk = ChalkBox::Base.new(enable: true) 40 | expect(output).must_equal chalk.green("green fields\n") 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /src/chalk_box/base.cr: -------------------------------------------------------------------------------- 1 | require "./supports" 2 | require "./styles" 3 | 4 | class ChalkBox::Base 5 | @enable : Bool | Supports 6 | alias Buffer = Array(String) 7 | 8 | def initialize(@buffer = [] of Buffer, @enable = Supports.new) 9 | end 10 | 11 | def styles 12 | ChalkBox::Styles 13 | end 14 | 15 | private def buffering(buffer, enable) 16 | return self unless is_enable(enable) 17 | reset_buffer 18 | ChalkBox::Base.new buffer, enable 19 | end 20 | 21 | private def add_buffer(open, close) 22 | @buffer << [open, close] 23 | end 24 | 25 | private def format!(buffer, *args) 26 | reset_buffer 27 | format(buffer, *args) 28 | end 29 | 30 | private def format(buffer, *args) 31 | text = args.join(" ") 32 | return text unless is_enable(@enable) 33 | formated = text.split("\n").map do |line| 34 | next line if line.empty? 35 | buffer.reverse.reduce(line) do |text, color| 36 | "#{color[0]}#{text}#{color[1]}" 37 | end 38 | end 39 | formated.join("\n") 40 | end 41 | 42 | private def reset_buffer 43 | @buffer = [] of Buffer 44 | end 45 | 46 | private def is_enable(enable : Bool) 47 | enable 48 | end 49 | 50 | private def is_enable(enable : Supports) 51 | enable.hasBasic 52 | end 53 | 54 | # :nodoc: 55 | macro def_modifier(name) 56 | private def buffering_{{name.id}}() 57 | {% if name == "reset" %} 58 | color = Styles.{{name.id}} 59 | add_buffer(color, color) 60 | {% else %} 61 | add_buffer(Styles.{{name.id}}.open, Styles.{{name.id}}.close) 62 | {% end %} 63 | end 64 | 65 | def {{name.id}}() 66 | buffering_{{name.id}} 67 | buffering(@buffer, @enable) 68 | end 69 | 70 | def {{name.id}}(*args) 71 | buffering_{{name.id}} 72 | format!(@buffer, *args) 73 | end 74 | end 75 | 76 | {% for name in Styles::Colors::COLORS %} 77 | def_modifier({{name}}) 78 | def_modifier(bg{{name.camelcase.id}}) 79 | {% end %} 80 | def_modifier("reset") 81 | 82 | {% for name in Styles::Colors::MODIFIERS %} 83 | def_modifier({{name}}) 84 | {% end %} 85 | end 86 | -------------------------------------------------------------------------------- /src/chalk_box/colors.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module ChalkBox::Styles::Colors 3 | # Foreground colors 4 | # 8 default, 3 alias for gray and more 8 for extends 5 | FORE_DEFAULT = ["39", "39"] 6 | FORE_BLACK = ["30", "39"] 7 | FORE_RED = ["31", "39"] 8 | FORE_GREEN = ["32", "39"] 9 | FORE_YELLOW = ["33", "39"] 10 | FORE_BLUE = ["34", "39"] 11 | FORE_MAGENTA = ["35", "39"] 12 | FORE_CYAN = ["36", "39"] 13 | FORE_GRAY = ["37", "39"] 14 | FORE_GREY = FORE_GRAY 15 | FORE_LIGHT_GRAY = FORE_GRAY 16 | FORE_LIGHT_GREY = FORE_GRAY 17 | FORE_DARK_GRAY = ["90", "39"] 18 | FORE_DARK_GREY = FORE_DARK_GRAY 19 | FORE_LIGHT_RED = ["91", "39"] 20 | FORE_LIGHT_GREEN = ["92", "39"] 21 | FORE_LIGHT_YELLOW = ["93", "39"] 22 | FORE_LIGHT_BLUE = ["94", "39"] 23 | FORE_LIGHT_MAGENTA = ["95", "39"] 24 | FORE_LIGHT_CYAN = ["96", "39"] 25 | FORE_WHITE = ["97", "39"] 26 | 27 | # Background colors 28 | # 8 default, 3 alias for gray and more 8 for extends 29 | BACK_DEFAULT = ["49", "49"] 30 | BACK_BLACK = ["40", "49"] 31 | BACK_RED = ["41", "49"] 32 | BACK_GREEN = ["42", "49"] 33 | BACK_YELLOW = ["43", "49"] 34 | BACK_BLUE = ["44", "49"] 35 | BACK_MAGENTA = ["45", "49"] 36 | BACK_CYAN = ["46", "49"] 37 | BACK_GRAY = ["47", "49"] 38 | BACK_GREY = BACK_GRAY 39 | BACK_LIGHT_GRAY = BACK_GRAY 40 | BACK_LIGHT_GREY = BACK_GRAY 41 | BACK_DARK_GRAY = ["100", "49"] 42 | BACK_DARK_GREY = BACK_DARK_GRAY 43 | BACK_LIGHT_RED = ["101", "49"] 44 | BACK_LIGHT_GREEN = ["102", "49"] 45 | BACK_LIGHT_YELLOW = ["103", "49"] 46 | BACK_LIGHT_BLUE = ["104", "49"] 47 | BACK_LIGHT_MAGENTA = ["105", "49"] 48 | BACK_LIGHT_CYAN = ["106", "49"] 49 | BACK_WHITE = ["107", "49"] 50 | 51 | # Modifiers 52 | MOD_DEFAULT = ["0", "0"] 53 | MOD_BOLD = ["1", "22"] 54 | MOD_BRIGHT = ["1", "22"] # Bold conflict 55 | MOD_DIM = ["2", "23"] 56 | MOD_UNDERLINE = ["4", "34"] 57 | MOD_BLINK = ["5", "27"] 58 | MOD_REVERSE = ["7", "28"] 59 | MOD_HIDDEN = ["8", "29"] 60 | 61 | COLORS = %w(black red green yellow blue magenta cyan gray grey light_gray light_grey dark_gray dark_grey light_red light_green light_yellow light_blue light_magenta light_cyan white) 62 | MODIFIERS = %w(bold bright dim underline blink reverse hidden) 63 | end 64 | -------------------------------------------------------------------------------- /src/chalk_box/styles.cr: -------------------------------------------------------------------------------- 1 | require "./colors" 2 | 3 | # [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles) for styling strings in the terminal 4 | # 5 | # This module is based in [chalk/ansi-styles](https://github.com/chalk/ansi-styles) 6 | # 7 | # ## Usage 8 | # 9 | # ``` 10 | # require "chalk-box" 11 | # include ChalkBox::Styles 12 | # 13 | # puts "#{red}Red#{reset}" 14 | # puts "#{green.open}Green#{green.close}" 15 | # ``` 16 | # 17 | # ## API 18 | # Each style has an open and close property (except reset) 19 | # 20 | module ChalkBox::Styles 21 | # :nodoc: 22 | ESCAPE_CODE = "\e" 23 | 24 | # :nodoc: 25 | module Private::Stringify 26 | def to_s 27 | self.open 28 | end 29 | 30 | def to_s(io : IO) 31 | io << to_s 32 | end 33 | end 34 | 35 | # :nodoc: 36 | module Private::Reset 37 | extend Stringify 38 | 39 | macro escape(code = 0) 40 | "#{ESCAPE_CODE}[#{{{code}}}m" 41 | end 42 | 43 | def close 44 | escape 45 | end 46 | 47 | def open 48 | close 49 | end 50 | 51 | def reset 52 | close 53 | end 54 | end 55 | 56 | # Default reset methods expose 57 | include Private::Reset 58 | 59 | # :nodoc: 60 | macro group(group_name, key, color, bg = false, method = nil) 61 | module {{group_name.id}} 62 | extend self 63 | include Private::Reset 64 | 65 | module {{color.camelcase.id}} 66 | extend self 67 | extend Private::Stringify 68 | include Private::Reset 69 | 70 | def open 71 | escape(Colors::{{key.id}}_{{color.upcase.id}}[0]) 72 | end 73 | 74 | def close 75 | escape(Colors::{{key.id}}_{{color.upcase.id}}[1]) 76 | end 77 | end 78 | 79 | def {{color.id}} 80 | {{color.camelcase.id}} 81 | end 82 | 83 | {% if bg %} 84 | def bg{{color.camelcase.id}} 85 | {{color.camelcase.id}} 86 | end 87 | {% end %} 88 | end 89 | 90 | def {{(bg ? "bg#{color.camelcase.id}" : color).id}} 91 | {{group_name.id}}::{{color.camelcase.id}} 92 | end 93 | end 94 | 95 | {% for name in Colors::COLORS %} 96 | group Color, FORE, {{name}} 97 | group BgColor, BACK, {{name}}, bg: true 98 | {% end %} 99 | 100 | {% for name in Colors::MODIFIERS %} 101 | group Modifier, MOD, {{name}} 102 | {% end %} 103 | 104 | def modifier 105 | Modifier 106 | end 107 | 108 | def color 109 | Color 110 | end 111 | 112 | def bgColor 113 | BgColor 114 | end 115 | 116 | extend self 117 | end 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chalk Box 2 | 3 | [![Circle CI](https://circleci.com/gh/azukiapp/crystal-chalk-box/tree/master.svg?style=svg)](https://circleci.com/gh/azukiapp/crystal-chalk-box/tree/master) 4 | 5 | Terminal toolbox to paint and embroider :) 6 | 7 | ## Why 8 | 9 | [Colorize](http://crystal-lang.org/api/Colorize.html) is the obvious choice for those who are starting coding on Crystal. However it changes the String class, and that's not a good practice. 10 | 11 | Besides that, it doesn't offer ways to handle multiple types of terminal. 12 | 13 | Obs: Yes, this lib is similar to the [chalk](https://github.com/chalk/chalk) lib from Node.js. The differences are: 14 | 15 | - This lib is written in Crystal (oh really?); 16 | - All-in-one (the style and support modules are integrated); 17 | - This lib doesn't support 256 and TrueColor for now. 18 | 19 | ## Installation 20 | 21 | Add this to your application's `shard.yml`: 22 | 23 | ```yaml 24 | dependencies: 25 | chalk_box: 26 | github: azukiapp/crystal-chalk-box 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```crystal 32 | require "chalk_box" 33 | 34 | module Basic 35 | extend ChalkBox 36 | extend self 37 | 38 | def main 39 | puts chalk.green("green fields") 40 | end 41 | end 42 | 43 | Basic.main 44 | ``` 45 | 46 | ## API 47 | 48 | ### chalk.style[.style...](*args) 49 | 50 | Example: `chalk.red.bold.underline("Hello", "world")` 51 | 52 | Chain [styles](#styles) and call the last one as a method with a string argument. Order doesn't matter, and later styles take precedent in case of a conflict. This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`. 53 | 54 | ### chalk.enabled 55 | 56 | Color support is automatically detected, but you can override it by setting the `enabled` property. 57 | 58 | For default enable is instance of `ChalkBox::Supports`. 59 | 60 | ### ChalkBox::Supports 61 | 62 | Detect whether the terminal supports color. Used internally and handled for you, but exposed for convenience. 63 | 64 | Can be overridden by the user with the flags `--color` and `--no-color`. For situations where using `--color` is not possible, add an environment variable `FORCE_COLOR` with any value to force color. Trumps `--no-color`. 65 | 66 | ## Styles 67 | 68 | ### Modifiers 69 | 70 | - `reset` 71 | - `bold` 72 | - `dim` 73 | - `italic` *(not widely supported)* 74 | - `underline` 75 | - `inverse` 76 | - `hidden` 77 | - `strikethrough` *(not widely supported)* 78 | 79 | ### Colors 80 | 81 | - `black` 82 | - `red` 83 | - `green` 84 | - `yellow` 85 | - `blue` *(on Windows the bright version is used as normal blue is illegible)* 86 | - `magenta` 87 | - `cyan` 88 | - `white` 89 | - `gray` 90 | 91 | ### Background colors 92 | 93 | - `bgBlack` 94 | - `bgRed` 95 | - `bgGreen` 96 | - `bgYellow` 97 | - `bgBlue` 98 | - `bgMagenta` 99 | - `bgCyan` 100 | - `bgWhite` 101 | 102 | ## TODO 103 | 104 | - Add examples for `ChalkBox::Styles`; 105 | - Add examples for `ChalkBox::Supports`; 106 | - Add support for 256 colors; 107 | - Add support for truecolor; 108 | 109 | ## Contributing 110 | 111 | 1. Fork it ( https://github.com/azukiapp/crystal-chalk-box/fork ) 112 | 2. Create your feature branch (git checkout -b my-new-feature) 113 | 3. Commit your changes (git commit -am 'Add some feature') 114 | 4. Push to the branch (git push origin my-new-feature) 115 | 5. Create a new Pull Request 116 | 117 | ## Contributors 118 | 119 | - [nuxlli](https://github.com/nuxlli) Everton Ribeiro - creator, maintainer 120 | 121 | ## License 122 | 123 | "Azuki", "azk" and the Azuki logo are copyright (c) 2013-2016 Azuki Serviços de Internet LTDA. 124 | 125 | **azk** source code is released under Apache 2 License. 126 | 127 | Check LEGAL and LICENSE files for more information. 128 | -------------------------------------------------------------------------------- /src/chalk_box/supports.cr: -------------------------------------------------------------------------------- 1 | # Detect whether a terminal supports color 2 | # 3 | # This module is based in [chalk/supports-color](https://github.com/chalk/supports-color) 4 | # 5 | # ## API 6 | # 7 | # The returned object specifies a level of support for color through a 8 | # `.level` property and a corresponding flag: 9 | # 10 | # ```text 11 | # .level = 1 and .hasBasic = true: Basic color support (16 colors) 12 | # .level = 2 and .has256 = true: 256 color support 13 | # .level = 3 and .has16m = true: 16 million (truecolor) support 14 | # ``` 15 | # 16 | # ## Info 17 | # 18 | # It obeys the `--color` and `--no-color` CLI flags. 19 | # 20 | # For situations where using `--color` is not possible, 21 | # add an environment variable `FORCE_COLOR` with any value to force color. 22 | # Trumps `--no-color`. 23 | # 24 | # Explicit 256/truecolor mode can be enabled using the 25 | # `--color=256` and `--color=16m` flags, respectively. 26 | # 27 | class ChalkBox::Supports 28 | @env : ENV.class | Hash(String, String) 29 | 30 | @argv : Array(String) 31 | @stdout : IO::FileDescriptor 32 | @level = -1 33 | 34 | def initialize(@env = ENV, @argv = ARGV, @stdout = STDOUT) 35 | @argv = @argv.take_while { |arg| arg != "--" } 36 | end 37 | 38 | # :nodoc: 39 | class LocalMacros 40 | # But have a explanation 41 | macro def_has(name, level, label) 42 | # Checks (if not already) and returns if have supports for {{label.id}} 43 | def {{name.id}} 44 | return level >= {{level}} 45 | end 46 | end 47 | end 48 | 49 | LocalMacros.def_has(hasBasic, 1, "basic ANSI colors") 50 | LocalMacros.def_has(has256, 2, "256 colors") 51 | LocalMacros.def_has(has16m, 3, "16m colors") 52 | 53 | # Checks (if not already) and returns colors support level 54 | # 55 | # Code levels: 56 | # 57 | # ```text 58 | # 1 - for basic supports 59 | # 2 - for 256 colors supports 60 | # 3 - for 16m colors supports 61 | # ``` 62 | def level 63 | if (@level < 0) 64 | @level = support 65 | @level = 1 if @level == 0 && env_flag(@env, :FORCE_COLOR) 66 | end 67 | return @level 68 | end 69 | 70 | private def support 71 | if has_flag(@argv, [ 72 | "color=false", "colors=false", 73 | "color=0", "colors=0", 74 | "no-color", "no-colors", 75 | "no-colors=false", "no-colors=0", 76 | ]) 77 | return 0 78 | elsif has_flag(@argv, [ 79 | "color", "colors", 80 | "color=true", "colors=true", 81 | "color=1", "colors=1", 82 | "color=always", "colors=always", 83 | ]) 84 | return 1 85 | elsif has_flag(@argv, ["color=256", "colors=256"]) 86 | return 2 87 | elsif has_flag(@argv, [ 88 | "color=full", "colors=full", 89 | "color=16m", "colors=16m", 90 | "color=truecolor", "colors=truecolor", 91 | ]) 92 | return 3 93 | elsif !@stdout.tty? 94 | return 0 95 | elsif env_flag(@env, :COLORTERM) 96 | return 1 97 | elsif /^xterm-256(?:color)?/ =~ env_value(@env, :TERM) 98 | return 2 99 | elsif /^screen|^xterm|^vt100|color|ansi|cygwin|linux/i =~ env_value(@env, :TERM) 100 | return 1 101 | else 102 | return 0 103 | end 104 | end 105 | 106 | private def env_value(env, key) 107 | env[key.to_s]? 108 | end 109 | 110 | private def env_flag(env, key) 111 | value = env_value(env, key) 112 | !(value.nil? || ["false", "0", "not"].includes?(value)) 113 | end 114 | 115 | private def has_flag(argv, flags : Array) 116 | flags.any? { |flag| has_flag(argv, flag) } 117 | end 118 | 119 | private def has_flag(argv, flag : String) 120 | argv.includes?("--#{flag}") 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/chalk_box/base_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | spec_mod ChalkBox::Base do 4 | it "should not raise exception with default args" do 5 | chalk = subject.new 6 | if STDOUT.tty? # is default for supports 7 | expect(chalk.red("foo")).must_equal "\e[31mfoo\e[39m" 8 | else 9 | expect(chalk.red("foo")).must_equal "foo" 10 | end 11 | end 12 | 13 | it "should not output colors when manually disabled" do 14 | chalk = subject.new(enable: false) 15 | expect(chalk.red("foo")).must_equal "foo" 16 | end 17 | 18 | it "should create a isolated context where colors can be disabled" do 19 | chalk_disabled = subject.new(enable: false) 20 | chalk_enabled = subject.new(enable: true) 21 | 22 | expect(chalk_disabled.red("foo")).must_equal "foo" 23 | expect(chalk_enabled.red("foo")).must_equal "\u001b[31mfoo\u001b[39m" 24 | end 25 | 26 | it "should expose the styles as ANSI escape codes" do 27 | chalk = subject.new 28 | expect(chalk.styles.red.open).must_equal "\u001b[31m" 29 | end 30 | 31 | describe "with support test object" do 32 | it "should format with colors if COLORTERM env is set" do 33 | env = {"FORCE_COLOR" => "true"} 34 | support = ChalkBox::Supports.new(env) 35 | chalk = subject.new(enable: support) 36 | expect(chalk.underline("foo")).must_equal "\u001b[4mfoo\u001b[34m" 37 | end 38 | 39 | it "should not output colors when COLORTERM env is set false" do 40 | env = {"FORCE_COLOR" => "false"} 41 | support = ChalkBox::Supports.new(env) 42 | chalk = subject.new(enable: support) 43 | expect(chalk.underline("foo")).must_equal "foo" 44 | end 45 | end 46 | 47 | describe "with enabled colors" do 48 | let(:chalk, SubjectType) { subject.new(enable: true) } 49 | 50 | it "should style string" do 51 | expect(chalk.underline("foo")).must_equal "\u001b[4mfoo\u001b[34m" 52 | expect(chalk.red("foo")).must_equal "\u001b[31mfoo\u001b[39m" 53 | expect(chalk.bgRed("foo")).must_equal "\u001b[41mfoo\u001b[49m" 54 | end 55 | 56 | it "should support applying multiple styles at once" do 57 | expected = "\u001b[31m\u001b[42m\u001b[4mfoo\u001b[34m\u001b[49m\u001b[39m" 58 | expect(chalk.red.bgGreen.underline("foo")).must_equal expected 59 | 60 | expected = "\u001b[4m\u001b[31m\u001b[42mfoo\u001b[49m\u001b[39m\u001b[34m" 61 | expect(chalk.underline.red.bgGreen("foo")).must_equal expected 62 | end 63 | 64 | it "should support nesting styles" do 65 | expected = "\u001b[31mfoo\u001b[4m\u001b[44mbar\u001b[49m\u001b[34m!\u001b[39m" 66 | result = chalk.red("foo#{chalk.underline.bgBlue("bar")}!") 67 | expect(result).must_equal expected 68 | end 69 | 70 | it "should reset all styles with `.reset()`" do 71 | expected = "\u001b[0m\u001b[31m\u001b[42m\u001b[4mfoo\u001b[34m\u001b[49m\u001b[39mfoo\u001b[0m" 72 | result = chalk.reset("#{chalk.red.bgGreen.underline("foo")}foo") 73 | expect(result).must_equal expected 74 | end 75 | 76 | it "should be able to cache multiple styles" do 77 | chalk = subject.new(enable: true) 78 | green = ->chalk.green(String) 79 | red = ->chalk.red(String) 80 | expect(red.call("foo")).must_equal chalk.red("foo") 81 | expect(red.call("foo")).wont_match green.call("foo") 82 | end 83 | 84 | it "should alias gray to grey" do 85 | expect(chalk.grey("foo")).must_equal "\u001b[37mfoo\u001b[39m" 86 | end 87 | 88 | it "should support variable number of arguments" do 89 | expect(chalk.red("foo", "bar")).must_equal "\u001b[31mfoo bar\u001b[39m" 90 | expect(chalk.red("foo", 1, :bar)).must_equal "\u001b[31mfoo 1 bar\u001b[39m" 91 | end 92 | 93 | it "should support falsy values" do 94 | expect(chalk.red(0)).must_equal "\u001b[31m0\u001b[39m" 95 | end 96 | 97 | it "shouldn't output escape codes if the input is empty" do 98 | expect(chalk.red("")).must_equal "" 99 | expect(chalk.red.blue.black("")).must_equal "" 100 | end 101 | 102 | it "line breaks should open and close colors" do 103 | expected = "\u001b[37mhello\u001b[39m\n\n\u001b[37mworld\u001b[39m" 104 | expect(chalk.grey("hello\n\nworld")).must_equal expected 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/chalk_box/supports_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module ChalkBox::SpecHelpers 4 | class MockFD < IO::FileDescriptor 5 | def initialize(@tty = false, *args) 6 | super(*args) 7 | end 8 | 9 | def tty? 10 | return @tty 11 | end 12 | end 13 | end 14 | 15 | spec_mod ChalkBox::Supports do 16 | let(:argv) { [] of String } 17 | let(:env) { {} of String => String } 18 | let(:stdout) { MockFD.new(true, File.open(__FILE__).fd) } 19 | 20 | it "should not raise exception with default args" do 21 | support = subject.new 22 | expect(support.hasBasic).must_equal(STDOUT.tty?) 23 | end 24 | 25 | it "should return true if `COLORTERM` is in env" do 26 | env = {"COLORTERM" => "true"} 27 | support = subject.new(env, argv, stdout) 28 | expect(support.hasBasic).must_equal(true) 29 | expect(support.level).must_equal(1) 30 | end 31 | 32 | describe "if basics TERMS" do 33 | it "should not support by default" do 34 | support = subject.new(env, argv) 35 | expect(support.hasBasic).must_equal(false) 36 | expect(support.level).must_equal(0) 37 | end 38 | 39 | it "should not support if dump term" do 40 | env = {"TERM" => "dump"} 41 | support = subject.new(env, argv, stdout) 42 | expect(support.hasBasic).must_equal(false) 43 | expect(support.level).must_equal(0) 44 | end 45 | 46 | it "should support basics TERM" do 47 | env = {"TERM" => "xterm"} 48 | support = subject.new(env, argv, stdout) 49 | expect(support.hasBasic).must_equal(true) 50 | expect(support.level).must_equal(1) 51 | end 52 | end 53 | 54 | describe "if xterm is 256 colors" do 55 | let(:env) { {"TERM" => "xterm-256color"} } 56 | 57 | it "should support by default" do 58 | support = subject.new(env, argv, stdout) 59 | expect(support.hasBasic).must_equal(true) 60 | expect(support.has256).must_equal(true) 61 | end 62 | 63 | it("should return false if --no-color or --no-colors flag is used") do 64 | argv = ["--no-color"] 65 | support = subject.new(env, argv, stdout) 66 | expect(support.hasBasic).must_equal(false) 67 | 68 | argv = ["--no-colors"] 69 | support = subject.new(env, argv, stdout) 70 | expect(support.hasBasic).must_equal(false) 71 | end 72 | 73 | it "should return true if --color or --colors flag is used" do 74 | argv = ["--color"] 75 | support = subject.new(env, argv, stdout) 76 | expect(support.hasBasic).must_equal(true) 77 | 78 | argv = ["--colors"] 79 | support = subject.new(env, argv, stdout) 80 | expect(support.hasBasic).must_equal(true) 81 | end 82 | 83 | it "should support `--color=true` or `--colors=always` flag" do 84 | argv = ["--color=true"] 85 | support = subject.new(env, argv, stdout) 86 | expect(support.hasBasic).must_equal(true) 87 | 88 | argv = ["--color=1"] 89 | support = subject.new(env, argv, stdout) 90 | expect(support.hasBasic).must_equal(true) 91 | 92 | argv = ["--colors=always"] 93 | support = subject.new(env, argv, stdout) 94 | expect(support.hasBasic).must_equal(true) 95 | end 96 | 97 | it "should support `--color=false` or `--colors=0` flag" do 98 | argv = ["--color=false"] 99 | support = subject.new(env, argv, stdout) 100 | expect(support.hasBasic).must_equal(false) 101 | 102 | argv = ["--color=0"] 103 | support = subject.new(env, argv, stdout) 104 | expect(support.hasBasic).must_equal(false) 105 | end 106 | 107 | it "should support `--color=256` flag" do 108 | argv = ["--color=256"] 109 | support = subject.new(env, argv, stdout) 110 | expect(support.hasBasic).must_equal(true) 111 | expect(support.level).must_equal(2) 112 | expect(support.has256).must_equal(true) 113 | end 114 | 115 | it "should support `full`, `16m` or `truecolor` flag" do 116 | argv = ["--color=full"] 117 | support = subject.new(env, argv, stdout) 118 | expect(support.hasBasic).must_equal(true) 119 | expect(support.level).must_equal(3) 120 | expect(support.has256).must_equal(true) 121 | expect(support.has16m).must_equal(true) 122 | 123 | argv = ["--color=16m"] 124 | support = subject.new(env, argv, stdout) 125 | expect(support.hasBasic).must_equal(true) 126 | expect(support.level).must_equal(3) 127 | 128 | argv = ["--colors=truecolor"] 129 | support = subject.new(env, argv, stdout) 130 | expect(support.hasBasic).must_equal(true) 131 | expect(support.level).must_equal(3) 132 | end 133 | 134 | it "should ignore post-terminator flags" do 135 | argv = ["--color", "--", "--no-color"] 136 | support = subject.new(env, argv, stdout) 137 | expect(support.hasBasic).must_equal(true) 138 | end 139 | end 140 | 141 | it("should return false if not TTY") do 142 | stdout = MockFD.new(false, File.open(__FILE__).fd) 143 | support = subject.new(env, argv, stdout) 144 | expect(support.hasBasic).must_equal(false) 145 | end 146 | 147 | it("should return true if `FORCE_COLOR` is in env") do 148 | env = {"FORCE_COLOR" => "true"} 149 | argv = ["--color=false"] 150 | support = subject.new(env) 151 | expect(support.hasBasic).must_equal(true, "FORCE_COLOR") 152 | expect(support.level).must_equal(1) 153 | end 154 | 155 | it "should allow tests of the properties on false" do 156 | env = {"TERM" => "xterm-256color"} 157 | argv = ["--color=false"] 158 | support = subject.new(env, argv) 159 | expect(support.hasBasic).must_equal(false) 160 | expect(support.has256).must_equal(false) 161 | expect(support.has16m).must_equal(false) 162 | expect(support.level).must_equal(0) 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "{}" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright 2013-2016 Azuki Serviços de Internet LTDA. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | --------------------------------------------------------------------------------