diff --git a/Makefile b/Makefile index dc675a05a148..cf3097ba9f39 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,19 @@ LLVM_CONFIG ?= ## llvm-config command path to use -release ?= ## Compile in release mode -stats ?= ## Enable statistics output -threads ?= ## Maximum number of threads to use -debug ?= ## Add symbolic debug info -verbose ?= ## Run specs in verbose mode +release ?= ## Compile in release mode +stats ?= ## Enable statistics output +threads ?= ## Maximum number of threads to use +debug ?= ## Add symbolic debug info +verbose ?= ## Run specs in verbose mode +color ?= auto ## Colorize the output O := .build SOURCES := $(shell find src -name '*.cr') SPEC_SOURCES := $(shell find spec -name '*.cr') -FLAGS := $(if $(release),--release )$(if $(stats),--stats )$(if $(threads),--threads $(threads) )$(if $(debug),-d ) +FLAGS := --color=$(color)$(if $(release), --release)$(if $(stats), --stats)$(if $(threads), --threads $(threads))$(if $(debug), -d) VERBOSE := $(if $(verbose),-v ) +COLOR := --color=$(color) EXPORTS := $(if $(release),,CRYSTAL_CONFIG_PATH=`pwd`/src) SHELL = bash LLVM_CONFIG_FINDER := \ @@ -73,15 +75,15 @@ help: ## Show this help .PHONY: spec spec: $(O)/all_spec ## Run all specs - $(O)/all_spec $(VERBOSE) + $(O)/all_spec $(COLOR) $(VERBOSE) .PHONY: std_spec std_spec: $(O)/std_spec ## Run standard library specs - $(O)/std_spec $(VERBOSE) + $(O)/std_spec $(COLOR) $(VERBOSE) .PHONY: compiler_spec compiler_spec: $(O)/compiler_spec ## Run compiler specs - $(O)/compiler_spec $(VERBOSE) + $(O)/compiler_spec $(COLOR) $(VERBOSE) .PHONY: doc doc: ## Generate standard library documentation diff --git a/bin/ci b/bin/ci index 5f616dc18fb2..333768fe133f 100755 --- a/bin/ci +++ b/bin/ci @@ -83,10 +83,10 @@ prepare_system() { } build() { - with_build_env 'make std_spec clean' - with_build_env 'make crystal spec doc' - with_build_env 'find samples -name "*.cr" | xargs -L 1 ./bin/crystal build --no-codegen' - with_build_env './bin/crystal tool format --check samples spec src' + with_build_env 'make std_spec clean color=always' + with_build_env 'make crystal spec doc color=always' + with_build_env 'find samples -name "*.cr" | xargs -L 1 ./bin/crystal build --color=always --no-codegen' + with_build_env './bin/crystal tool format --color=always --check samples spec src' } deploy() { diff --git a/spec/std/colorize_spec.cr b/spec/std/colorize_spec.cr index 78f9d6d4f612..9038cf78d857 100644 --- a/spec/std/colorize_spec.cr +++ b/spec/std/colorize_spec.cr @@ -1,148 +1,308 @@ require "spec" require "colorize" -describe "colorize" do - it "colorizes without change" do - "hello".colorize.to_s.should eq("hello") +private class FakeTTY < IO::Memory + def tty? + true end +end - it "colorizes foreground" do - "hello".colorize.black.to_s.should eq("\e[30mhello\e[0m") - "hello".colorize.red.to_s.should eq("\e[31mhello\e[0m") - "hello".colorize.green.to_s.should eq("\e[32mhello\e[0m") - "hello".colorize.yellow.to_s.should eq("\e[33mhello\e[0m") - "hello".colorize.blue.to_s.should eq("\e[34mhello\e[0m") - "hello".colorize.magenta.to_s.should eq("\e[35mhello\e[0m") - "hello".colorize.cyan.to_s.should eq("\e[36mhello\e[0m") - "hello".colorize.light_gray.to_s.should eq("\e[37mhello\e[0m") - "hello".colorize.dark_gray.to_s.should eq("\e[90mhello\e[0m") - "hello".colorize.light_red.to_s.should eq("\e[91mhello\e[0m") - "hello".colorize.light_green.to_s.should eq("\e[92mhello\e[0m") - "hello".colorize.light_yellow.to_s.should eq("\e[93mhello\e[0m") - "hello".colorize.light_blue.to_s.should eq("\e[94mhello\e[0m") - "hello".colorize.light_magenta.to_s.should eq("\e[95mhello\e[0m") - "hello".colorize.light_cyan.to_s.should eq("\e[96mhello\e[0m") - "hello".colorize.white.to_s.should eq("\e[97mhello\e[0m") +private def colorize(obj, io = IO::Memory.new, **args) + if obj + yield(obj.colorize(**args).when(:always).when(args[:when]?)).to_s io + else + yield(with_color(**args).when(:always).when(args[:when]?)).as(Colorize::Style).surround(io) { } end + io.to_s +end - it "colorizes background" do - "hello".colorize.on_black.to_s.should eq("\e[40mhello\e[0m") - "hello".colorize.on_red.to_s.should eq("\e[41mhello\e[0m") - "hello".colorize.on_green.to_s.should eq("\e[42mhello\e[0m") - "hello".colorize.on_yellow.to_s.should eq("\e[43mhello\e[0m") - "hello".colorize.on_blue.to_s.should eq("\e[44mhello\e[0m") - "hello".colorize.on_magenta.to_s.should eq("\e[45mhello\e[0m") - "hello".colorize.on_cyan.to_s.should eq("\e[46mhello\e[0m") - "hello".colorize.on_light_gray.to_s.should eq("\e[47mhello\e[0m") - "hello".colorize.on_dark_gray.to_s.should eq("\e[100mhello\e[0m") - "hello".colorize.on_light_red.to_s.should eq("\e[101mhello\e[0m") - "hello".colorize.on_light_green.to_s.should eq("\e[102mhello\e[0m") - "hello".colorize.on_light_yellow.to_s.should eq("\e[103mhello\e[0m") - "hello".colorize.on_light_blue.to_s.should eq("\e[104mhello\e[0m") - "hello".colorize.on_light_magenta.to_s.should eq("\e[105mhello\e[0m") - "hello".colorize.on_light_cyan.to_s.should eq("\e[106mhello\e[0m") - "hello".colorize.on_white.to_s.should eq("\e[107mhello\e[0m") - end +private def colorize(obj, **args) + colorize obj, **args, &.itself +end - it "colorizes mode" do - "hello".colorize.bold.to_s.should eq("\e[1mhello\e[0m") - "hello".colorize.bright.to_s.should eq("\e[1mhello\e[0m") - "hello".colorize.dim.to_s.should eq("\e[2mhello\e[0m") - "hello".colorize.underline.to_s.should eq("\e[4mhello\e[0m") - "hello".colorize.blink.to_s.should eq("\e[5mhello\e[0m") - "hello".colorize.reverse.to_s.should eq("\e[7mhello\e[0m") - "hello".colorize.hidden.to_s.should eq("\e[8mhello\e[0m") - end +describe Colorize do + [ + {Colorize::Object, ""}, + {Colorize::Style, nil}, + ].each do |cls, obj| + describe cls do + it "colorizes without change" do + colorize(obj).should eq("") + end - it "colorizes mode combination" do - "hello".colorize.bold.dim.underline.blink.reverse.hidden.to_s.should eq("\e[1;2;4;5;7;8mhello\e[0m") - end + {% for ground in %w(fore back) %} + {% prefix = "fore" == ground ? "".id : "on_".id %} + {% carry = "fore" == ground ? 0 : 10 %} - it "colorizes foreground with background" do - "hello".colorize.blue.on_green.to_s.should eq("\e[34;42mhello\e[0m") - end + it "colorize #{{{ground}}}ground with default color" do + colorize(obj, &.{{prefix}}default).should eq("") - it "colorizes foreground with background with mode" do - "hello".colorize.blue.on_green.bold.to_s.should eq("\e[34;42;1mhello\e[0m") - end + colorize(obj, &.{{ground.id}}(:default)).should eq("") + colorize(obj, &.{{ground.id}}("default")).should eq("") + colorize(obj, &.{{ground.id}}(Colorize::ColorANSI::Default)).should eq("") - it "colorizes foreground with symbol" do - "hello".colorize(:red).to_s.should eq("\e[31mhello\e[0m") - "hello".colorize.fore(:red).to_s.should eq("\e[31mhello\e[0m") - end + {% if "back" == ground %} + colorize(obj, &.on(:default)).should eq("") + colorize(obj, &.on("default")).should eq("") + colorize(obj, &.on(Colorize::ColorANSI::Default)).should eq("") + {% end %} - it "colorizes mode with symbol" do - "hello".colorize.mode(:bold).to_s.should eq("\e[1mhello\e[0m") - end + colorize(obj, {{ground.id}}: :default).should eq("") + colorize(obj, {{ground.id}}: "default").should eq("") + colorize(obj, {{ground.id}}: Colorize::ColorANSI::Default).should eq("") + end - it "raises on unknown foreground color" do - expect_raises ArgumentError, "unknown color: brown" do - "hello".colorize(:brown) - end - end + it "colorizes #{{{ground}}}ground with ANSI color" do + {% for color in Colorize::ColorANSI.constants.reject { |name| name == "Default" } %} + ans = "\e[#{{{carry + Colorize::ColorANSI.constant color}}}m\e[0m" - it "raises on unknown background color" do - expect_raises ArgumentError, "unknown color: brown" do - "hello".colorize.back(:brown) - end - end + colorize(obj, &.{{prefix}}{{color.underscore}}).should eq(ans) - it "raises on unknown mode" do - expect_raises ArgumentError, "unknown mode: bad" do - "hello".colorize.mode(:bad) - end - end + colorize(obj, &.{{ground.id}}({{color.underscore.symbolize}})).should eq(ans) + colorize(obj, &.{{ground.id}}({{color.underscore.stringify}})).should eq(ans) + colorize(obj, &.{{ground.id}}(Colorize::ColorANSI::{{color}})).should eq(ans) - it "inspects" do - "hello".colorize(:red).inspect.should eq("\e[31m\"hello\"\e[0m") - end + {% if "back" == ground %} + colorize(obj, &.on({{color.underscore.symbolize}})).should eq(ans) + colorize(obj, &.on({{color.underscore.stringify}})).should eq(ans) + colorize(obj, &.on(Colorize::ColorANSI::{{color}})).should eq(ans) + {% end %} - it "colorizes io with method" do - io = IO::Memory.new - with_color.red.surround(io) do - io << "hello" - end - io.to_s.should eq("\e[31mhello\e[0m") - end + colorize(obj, {{ground.id}}: {{color.underscore.symbolize}}).should eq(ans) + colorize(obj, {{ground.id}}: {{color.underscore.stringify}}).should eq(ans) + colorize(obj, {{ground.id}}: Colorize::ColorANSI::{{color}}).should eq(ans) + {% end %} + end - it "colorizes io with symbol" do - io = IO::Memory.new - with_color(:red).surround(io) do - io << "hello" - end - io.to_s.should eq("\e[31mhello\e[0m") - end + it "colorizes #{{{ground}}}ground with 256 color" do + 256.times do |i| + ans = "\e[#{{{carry + 38}}};5;#{i}m\e[0m" + + colorize(obj, &.{{ground.id}}(i)).should eq(ans) + colorize(obj, &.{{ground.id}}(i.to_s)).should eq(ans) + colorize(obj, &.{{ground.id}}(Colorize::Color256.new i)).should eq(ans) + + {% if "back" == ground %} + colorize(obj, &.on(i)).should eq(ans) + colorize(obj, &.on(i.to_s)).should eq(ans) + colorize(obj, &.on(Colorize::Color256.new i)).should eq(ans) + {% end %} + + colorize(obj, {{ground.id}}: i).should eq(ans) + colorize(obj, {{ground.id}}: i.to_s).should eq(ans) + colorize(obj, {{ground.id}}: Colorize::Color256.new i).should eq(ans) + end + end + + it "colorizes #{{{ground}}}ground with 32bit true color" do + [ + {"#000", {0x00, 0x00, 0x00}}, + {"#123", {0x11, 0x22, 0x33}}, + {"#FFF", {0xFF, 0xFF, 0xFF}}, + {"#012345", {0x01, 0x23, 0x45}}, + ].each do |(name, code)| + ans = "\e[#{{{carry + 38}}};2;#{code.join ";"}m\e[0m" + + colorize(obj, &.{{ground.id}}(name)).should eq(ans) + colorize(obj, &.{{ground.id}}(Colorize::ColorRGB.new *code)).should eq(ans) - it "colorizes with push and pop" do - io = IO::Memory.new - with_color.red.push(io) do - io << "hello" - with_color.green.push(io) do - io << "world" + {% if "back" == ground %} + colorize(obj, &.on(name)).should eq(ans) + colorize(obj, &.on(Colorize::ColorRGB.new *code)).should eq(ans) + {% end %} + + colorize(obj, {{ground.id}}: name).should eq(ans) + colorize(obj, {{ground.id}}: Colorize::ColorRGB.new *code).should eq(ans) + end + end + {% end %} + + it "colorizes foreground with background" do + colorize(obj, &.blue.on_green).should eq("\e[34;42m\e[0m") + colorize(obj, fore: :blue, back: :green).should eq("\e[34;42m\e[0m") end - io << "bye" - end - io.to_s.should eq("\e[31mhello\e[0;32mworld\e[0;31mbye\e[0m") - end - it "colorizes with push and pop resets" do - io = IO::Memory.new - with_color.red.push(io) do - io << "hello" - with_color.green.bold.push(io) do - io << "world" + it "colorizes mode" do + {% for mode, code in {bold: 1, bright: 1, dim: 2, underline: 4, blink: 5, reverse: 7, hidden: 8} %} + ans = "\e[#{{{code}}}m\e[0m" + + colorize(obj, &.{{mode}}).should eq(ans) + + colorize(obj, &.mode({{mode.symbolize}})).should eq(ans) + colorize(obj, &.mode({{mode.stringify}})).should eq(ans) + colorize(obj, &.mode(Colorize::Mode::{{mode.capitalize}})).should eq(ans) + + colorize(obj, mode: {{mode.symbolize}}).should eq(ans) + colorize(obj, mode: {{mode.stringify}}).should eq(ans) + colorize(obj, mode: Colorize::Mode::{{mode.capitalize}}).should eq(ans) + {% end %} + end + + it "colorizes mode combination" do + colorize(obj, &.bold.dim.underline.blink.reverse.hidden).should eq("\e[1;2;4;5;7;8m\e[0m") + colorize(obj, &.bold.bright.dim.underline.blink.reverse.hidden).should eq("\e[1;2;4;5;7;8m\e[0m") + + colorize(obj, &.mode(Colorize::Mode::All)).should eq("\e[1;2;4;5;7;8m\e[0m") + colorize(obj, mode: Colorize::Mode::All).should eq("\e[1;2;4;5;7;8m\e[0m") + end + + it "colorizes foreground with background with mode" do + colorize(obj, &.blue.on_green.bold).should eq("\e[34;42;1m\e[0m") + colorize(obj, fore: :blue, back: :green, mode: :bold).should eq("\e[34;42;1m\e[0m") + end + + it "colorizes when given io is TTY on 'auto' policy" do + colorize(obj, when: :auto, &.black).should eq("") + colorize(obj, when: "auto", &.black).should eq("") + colorize(obj, when: Colorize::When::Auto, &.black).should eq("") + colorize(obj, &.black.auto).should eq("") + colorize(obj, &.black.when(:auto)).should eq("") + colorize(obj, &.black.when("auto")).should eq("") + colorize(obj, &.black.when(Colorize::When::Auto)).should eq("") + + colorize(obj, io: FakeTTY.new, when: :auto, &.black).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, when: "auto", &.black).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, when: Colorize::When::Auto, &.black).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.auto).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.when(:auto)).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.when("auto")).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.when(Colorize::When::Auto)).should eq("\e[30m\e[0m") + end + + it "colorizes always" do + colorize(obj, when: :always, &.black).should eq("\e[30m\e[0m") + colorize(obj, when: "always", &.black).should eq("\e[30m\e[0m") + colorize(obj, when: Colorize::When::Always, &.black).should eq("\e[30m\e[0m") + colorize(obj, &.black.always).should eq("\e[30m\e[0m") + colorize(obj, &.black.when(:always)).should eq("\e[30m\e[0m") + colorize(obj, &.black.when("always")).should eq("\e[30m\e[0m") + colorize(obj, &.black.when(Colorize::When::Always)).should eq("\e[30m\e[0m") + + colorize(obj, io: FakeTTY.new, when: :always, &.black).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, when: "always", &.black).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, when: Colorize::When::Always, &.black).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.always).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.when(:always)).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.when("always")).should eq("\e[30m\e[0m") + colorize(obj, io: FakeTTY.new, &.black.when(Colorize::When::Always)).should eq("\e[30m\e[0m") + end + + it "colorizes never" do + colorize(obj, when: :never, &.black).should eq("") + colorize(obj, when: "never", &.black).should eq("") + colorize(obj, when: Colorize::When::Never, &.black).should eq("") + colorize(obj, &.black.never).should eq("") + colorize(obj, &.black.when(:never)).should eq("") + colorize(obj, &.black.when("never")).should eq("") + colorize(obj, &.black.when(Colorize::When::Never)).should eq("") + + colorize(obj, io: FakeTTY.new, when: :never, &.black).should eq("") + colorize(obj, io: FakeTTY.new, when: "never", &.black).should eq("") + colorize(obj, io: FakeTTY.new, when: Colorize::When::Never, &.black).should eq("") + colorize(obj, io: FakeTTY.new, &.black.never).should eq("") + colorize(obj, io: FakeTTY.new, &.black.when(:never)).should eq("") + colorize(obj, io: FakeTTY.new, &.black.when("never")).should eq("") + colorize(obj, io: FakeTTY.new, &.black.when(Colorize::When::Never)).should eq("") + end + + it "is chainable but apply only last" do + colorize(obj, &.blue.red).should eq("\e[31m\e[0m") + colorize(obj, &.on_blue.on_red).should eq("\e[41m\e[0m") + colorize(obj, &.always.never).should eq("") + end + + it "toggles off" do + colorize(obj, &.black.toggle(false)).should eq("") + colorize(obj, &.toggle(false).black).should eq("") + end + + it "toggles off and on" do + colorize(obj, io: FakeTTY.new, &.toggle(false).black.toggle(true)).should eq("\e[30m\e[0m") + end + + it "is chainable, `nil` has no effect" do + colorize(obj, &.blue.fore(nil)).should eq("\e[34m\e[0m") + colorize(obj, &.on_blue.back(nil)).should eq("\e[44m\e[0m") + colorize(obj, &.bold.mode(nil)).should eq("\e[1m\e[0m") + colorize(obj, io: FakeTTY.new, &.when(:never).when(nil)).should eq("") + end + + it "raises on unknown foreground color" do + expect_raises ArgumentError, "unknown color: brown" do + colorize(obj, fore: :brown) + end + end + + it "raises on unknown background color" do + expect_raises ArgumentError, "unknown color: brown" do + colorize(obj, back: :brown) + end + end + + it "raises on unknown mode" do + expect_raises ArgumentError, "unknown mode: bad" do + colorize(obj, mode: :bad) + end end - io << "bye" end - io.to_s.should eq("\e[31mhello\e[0;32;1mworld\e[0;31mbye\e[0m") end - it "toggles off" do - "hello".colorize.black.toggle(false).to_s.should eq("hello") - "hello".colorize.toggle(false).black.to_s.should eq("hello") - end + describe Colorize::Style do + describe "#surround" do + it "colorizes with surround stack" do + FakeTTY.new.tap do |io| + with_color.red.surround(io) do |io| + io << "hello" + with_color.green.bold.surround(io) do |io| + io << "world" + end + io << "bye" + end + end.to_s.should eq("\e[31mhello\e[0;32;1mworld\e[0;31mbye\e[0m") + end + + it "colorizes with surround stack having Object" do + FakeTTY.new.tap do |io| + with_color.red.surround(io) do |io| + io << "hello" + "world".colorize.green.bold.to_s io + io << "bye" + end + end.to_s.should eq("\e[31mhello\e[0;32;1mworld\e[0;31mbye\e[0m") + end + + it "colorizes with surround stack having same styles" do + FakeTTY.new.tap do |io| + with_color.red.surround(io) do |io| + io << "hello" + with_color.red.surround(io) do |io| + io << "world" + end + io << "bye" + end + end.to_s.should eq("\e[31mhelloworldbye\e[0m") + end - it "toggles off and on" do - "hello".colorize.toggle(false).black.toggle(true).to_s.should eq("\e[30mhello\e[0m") + it "colorizes with surround stack having default styles" do + io = FakeTTY.new + with_color.surround(io) do |io| + io << "hello" + with_color.surround(io) do |io| + io << "foo" + with_color.green.surround(io) do |io| + io << "fizz" + with_color.surround(io) do |io| + io << "world" + end + io << "buzz" + end + io << "bar" + end + io << "bye" + end + io.to_s.should eq("hellofoo\e[32mfizz\e[0mworld\e[32mbuzz\e[0mbarbye") + end + end end end + diff --git a/spec/std/spec_spec.cr b/spec/std/spec_spec.cr index fb0c04224bd9..8b08468aa6af 100644 --- a/spec/std/spec_spec.cr +++ b/spec/std/spec_spec.cr @@ -101,14 +101,14 @@ describe "Spec matchers" do end describe "Spec" do - describe "use_colors?" do + describe "use_colors" do it "returns if output is colored or not" do - saved = Spec.use_colors? + saved = Spec.use_colors begin - Spec.use_colors = false - Spec.use_colors?.should be_false - Spec.use_colors = true - Spec.use_colors?.should be_true + Spec.use_colors = Colorize::When::Never + Spec.use_colors.should eq(Colorize::When::Never) + Spec.use_colors = Colorize::When::Auto + Spec.use_colors.should eq(Colorize::When::Auto) ensure Spec.use_colors = saved end diff --git a/src/colorize.cr b/src/colorize.cr index 6e5471eee529..acc8fb4e9749 100644 --- a/src/colorize.cr +++ b/src/colorize.cr @@ -1,316 +1,135 @@ -# With Colorize you can change the fore- and background colors and text decorations when rendering text -# on terminals supporting ANSI escape codes. It adds the `colorize` method to `Object` and thus all classes -# as its main interface, which calls `to_s` and surrounds it with the necessary escape codes -# when it comes to obtaining a string representation of the object. +require "colorize/*" + +# With `Colorize` you can change the fore- and background colors and text decorations when rendering text +# on terminals supporting ANSI escape codes. +# +# It adds the `colorize` method to `Object` and thus all classes as its main +# interface, which calls `to_s` and surrounds it with the necessary escape +# codes when it comes to obtaining a string representation of the object. +# +# Or you can use `#with_color` global method, which returns `Style` object +# to represent a style (fore- and background colors and text decorations) on a terminal. `Style#surround` colorize given block outputs with its style. +# +# `Object` and `Style` are mixed in `Builder`, so we can construct a style of both classes in the same way. +# +# Theirs first argument changes the foreground color: # -# Its first argument changes the foreground color: # ``` # require "colorize" # # "foo".colorize(:green) # 100.colorize(:red) # [1, 2, 3].colorize(:blue) +# with_color(:yellow) # ``` # # There are alternative ways to change the foreground color: +# # ``` +# "foo".colorize(fore: :green) +# with_color(fore: :green) # "foo".colorize.fore(:green) +# with_color.fore(:green) # "foo".colorize.green +# with_color.green # ``` # # To change the background color, the following methods are available: +# # ``` +# "foo".colorize(back: :green) +# with_color(back: :green) # "foo".colorize.back(:green) +# with_color.back(:green) # "foo".colorize.on(:green) +# with_color.on(:green) # "foo".colorize.on_green +# with_color.on_green +# ``` +# +# By the way, you can use `String` (via `.parse_color), `Int` (via `Color256.new`) and `Color` instances to specify color: +# +# ``` +# with_color(fore: "red") # use `String` instead. +# +# with_color(fore: "#FDD") # FDD means a color code, not Floppy Dick Drive. +# with_color(fore: "#FFDDDD") # It is same above. +# # These color works on only newer terminals. +# +# with_color(fore: 111) # 111 means 256 color on terminal. +# with_color(fore: "111") # Also, use `String`. +# +# # `Color` instances. +# with_color(fore: Colorize::ColorANSI::Red) +# with_color(fore: Colorize::ColorRGB.parse "#FFDDDD") +# with_color(fore: Colorize::Color256.new 111) # ``` # # It's also possible to change the text decoration: +# # ``` +# "foo".colorize(mode: :underline) +# with_color(mode: :underline) +# "foo".colorize(mode: "underline") +# with_color(mode: "underline") # "foo".colorize.mode(:underline) +# with_color.mode(:underline) # "foo".colorize.underline +# with_color.underline # ``` # -# The `colorize` method returns a `Colorize::Object` instance, +# The `ObjectExtension#colorize` method returns a `Colorize::Object` instance, # which allows chaining methods together: +# # ``` # "foo".colorize.fore(:yellow).back(:blue).mode(:underline) +# with_color.fore(:yellow).back(:blue).mode(:underline) # ``` # -# With the `toggle` method you can temporarily disable adding the escape codes. -# Settings of the instance are preserved however and can be turned back on later: -# ``` -# "foo".colorize(:red).toggle(false) -# # => "foo" without color -# "foo".colorize(:red).toggle(false).toggle(true) -# # => "foo" in red -# ``` +# It outputs escape sequences only if target `IO` is TTY. +# You can change this behavior with `always`, `never`, `auto` and `when` methods: # -# Available colors are: -# ``` -# :black -# :red -# :green -# :yellow -# :blue -# :magenta -# :cyan -# :light_gray -# :dark_gray -# :light_red -# :light_green -# :light_yellow -# :light_blue -# :light_magenta -# :light_cyan -# :white # ``` +# # Output colorized `"foo"` if `STDOUT` is TTY (default) +# puts "foo".colorize(fore: :red) +# puts "foo".colorize(fore: :red).auto # explicit +# with_color(fore: :red).surround { puts "foo" } +# with_color(fore: :red).auto.surround { puts "foo" } # explicit # -# Available text decorations are: -# ``` -# :bold -# :bright -# :dim -# :underline -# :blink -# :reverse -# :hidden +# # Output colorized `"foo"` even if `STDOUT` is not TTY. +# puts "foo".colorize(fore: :red).always +# with_color(fore: :red).always.surround { puts "foo" } +# +# # Output not colorized `"foo"` even if `STDOUT` is TTY. +# puts "foo".colorize(fore: :red).never +# with_color(fore: :red).never.surround { puts "foo" } +# +# # Alternative ways: +# puts "foo".colorize(fore: :red, when: :always) +# with_color(fore: :red, when: :always).surround { puts "foo" } +# puts "foo".colorize(fore: :red).when(:never) +# with_color(fore: :red).when(:always).surround { puts "foo" } +# +# # Last specified policy is only available. +# puts "foo".colorize.always.auto.never # output no escape sequence. +# with_color(fore: :red).never.auto.always.surround { puts "foo" } # output no escape sequence. # ``` module Colorize -end - -def with_color - "".colorize -end - -def with_color(color : Symbol) - "".colorize(color) -end - -module Colorize::ObjectExtensions - def colorize - Colorize::Object.new(self) + module ObjectExtensions + def colorize(fore = nil, back = nil, mode = nil, when policy = nil) + Colorize::Object.new(self) + .fore(fore) + .back(back) + .mode(mode) + .when(policy) + end end +end - def colorize(fore) - Colorize::Object.new(self).fore(fore) - end +def with_color(fore = nil, back = nil, mode = nil, when policy = nil) + Colorize::Style.new fore, back, mode, policy end class Object include Colorize::ObjectExtensions end - -struct Colorize::Object(T) - FORE_DEFAULT = "39" - FORE_BLACK = "30" - FORE_RED = "31" - FORE_GREEN = "32" - FORE_YELLOW = "33" - FORE_BLUE = "34" - FORE_MAGENTA = "35" - FORE_CYAN = "36" - FORE_LIGHT_GRAY = "37" - FORE_DARK_GRAY = "90" - FORE_LIGHT_RED = "91" - FORE_LIGHT_GREEN = "92" - FORE_LIGHT_YELLOW = "93" - FORE_LIGHT_BLUE = "94" - FORE_LIGHT_MAGENTA = "95" - FORE_LIGHT_CYAN = "96" - FORE_WHITE = "97" - - BACK_DEFAULT = "49" - BACK_BLACK = "40" - BACK_RED = "41" - BACK_GREEN = "42" - BACK_YELLOW = "43" - BACK_BLUE = "44" - BACK_MAGENTA = "45" - BACK_CYAN = "46" - BACK_LIGHT_GRAY = "47" - BACK_DARK_GRAY = "100" - BACK_LIGHT_RED = "101" - BACK_LIGHT_GREEN = "102" - BACK_LIGHT_YELLOW = "103" - BACK_LIGHT_BLUE = "104" - BACK_LIGHT_MAGENTA = "105" - BACK_LIGHT_CYAN = "106" - BACK_WHITE = "107" - - MODE_DEFAULT = "0" - MODE_BOLD = "1" - MODE_BRIGHT = "1" - MODE_DIM = "2" - MODE_UNDERLINE = "4" - MODE_BLINK = "5" - MODE_REVERSE = "7" - MODE_HIDDEN = "8" - - MODE_BOLD_FLAG = 1 - MODE_BRIGHT_FLAG = 1 - MODE_DIM_FLAG = 2 - MODE_UNDERLINE_FLAG = 4 - MODE_BLINK_FLAG = 8 - MODE_REVERSE_FLAG = 16 - MODE_HIDDEN_FLAG = 32 - - COLORS = %w(black red green yellow blue magenta cyan light_gray dark_gray light_red light_green light_yellow light_blue light_magenta light_cyan white) - MODES = %w(bold bright dim underline blink reverse hidden) - - def initialize(@object : T) - @fore = FORE_DEFAULT - @back = BACK_DEFAULT - @mode = 0 - @on = true - end - - {% for name in COLORS %} - def {{name.id}} - @fore = FORE_{{name.upcase.id}} - self - end - - def on_{{name.id}} - @back = BACK_{{name.upcase.id}} - self - end - {% end %} - - {% for name in MODES %} - def {{name.id}} - @mode |= MODE_{{name.upcase.id}}_FLAG - self - end - {% end %} - - def fore(color : Symbol) - {% for name in COLORS %} - if color == :{{name.id}} - @fore = FORE_{{name.upcase.id}} - return self - end - {% end %} - - raise ArgumentError.new "unknown color: #{color}" - end - - def back(color : Symbol) - {% for name in COLORS %} - if color == :{{name.id}} - @back = BACK_{{name.upcase.id}} - return self - end - {% end %} - - raise ArgumentError.new "unknown color: #{color}" - end - - def mode(mode : Symbol) - {% for name in MODES %} - if mode == :{{name.id}} - @mode |= MODE_{{name.upcase.id}}_FLAG - return self - end - {% end %} - - raise ArgumentError.new "unknown mode: #{mode}" - end - - def on(color : Symbol) - back color - end - - def toggle(on) - @on = !!on - self - end - - def to_s(io) - surround(io) do - io << @object - end - end - - def inspect(io) - surround(io) do - @object.inspect(io) - end - end - - def surround(io = STDOUT) - must_append_end = append_start(io) - yield io - append_end(io) if must_append_end - end - - STACK = [] of Colorize::Object(String) - - def push(io = STDOUT) - last_color = STACK.last? - - append_start(io, !!last_color) - - STACK.push self - yield io - STACK.pop - - if last_color - last_color.append_start(io, true) - else - append_end(io) - end - end - - protected def append_start(io, reset = false) - return false unless @on - - fore_is_default = @fore == FORE_DEFAULT - back_is_default = @back == BACK_DEFAULT - mode_is_default = @mode == 0 - - if fore_is_default && back_is_default && mode_is_default && !reset - false - else - io << "\e[" - - printed = false - - if reset - io << MODE_DEFAULT - printed = true - end - - unless fore_is_default - io << ";" if printed - io << @fore - printed = true - end - - unless back_is_default - io << ";" if printed - io << @back - printed = true - end - - unless mode_is_default - # Can't reuse MODES constant because it has bold/bright duplicated - {% for name in %w(bold dim underline blink reverse hidden) %} - if (@mode & MODE_{{name.upcase.id}}_FLAG) != 0 - io << ";" if printed - io << MODE_{{name.upcase.id}} - printed = true - end - {% end %} - end - - io << "m" - - true - end - end - - protected def append_end(io) - io << "\e[0m" - end -end diff --git a/src/colorize/builder.cr b/src/colorize/builder.cr new file mode 100644 index 000000000000..46db2e91be0e --- /dev/null +++ b/src/colorize/builder.cr @@ -0,0 +1,233 @@ +require "./color" +require "./mode" +require "./when" + +# `Builder` is a mixin module for `Style` and `Object`. +# +# It provides builder methods for construct a style on terminal. +module Colorize::Builder + # Foreground color. See `Color`. + property fore = ColorANSI::Default + + # Background color. See `Color`. + property back = ColorANSI::Default + + # Activated text decoration modes. See `Mode`. + property mode = Mode::None + + # When to output escape sequence. See `When`. + property :when; @when = When::Auto + + {% for color in ColorANSI.constants %} + # Set `ColorANSI::{{color}}` to `#fore`, then return `self`. + def {{color.underscore}} + fore ColorANSI::{{color}} + end + + # Set `ColorANSI::{{color}}` to `#back`, then return `self`. + def on_{{color.underscore}} + back ColorANSI::{{color}} + end + {% end %} + + {% for mode in Mode.constants.reject { |name| name == "All" || name == "None" } %} + # Activate `Mode::{{mode}}` mode, then return `self`. + def {{mode.underscore}} + mode Mode::{{mode}} + end + {% end %} + + {% for policy in When.constants %} + # Set `When::{{policy}}` to `#when`, then return `self`. + def {{policy.underscore}} + self.when When::{{policy}} + end + {% end %} + + # Set specified *color* to `#fore`, then return `self`. + # + # Available colors are: + # + # ``` + # :black + # :red + # :green + # :yellow + # :blue + # :magenta + # :cyan + # :light_gray + # :dark_gray + # :light_red + # :light_green + # :light_yellow + # :light_blue + # :light_magenta + # :light_cyan + # :white + # ``` + def fore(color : Symbol) + fore ColorANSI.parse?(color.to_s) || raise ArgumentError.new "unknown color: #{color}" + end + + # Set specified *color* to `#fore`, then return `self`. + # + # Such colors are available: + # + # ``` + # "red" + # "green" + # "#FF00FF" + # "#FDD" + # ``` + # + # See `Colorize.parse_color`. + def fore(color : String) + fore Colorize.parse_color color + end + + # Set specified *color* as `Color256` to `#fore`, then return `self`. + def fore(color : Int) + fore Color256.new color + end + + # Set specified *color* to `#fore`, then return `self`. + def fore(color : Color) + @fore = color + self + end + + # Not change `#fore` if *color* is `nil`, and return `self`. + def fore(color : Nil) + self + end + + # Set specified *color* to `#back`, then return `self`. + # + # Available colors are: + # + # ``` + # :black + # :red + # :green + # :yellow + # :blue + # :magenta + # :cyan + # :light_gray + # :dark_gray + # :light_red + # :light_green + # :light_yellow + # :light_blue + # :light_magenta + # :light_cyan + # :white + # ``` + def back(color : Symbol) + back ColorANSI.parse?(color.to_s) || raise ArgumentError.new "unknown color: #{color}" + end + + # Set specified *color* to `#back`, then return `self`. + # + # Such colors are available: + # + # ``` + # "red" + # "green" + # "#FF00FF" + # "#FDD" + # ``` + # + # See `Colorize.parse_color`. + def back(color : String) + back Colorize.parse_color color + end + + # Set specified *color* as `Color256` to `#back`, then return `self`. + def back(color : Int) + back Color256.new color + end + + # Set specified *color* to `#back`, then return `self`. + def back(color : Color) + @back = color + self + end + + # Not change `#back` if *color* is `nil`, and return `self`. + def back(color : Nil) + self + end + + # Alias for `#back`. + def on(color) + back color + end + + # Activate specified *mode*, then return `self`. + # + # Available text decoration modes are: + # + # ``` + # :bold + # :bright + # :dim + # :underline + # :blink + # :reverse + # :hidden + # ``` + # + # See `Mode`. + def mode(mode : Symbol | String) + mode Mode.parse?(mode.to_s) || raise ArgumentError.new "unknown mode: #{mode}" + end + + # Activate specified *mode*, then return `self`. + def mode(mode : Mode) + @mode |= mode + self + end + + # Activate nothing, and return `self`. + def mode(mode : Nil) + self + end + + # Set specified *policy* to `#when`, then return `self`. + # + # Available policies are: + # + # ``` + # :auto + # :always + # :never + # ``` + # + # See `When`. + def when(policy : Symbol | String) + self.when When.parse?(policy.to_s) || raise ArgumentError.new "unknown policy: #{policy}" + end + + # Set specified *policy* to `#when`, then return `self`. + def when(policy : When) + @when = policy + self + end + + # Not change `#when` if *policy* is `nil`, and return `self`. + def when(policy : Nil) + self + end + + # Set `When::Always` to `#when` if *enabled* is `true`, or set `When::Never` to `#when` if *enabled* is `false`, then return `self`. + def toggle(enabled) + self.when enabled ? When::Auto : When::Never + end + + # Return `true` if `#fore`, `#back` and `#mode` is still default. + def all_default? + fore.default? && back.default? && mode.none? + end +end diff --git a/src/colorize/color.cr b/src/colorize/color.cr new file mode 100644 index 000000000000..9be86ec0ddd7 --- /dev/null +++ b/src/colorize/color.cr @@ -0,0 +1,217 @@ +module Colorize + # `Color` is a union of available colors on a terminal. + # + # You can create `Color` by using `Colorize.parse_color` or `Colorize.parse_color?`. + # + # See [Wikipedia's article](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors). + alias Color = ColorANSI | Color256 | ColorRGB + + # Parse `String` *color* as `Color`, or return `nil` when parse failed. + # + # ``` + # # ANSI colors + # Colorize.parse_color? "red" # => Colorize::ColorANSI::Red + # Colorize.parse_color? "light_blue" # => Colorize::ColorANSI::Blue + # + # # 256 colors + # Colorize.parse_color? "42" # => Colorize::Color256(@value=42) + # Colorize.parse_color? "111" # => Colorize::Color256(@value=111) + # + # # 32bit true colors + # Colorize.parse_color? "#123" # => Colorize::ColorRGB(@blue=51, @green=34, @red=17) + # Colorize.parse_color? "#112233" # => Colorize::ColorRGB(@blue=51, @green=34, @red=17) + # + # Colorize.parse_color? "invalid" # => nil + # ``` + # + # See `Color256.parse?` and `ColorRGB.parse?`. + def self.parse_color?(color) + ColorANSI.parse?(color) || Color256.parse?(color) || ColorRGB.parse?(color) + end + + # Parse `String` *color* as `Color`, or raise an error when parse failed. + # + # ``` + # # ANSI colors + # Colorize.parse_color "red" # => Colorize::ColorANSI::Red + # Colorize.parse_color "light_blue" # => Colorize::ColorANSI::Blue + # + # # 256 colors + # Colorize.parse_color "42" # => Colorize::Color256(@value=42) + # Colorize.parse_color "111" # => Colorize::Color256(@value=111) + # + # # 32bit true colors + # Colorize.parse_color "#123" # => Colorize::ColorRGB(@blue=51, @green=34, @red=17) + # Colorize.parse_color "#112233" # => Colorize::ColorRGB(@blue=51, @green=34, @red=17) + # + # Colorize.parse_color "invalid" # raises ArgumentError + # ``` + # + # See `Color256.parse` and `ColorRGB.parse`. + def self.parse_color(color) + self.parse_color?(color) || raise "invalid color: #{color}" + end + + # `ColorANSI` represents 8-color defined in ANSI escape sequence. + # + # The `value` means foreground color code. + enum ColorANSI + Default = 39 + Black = 30 + Red = 31 + Green = 32 + Yellow = 33 + Blue = 34 + Magenta = 35 + Cyan = 36 + LightGray = 37 + DarkGray = 90 + LightRed = 91 + LightGreen = 92 + LightYellow = 93 + LightBlue = 94 + LightMagenta = 95 + LightCyan = 96 + White = 97 + + # Return foreground color code. + def fore_code + value.to_s + end + + # Return background color code. + def back_code + (value + 10).to_s + end + end + + # `Color256` represents 256 color on a terminal. + # + # - `0x00..0x07`: standard colors (as in `"\e[30m".."\e[37m"`) + # - `0x08..0x0F`: high intensity colors (as in `"\e[90m".."\e[97m"`) + # - `0x10..0xE7`: `6 * 6 * 6 = 216` colors (calculated by `16 + r * 36 + g * 6 + b`) + # - `0xE8..0xFF`: grayscale from black to white in 24 steps + # + # NOTE: It is also converted from `ColorRGB` by `ColorRGB#to_color256`. + struct Color256 + # Color code value. + getter value : UInt8 + + # Create `Color256` from given *value*. + def initialize(value) + @value = value.to_u8 + end + + # Parse and create a new `Color256` from `String` *value*, or return `nil` when parse failed. + # + # ``` + # Colorize::Color256.parse? "111" # => Colorize::Color256(@value=111) + # Colorize::Color256.parse? "12345" # => nil + # ``` + def self.parse?(value) + if value = value.to_u8? + new value + end + end + + # Parse and create a new `Color256` from `String` *value*, or raise an error when parse failed. + # + # ``` + # Colorize::Color256.parse "111" # => Colorize::Color256(@value=111) + # Colorize::Color256.parse "12345" # raises ArgumentError + # ``` + def self.parse(value) + parse?(css_color_code) || raise ArgumentError.new "invalid color: #{css_color_code}" + end + + # Return foreground color code. + def fore_code + "38;5;#{value}" + end + + # Return background color code. + def back_code + "48;5;#{value}" + end + + # :nodoc: + def default? + false + end + end + + # `ColorRGB` represents 24bit true color on a terminal. + # + # It is useful but supported by only newer terminals. + struct ColorRGB + # Red value + getter red : UInt8 + # Green value + getter green : UInt8 + # Blue value + getter blue : UInt8 + + # Create `ColorRGB` with *red*, *green* and *blue* values. + def initialize(red, green, blue) + @red = red.to_u8 + @green = green.to_u8 + @blue = blue.to_u8 + end + + # Parse and create a new `ColorRGB` from *css_color_code* like `"#112233"` and `"#123"`, or return `nil` when parse failed. + # + # ``` + # Colorize::ColorRGB.parse? "#112233" # => Colorize::Color256(@blue=51, @green=34, @red=17) + # Colorize::ColorRGB.parse? "#123" # => Colorize::Color256(@blue=51, @green=34, @red=17) + # Colorize::ColorRGB.parse? "112233" # => nil + # ``` + def self.parse?(css_color_code) + if css_color_code =~ /\A#(?:[[:xdigit:]]{6}|[[:xdigit:]]{3})\Z/ + if css_color_code.size == 4 + r = css_color_code[1].to_i(16).tap { |r| break r * 16 + r } + g = css_color_code[2].to_i(16).tap { |g| break g * 16 + g } + b = css_color_code[3].to_i(16).tap { |b| break b * 16 + b } + else + r = css_color_code[1..2].to_i 16 + g = css_color_code[3..4].to_i 16 + b = css_color_code[5..6].to_i 16 + end + ColorRGB.new r, g, b + end + end + + # Parse and create a new `ColorRGB` from *css_color_code* like `"#112233"` and `"#123"`, or raise an error when parse failed. + # + # ``` + # Colorize::ColorRGB.parse "#112233" # => Colorize::Color256(@blue=51, @green=34, @red=17) + # Colorize::ColorRGB.parse "#123" # => Colorize::Color256(@blue=51, @green=34, @red=17) + # Colorize::ColorRGB.parse "112233" # raises ArgumentError + # ``` + def self.parse(css_color_code) + parse?(css_color_code) || raise ArgumentError.new "invalid color: #{css_color_code}" + end + + # Return foreground color code. + def fore_code + "38;2;#{red};#{green};#{blue}" + end + + # Return background color code. + def back_code + "48;2;#{red};#{green};#{blue}" + end + + # Convert to `Color256`. + def to_color256 + r = (red.to_f / 256 * 6).to_i + g = (green.to_f / 256 * 6).to_i + b = (blue.to_f / 256 * 6).to_i + Color256.new 16 + r * 36 + g * 6 + b + end + + # :nodoc: + def default? + false + end + end +end diff --git a/src/colorize/io.cr b/src/colorize/io.cr new file mode 100644 index 000000000000..19bd89458c15 --- /dev/null +++ b/src/colorize/io.cr @@ -0,0 +1,87 @@ +require "./builder" + +module Colorize + # Output escape sequence to reset. + def self.reset(io) + io << reset + end + + # Return escape sequence to reset. + def self.reset + "\e[0m" + end + + # Keep last style for nesting `.surround`. + @@last_style : Builder? = nil + + # Colorize the content in the block with this style. + # + # It outputs an escape sequence, then invokes the block. After all, it outputs reset escape sequence if needed. + # + # This method has a stack internally, so it keeps colorizing if nested. + + # :nodoc: + def self.surround(style, io = STDOUT) : Nil + last_style = @@last_style + if style.when.colorizable_io?(io) && style != last_style + must_reset = write_style style, io, reset: !(last_style.nil? || last_style.all_default?) + end + @@last_style = style + + begin + yield io + ensure + @@last_style = last_style + if must_reset + if last_style + write_style last_style, io, reset: !style.all_default? + else + reset io + end + end + end + end + + # Write escape sequence to colorize with *style*. + # If *reset* is `true`, it invokes `#reset` before applying *style*. + # + # It is used by `Style` and `Object` internally. + + # :nodoc: + def self.write_style(style, io, reset = false) + return false if style.all_default? && !reset + + io << "\e[" + + printed = false + + if reset + io << "0" + printed = true + end + + unless style.fore.default? + io << ";" if printed + io << style.fore.fore_code + printed = true + end + + unless style.back.default? + io << ";" if printed + io << style.back.back_code + printed = true + end + + unless style.mode.none? + style.mode.codes do |i| + io << ";" if printed + io << i + printed = true + end + end + + io << "m" + + true + end +end diff --git a/src/colorize/mode.cr b/src/colorize/mode.cr new file mode 100644 index 000000000000..fb6909f7443e --- /dev/null +++ b/src/colorize/mode.cr @@ -0,0 +1,16 @@ +@[Flags] +enum Colorize::Mode + Bold = 1 << 0 + Bright = Bold + Dim = 1 << 1 + Underline = 1 << 3 + Blink = 1 << 4 + Reverse = 1 << 6 + Hidden = 1 << 7 + + def codes + 8.times do |i| + yield i + 1 unless value & (1 << i) == 0 + end + end +end diff --git a/src/colorize/object.cr b/src/colorize/object.cr new file mode 100644 index 000000000000..1d3ba099fff4 --- /dev/null +++ b/src/colorize/object.cr @@ -0,0 +1,25 @@ +require "./builder" +require "./io" + +# `Object` wraps given object to colorize with a style on terminal. +# +# It is usual created by `ObjectExtension#colorize`. +struct Colorize::Object(T) + include Builder + + # Wrap a *object*. + def initialize(@object : T) + end + + # Return wrapped object. + getter object + + # Output colorized object with this style. + def to_s(io) + Colorize.surround(self, io) do + @object.to_s io + end + end + + def_equals_and_hash fore, back, mode, :when, object +end diff --git a/src/colorize/style.cr b/src/colorize/style.cr new file mode 100644 index 000000000000..13a5d473e816 --- /dev/null +++ b/src/colorize/style.cr @@ -0,0 +1,60 @@ +require "./builder" +require "./io" + +# `Style` represents a colorize style on the terminal. +# +# ``` +# # Create a new style by the constructor. +# style = Colorize::Style.new(:red, :blue, :underline) +# +# # Or, we can use `Builder`'s methods for construction. +# style = Colorize::Style.new +# .fore(:red) +# .back(:blue) +# .mode(:underline) +# +# # Get an escape sequence to colorize with this style. +# style.to_s # => "\e[31;44;4m" +# +# # Colorize the content in a block with this style. +# style.surround do +# puts "Hello, World!" +# end +# ``` +struct Colorize::Style + include Builder + + # Creates a new instance with *fore*, *back* and *mode*. + # + # All parameter is passed to each setter methods. + def initialize(fore = nil, back = nil, mode = nil, when policy = nil) + fore fore + back back + mode mode + self.when policy + end + + # Get an escape sequence to colorize with this style. + # + # NOTE: This method does not check given *io* is TTY when `#policy` is `When::Auto`. + def to_s(io) + Colorize.write_style self, io + end + + # Colorize the content in the block with this style. + # + # It outputs an escape sequence, then invokes the block. After all, it outputs reset escape sequence if needed. + # + # This method has a stack internally, so it keeps colorizing if nested. + def surround(io = STDOUT) + Colorize.surround(self, io) { |io| yield io } + end + + # DEPRECATED: use `#surround`. This method will be removed after 0.21.0. + def push(io = STDOUT) + {{ puts "`Colorize::Style#push` is deprecated and will be removed after 0.21.0, use `Colorize::Style#surround` instead".id }} + surround(io) { |io| yield io } + end + + def_equals_and_hash fore, back, mode, :when +end diff --git a/src/colorize/when.cr b/src/colorize/when.cr new file mode 100644 index 000000000000..f591fc0336fc --- /dev/null +++ b/src/colorize/when.cr @@ -0,0 +1,17 @@ +# `When` is a policy to output escape sequence. +enum Colorize::When + # Output escape sequence if given *io* is TTY. + Auto + + # Always output escape sequence even if given *io* is not TTY. + Always + + # Not output escape sequence even if given *io* is TTY. + Never + + # Return `true` if given *io* is colorizable on this policy. + # See `Auto`, `Always` and `Never`. + def colorizable_io?(io) + always? || auto? && io.responds_to?(:tty?) && io.tty? + end +end diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index d446a0ab98b3..298dc109198a 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -52,7 +52,7 @@ class Crystal::Command private getter options def initialize(@options : Array(String)) - @color = true + @color = Colorize::When::Auto @stats = @time = false end @@ -344,9 +344,15 @@ class Crystal::Command end end + opts.on("--color auto|always|never", "Colorize the output") do |policy| + color = Colorize::When.parse policy + @color = color + compiler.color = color + end + opts.on("--no-color", "Disable colored output") do - @color = false - compiler.color = false + @color = Colorize::When::Never + compiler.color = Colorize::When::Never end unless no_codegen @@ -477,9 +483,14 @@ class Crystal::Command puts opts exit end + opts.on("--color auto|always|never", "Colorize the output") do |policy| + color = Colorize::When.parse policy + @color = color + compiler.color = color + end opts.on("--no-color", "Disable colored output") do - @color = false - compiler.color = false + @color = Colorize::When::Never + compiler.color = Colorize::When::Never end opts.invalid_option { } end @@ -495,7 +506,7 @@ class Crystal::Command private def error(msg, exit_code = 1) # This is for the case where the main command is wrong - @color = false if ARGV.includes?("--no-color") + @color = Colorize::When::Never if ARGV.includes?("--no-color") Crystal.error msg, @color, exit_code: exit_code end end diff --git a/src/compiler/crystal/command/format.cr b/src/compiler/crystal/command/format.cr index 7c942e16bd4a..75cf9f740ad4 100644 --- a/src/compiler/crystal/command/format.cr +++ b/src/compiler/crystal/command/format.cr @@ -34,8 +34,12 @@ class Crystal::Command exit end + opts.on("--color auto|always|never", "Colorize the output") do |policy| + @color = Colorize::When.parse policy + end + opts.on("--no-color", "Disable colored output") do - @color = false + @color = Colorize::When::Never end end @@ -86,8 +90,8 @@ class Crystal::Command print result STDOUT.flush rescue ex : InvalidByteSequenceError - print "Error: ".colorize.toggle(@color).red.bold - print "source is not a valid Crystal source file: ".colorize.toggle(@color).bold + print "Error: ".colorize.when(@color).red.bold + print "source is not a valid Crystal source file: ".colorize.when(@color).bold puts ex.message exit 1 rescue ex : Crystal::SyntaxException @@ -114,8 +118,8 @@ class Crystal::Command File.write(filename, result) rescue ex : InvalidByteSequenceError - print "Error: ".colorize.toggle(@color).red.bold - print "file '#{Crystal.relative_filename(filename)}' is not a valid Crystal source file: ".colorize.toggle(@color).bold + print "Error: ".colorize.when(@color).red.bold + print "file '#{Crystal.relative_filename(filename)}' is not a valid Crystal source file: ".colorize.when(@color).bold puts ex.message exit 1 rescue ex : Crystal::SyntaxException @@ -162,21 +166,21 @@ class Crystal::Command check_files << FormatResult.new(filename, FormatResult::Code::FORMAT) else File.write(filename, result) - STDOUT << "Format".colorize(:green).toggle(@color) << " " << filename << "\n" + STDOUT << "Format".colorize(:green).when(@color) << " " << filename << "\n" end rescue ex : InvalidByteSequenceError if check_files check_files << FormatResult.new(filename, FormatResult::Code::INVALID_BYTE_SEQUENCE) else - print "Error: ".colorize.toggle(@color).red.bold - print "file '#{Crystal.relative_filename(filename)}' is not a valid Crystal source file: ".colorize.toggle(@color).bold + print "Error: ".colorize.when(@color).red.bold + print "file '#{Crystal.relative_filename(filename)}' is not a valid Crystal source file: ".colorize.when(@color).bold puts ex.message end rescue ex : Crystal::SyntaxException if check_files check_files << FormatResult.new(filename, FormatResult::Code::SYNTAX) else - STDOUT << "Syntax Error:".colorize(:yellow).toggle(@color) << " " << ex.message << " at " << filename << ":" << ex.line_number << ":" << ex.column_number << "\n" + STDOUT << "Syntax Error:".colorize(:yellow).when(@color) << " " << ex.message << " at " << filename << ":" << ex.line_number << ":" << ex.column_number << "\n" end rescue ex if check_files @@ -190,7 +194,7 @@ class Crystal::Command end private def couldnt_format(file) - STDERR << "Error:".colorize(:red).toggle(@color) << ", " << + STDERR << "Error:".colorize(:red).when(@color) << ", " << "couldn't format " << file << ", please report a bug including the contents of it: https://github.com/crystal-lang/crystal/issues" end end diff --git a/src/compiler/crystal/command/spec.cr b/src/compiler/crystal/command/spec.cr index 49f1803dfc9b..7a1d3e9a846b 100644 --- a/src/compiler/crystal/command/spec.cr +++ b/src/compiler/crystal/command/spec.cr @@ -60,6 +60,10 @@ class Crystal::Command end end + unless @color.auto? + options << "--color=#{@color}" + end + source_filename = File.expand_path("spec") source = target_filenames.map { |filename| %(require "./#{filename}") }.join("\n") diff --git a/src/compiler/crystal/compiler.cr b/src/compiler/crystal/compiler.cr index 3f22f504099e..558848572b53 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -57,8 +57,8 @@ module Crystal # Sets the mattr (features). Check LLVM docs to learn about this. property mattr : String? - # If `false`, color won't be used in output messages. - property? color = true + # Colorize policy. See `Colorize::When`. + property color = Colorize::When::Auto # If `true`, skip cleanup process on semantic analysis. property? no_cleanup = false @@ -166,7 +166,7 @@ module Crystal program.flags << "debug" unless debug.none? program.flags.merge! @flags program.wants_doc = wants_doc? - program.color = color? + program.color = color program.stdout = stdout program.show_error_trace = show_error_trace? program @@ -381,7 +381,7 @@ module Crystal end private def colorize(obj) - obj.colorize.toggle(@color) + obj.colorize.when(@color) end # An LLVM::Module with information to compile it. diff --git a/src/compiler/crystal/exception.cr b/src/compiler/crystal/exception.cr index d91d9522e978..72e9eafab147 100644 --- a/src/compiler/crystal/exception.cr +++ b/src/compiler/crystal/exception.cr @@ -3,7 +3,7 @@ require "colorize" module Crystal abstract class Exception < ::Exception - property? color = false + property color = Colorize::When::Auto @filename : String | VirtualFile | Nil @@ -47,11 +47,11 @@ module Crystal end def colorize(obj) - obj.colorize.toggle(@color) + obj.colorize.when(@color) end def with_color - ::with_color.toggle(@color) + ::with_color.when(@color) end def replace_leading_tabs_with_spaces(line) diff --git a/src/compiler/crystal/program.cr b/src/compiler/crystal/program.cr index 02d334f96989..affb9587c3a4 100644 --- a/src/compiler/crystal/program.cr +++ b/src/compiler/crystal/program.cr @@ -66,8 +66,8 @@ module Crystal # If `true`, doc comments are attached to types and methods. property? wants_doc = false - # If `true`, error messages can be colorized - property? color = true + # Colorize policy. See `Colorize::When`. + property color = Colorize::When::Auto # All required files. The set stores absolute files. This way # files loaded by `require` nodes are only processed once. @@ -486,7 +486,7 @@ module Crystal # Colorizes the given object, depending on whether this program # is configured to use colors. def colorize(obj) - obj.colorize.toggle(@color) + obj.colorize.when(@color) end private def abstract_value_type(type) diff --git a/src/compiler/crystal/semantic/exception.cr b/src/compiler/crystal/semantic/exception.cr index e0fd78d836c2..559d9059e9a4 100644 --- a/src/compiler/crystal/semantic/exception.cr +++ b/src/compiler/crystal/semantic/exception.cr @@ -10,7 +10,7 @@ module Crystal @size : Int32 def color=(color) - @color = !!color + @color = color inner.try &.color=(color) end diff --git a/src/compiler/crystal/tools/playground/server.cr b/src/compiler/crystal/tools/playground/server.cr index af3f2860be26..f3692668b2c1 100644 --- a/src/compiler/crystal/tools/playground/server.cr +++ b/src/compiler/crystal/tools/playground/server.cr @@ -49,7 +49,7 @@ module Crystal::Playground ] output_filename = tempfile "play-#{@session_key}-#{tag}" compiler = Compiler.new - compiler.color = false + compiler.color = Colorize::When::Never begin @logger.info "Instrumented code compilation started (session=#{@session_key}, tag=#{tag})." result = compiler.compile sources, output_filename diff --git a/src/compiler/crystal/tools/print_hierarchy.cr b/src/compiler/crystal/tools/print_hierarchy.cr index 3882f14c47e5..66930b13e7a5 100644 --- a/src/compiler/crystal/tools/print_hierarchy.cr +++ b/src/compiler/crystal/tools/print_hierarchy.cr @@ -25,7 +25,7 @@ module Crystal compute_targets(@program.types, exp, false) end - with_color.light_gray.bold.push(STDOUT) do + with_color.light_gray.bold.surround(STDOUT) do print_type @program.object end end @@ -124,7 +124,7 @@ module Crystal if (type.is_a?(NonGenericClassType) || type.is_a?(GenericClassInstanceType)) && !type.is_a?(PointerInstanceType) && !type.is_a?(ProcInstanceType) size = @llvm_typer.size_of(@llvm_typer.llvm_struct_type(type)) - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print " (" print size.to_s print " bytes)" @@ -174,7 +174,7 @@ module Crystal print " " end - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print name.ljust(max_name_size) print " : " print var @@ -209,13 +209,13 @@ module Crystal print " " end - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print ivar.name.ljust(max_name_size) print " : " if ivar_type = ivar.type? print ivar_type.to_s.ljust(max_type_size) size = @llvm_typer.size_of(@llvm_typer.llvm_embedded_type(ivar_type)) - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print " (" print size.to_s.rjust(max_bytes_size) print " bytes)" @@ -261,7 +261,7 @@ module Crystal end def with_color - ::with_color.toggle(@program.color?) + ::with_color.when(@program.color) end end diff --git a/src/spec.cr b/src/spec.cr index cbc605a6c812..2e7ff6ce38a8 100644 --- a/src/spec.cr +++ b/src/spec.cr @@ -97,8 +97,11 @@ OptionParser.parse! do |opts| opts.on("-v", "--verbose", "verbose output") do Spec.override_default_formatter(Spec::VerboseFormatter.new) end + opts.on("--color auto|always|never", "Colorize the output") do |color| + Spec.use_colors = Colorize::When.parse color + end opts.on("--no-color", "Disable colored output") do - Spec.use_colors = false + Spec.use_colors = Colorize::When::Never end opts.unknown_args do |args| end diff --git a/src/spec/context.cr b/src/spec/context.cr index 39e57733de4c..d7bfb03cc669 100644 --- a/src/spec/context.cr +++ b/src/spec/context.cr @@ -95,7 +95,7 @@ module Spec if ex.is_a?(AssertionFailed) puts - puts " # #{Spec.relative_file(ex.file)}:#{ex.line}".colorize.cyan + puts " # #{Spec.relative_file(ex.file)}:#{ex.line}".colorize.when(Spec.use_colors).cyan end end end @@ -110,11 +110,11 @@ module Spec puts "Top #{Spec.slowest} slowest examples (#{top_n_time} seconds, #{percent.round(2)}% of total time):" top_n.each do |res| puts " #{res.description}" + res_elapsed = res.elapsed.not_nil!.total_seconds.to_s - if Spec.use_colors? - res_elapsed = res_elapsed.colorize.bold - end - puts " #{res_elapsed} seconds #{Spec.relative_file(res.file)}:#{res.line}" + print " " + print res_elapsed.colorize.when(Spec.use_colors) + puts " seconds #{Spec.relative_file(res.file)}:#{res.line}" end end @@ -137,8 +137,8 @@ module Spec puts "Failed examples:" puts failures_and_errors.each do |fail| - print "crystal spec #{Spec.relative_file(fail.file)}:#{fail.line}".colorize.red - puts " # #{fail.description}".colorize.cyan + print "crystal spec #{Spec.relative_file(fail.file)}:#{fail.line}".colorize.when(Spec.use_colors).red + puts " # #{fail.description}".colorize.when(Spec.use_colors).cyan end end end diff --git a/src/spec/dsl.cr b/src/spec/dsl.cr index 0ca2168a9004..dcc075740021 100644 --- a/src/spec/dsl.cr +++ b/src/spec/dsl.cr @@ -17,24 +17,12 @@ module Spec pending: '*', } - @@use_colors = true - # :nodoc: - def self.color(str, status) - if use_colors? - str.colorize(COLORS[status]) - else - str - end - end + class_property use_colors = Colorize::When::Auto # :nodoc: - def self.use_colors? - @@use_colors - end - - # :nodoc: - def self.use_colors=(@@use_colors) + def self.color(str, status) + str.colorize(fore: COLORS[status], when: use_colors) end # :nodoc: