diff --git a/Makefile b/Makefile index dc675a05a148..555c971fb348 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,21 @@ 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') +# TODO: Uncomment below after next release. +# FLAGS := --color=$(color)$(if $(release), --release)$(if $(stats), --stats)$(if $(threads), --threads $(threads))$(if $(debug), -d) FLAGS := $(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 +77,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 4d0761628ad0..67623168568b 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/compiler/semantic/did_you_mean_spec.cr b/spec/compiler/semantic/did_you_mean_spec.cr index 4f0e67d7f467..9dd1eaa20ece 100644 --- a/spec/compiler/semantic/did_you_mean_spec.cr +++ b/spec/compiler/semantic/did_you_mean_spec.cr @@ -237,8 +237,6 @@ describe "Semantic: did you mean" do end it "suggests a better alternative to logical operators (#2715)" do - message = "undefined method 'and'" - message = " (did you mean '&&'?)".colorize.yellow.bold.to_s assert_error %( def rand(x : Int32) end @@ -252,7 +250,7 @@ describe "Semantic: did you mean" do if "a".bytes and 1 1 end - ), message + ), "did you mean '&&'?" end it "says did you mean in instance var declaration" do diff --git a/spec/std/colorize_spec.cr b/spec/std/colorize_spec.cr index dc80dd557e7b..277305b33061 100644 --- a/spec/std/colorize_spec.cr +++ b/spec/std/colorize_spec.cr @@ -1,148 +1,481 @@ require "spec" require "colorize" -describe "colorize" do - it "colorizes without change" do - "hello".colorize.to_s.should eq("hello") - end +private class FakeTTY < IO::Memory + include Colorize::ColorizableIO - 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") - end + @colorize_when = Colorize::When::Always - 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 + property? tty = false - 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 + INSTANCE = new +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 +private def colorize(obj, tty = true, colorize_when = Colorize::When::Always, **args) + io = FakeTTY::INSTANCE - it "colorizes foreground with background" do - "hello".colorize.blue.on_green.to_s.should eq("\e[34;42mhello\e[0m") + begin + io.colorize_when = colorize_when + io.tty = tty + if obj + io << yield obj.colorize **args + else + io.surround(yield with_color **args) { } + end + ensure + io.colorize_when = Colorize::When::Always + io.tty = false end - 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 + io.to_s.tap { io.clear } +end - 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 +private def colorize(obj, **args) + colorize obj, **args, &.itself +end - it "colorizes mode with symbol" do - "hello".colorize.mode(:bold).to_s.should eq("\e[1mhello\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 + + {% for ground in %w(fore back) %} + {% prefix = "fore" == ground ? "".id : "on_".id %} + {% carry = "fore" == ground ? 0 : 10 %} + + it "colorize #{{{ground}}}ground with default color" do + colorize(obj, &.{{prefix}}default).should eq("") + + colorize(obj, &.{{ground.id}}(:default)).should eq("") + colorize(obj, &.{{ground.id}}("default")).should eq("") + colorize(obj, &.{{ground.id}}(Colorize::ColorANSI::Default)).should eq("") + + {% if "back" == ground %} + colorize(obj, &.on(:default)).should eq("") + colorize(obj, &.on("default")).should eq("") + colorize(obj, &.on(Colorize::ColorANSI::Default)).should eq("") + {% 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 "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" + + colorize(obj, &.{{prefix}}{{color.underscore}}).should eq(ans) + + 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) + + {% 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 %} + + 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 #{{{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) + + {% 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 + + 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, tty: false, colorize_when: :auto, &.black).should eq("") + colorize(obj, tty: false, colorize_when: "auto", &.black).should eq("") + colorize(obj, tty: false, colorize_when: Colorize::When::Auto, &.black).should eq("") + + colorize(obj, tty: true, colorize_when: :auto, &.black).should eq("\e[30m\e[0m") + colorize(obj, tty: true, colorize_when: "auto", &.black).should eq("\e[30m\e[0m") + colorize(obj, tty: true, colorize_when: Colorize::When::Auto, &.black).should eq("\e[30m\e[0m") + end + + it "colorizes always" do + colorize(obj, tty: false, colorize_when: :always, &.black).should eq("\e[30m\e[0m") + colorize(obj, tty: false, colorize_when: "always", &.black).should eq("\e[30m\e[0m") + colorize(obj, tty: false, colorize_when: Colorize::When::Always, &.black).should eq("\e[30m\e[0m") + + colorize(obj, tty: true, colorize_when: :always, &.black).should eq("\e[30m\e[0m") + colorize(obj, tty: true, colorize_when: "always", &.black).should eq("\e[30m\e[0m") + colorize(obj, tty: true, colorize_when: Colorize::When::Always, &.black).should eq("\e[30m\e[0m") + end + + it "colorizes never" do + colorize(obj, tty: false, colorize_when: :never, &.black).should eq("") + colorize(obj, tty: false, colorize_when: "never", &.black).should eq("") + colorize(obj, tty: false, colorize_when: Colorize::When::Never, &.black).should eq("") + + colorize(obj, tty: true, colorize_when: :never, &.black).should eq("") + colorize(obj, tty: true, colorize_when: "never", &.black).should eq("") + colorize(obj, tty: true, colorize_when: Colorize::When::Never, &.black).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") + 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") + 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 foreground color" do - expect_raises ArgumentError, "Unknown color: brown" do - "hello".colorize(:brown) + it "raises on unknown mode" do + expect_raises ArgumentError, "unknown mode: bad" do + colorize(obj, mode: :bad) + end + end + + it "toggles to disable" do + colorize(obj, fore: :red, &.toggle(false)).should eq("") + end + + it "toggles to disable, then enable" do + colorize(obj, fore: :red, &.toggle(false).toggle(true)).should eq("\e[31m\e[0m") + end end end - it "raises on unknown background color" do - expect_raises ArgumentError, "Unknown color: brown" do - "hello".colorize.back(:brown) + describe Colorize::IOExtension do + describe "colorizable" do + it "creates a new Colorize::ColorizableIO instance" do + original = IO::Memory.new + colorizable = original.to_colorizable + colorizable.should be_a(Colorize::ColorizableIO) + colorizable.should_not be(original) + colorizable.should be_a(Colorize::IO) + colorizable.as(Colorize::IO).io.should be(original) + end + + it "s default policy is always" do + IO::Memory.new.to_colorizable.colorize_when.should eq(Colorize::When::Always) + end + + it "creates a new Colorize::ColorizableIO instance with specified policy" do + original = IO::Memory.new + colorizable = original.to_colorizable(:never) + colorizable.should_not be(original) + colorizable.colorize_when.should eq(Colorize::When::Never) + end + + it "returns itself if it is a Colorize::ColorizableIO" do + original = FakeTTY.new + colorizable = original.to_colorizable + colorizable.should be_a(Colorize::ColorizableIO) + colorizable.should be(original) + end + + it "returns itself if it is a Colorize::ColorizableIO with original policy" do + original = FakeTTY.new + colorizable = original.to_colorizable(:never) + colorizable.should be_a(Colorize::ColorizableIO) + colorizable.should be(original) + colorizable.colorize_when.should eq(Colorize::When::Always) + end end end - it "raises on unknown mode" do - expect_raises ArgumentError, "Unknown mode: bad" do - "hello".colorize.mode(:bad) + describe Colorize::ColorizableIO do + it "IO::FileDescriptor is a Colorize::ColorizableIO" do + File.open(__FILE__) do |f| + f.should be_a(Colorize::ColorizableIO) + end end - end - it "inspects" do - "hello".colorize(:red).inspect.should eq("\e[31m\"hello\"\e[0m") - end + describe "#colorize_when" do + it "default value on IO::FileDescriptor is Colorize::When::Auto" do + File.open(__FILE__) do |f| + f.colorize_when.should eq(Colorize::When::Auto) + end + end - it "colorizes io with method" do - io = IO::Memory.new - with_color.red.surround(io) do - io << "hello" + it "invoke block with specified policy" do + tty = FakeTTY.new + tty.colorize_when(Colorize::When::Never) do |io| + io.colorize_when.should eq(Colorize::When::Never) + end + tty.colorize_when.should eq(Colorize::When::Always) + end end - io.to_s.should eq("\e[31mhello\e[0m") - end - it "colorizes io with symbol" do - io = IO::Memory.new - with_color(:red).surround(io) do - io << "hello" + describe "#colorize_when=" do + it "sets policies" do + tty = FakeTTY.new + {% for policy in Colorize::When.constants %} + tty.colorize_when = Colorize::When::{{policy}} + tty.colorize_when.should eq(Colorize::When::{{policy}}) + tty.colorize_when = {{policy.underscore.stringify}} + tty.colorize_when.should eq(Colorize::When::{{policy}}) + tty.colorize_when = {{policy.underscore.symbolize}} + tty.colorize_when.should eq(Colorize::When::{{policy}}) + {% end %} + end + + it "raises on unknown policy symbol" do + expect_raises ArgumentError, "unknown policy: bad" do + FakeTTY.new.colorize_when = :bad + end + end + + it "raises on unknown policy string" do + expect_raises ArgumentError, "unknown policy: bad" do + FakeTTY.new.colorize_when = "bad" + end + end end - io.to_s.should eq("\e[31mhello\e[0m") - end - 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" + describe "#surround" do + it "colorizes with surround stack" do + FakeTTY.new.tap do |io| + io.surround(with_color.red) do |io| + io << "hello" + io.surround(with_color.green.bold) 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| + io.surround(with_color.red) do |io| + io << "hello" + io << "world".colorize.green.bold + 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| + io.surround(with_color.red) do |io| + io << "hello" + io.surround(with_color.red) do |io| + io << "world" + end + io << "bye" + end + end.to_s.should eq("\e[31mhelloworldbye\e[0m") + end + + it "colorizes with surround stack having default styles" do + FakeTTY.new.tap do |io| + io.surround(with_color) do |io| + io << "hello" + io.surround(with_color) do |io| + io << "foo" + io.surround(with_color.green) do |io| + io << "fizz" + io.surround(with_color) do |io| + io << "world" + end + io << "buzz" + end + io << "bar" + end + io << "bye" + end + end.to_s.should eq("hellofoo\e[32mfizz\e[0mworld\e[32mbuzz\e[0mbarbye") 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" + describe Colorize::Builder do + describe "#<<" do + it "accepts some objects" do + io = Colorize::Builder.new + (io << "foo" << :foo << 1).should be(io) + + io.@contents.size.should eq(1) + io.@contents[0].to_s.should eq("foofoo1") + end + + it "accepts Colorize::Object" do + io = Colorize::Builder.new + (io << "foo".colorize.red << "bar".colorize.blue).should be(io) + + io.@contents.size.should eq(2) + io.@contents[0].should eq("foo".colorize.red) + io.@contents[1].should eq("bar".colorize.blue) + end + + it "accepts mixed objects" do + io = Colorize::Builder.new + (io << 1.1 << "foo".colorize.red << :bar << 42).should be(io) + + io.@contents.size.should eq(3) + io.@contents[0].to_s.should eq("1.1") + io.@contents[1].should eq("foo".colorize.red) + io.@contents[2].to_s.should eq("bar42") 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 "#surround" do + it "creates a new builder" do + io = Colorize::Builder.new + io.surround(with_color.red) do |io2| + io.should_not be(io2) + end + end + + it "surrounds objects" do + io = Colorize::Builder.new + io.surround(with_color.red) do |io2| + io2 << "foo".colorize.bold + end + + io.@contents.size.should eq(1) + io.@contents[0].should be_a(Colorize::Object(Colorize::Builder)) + + io2 = io.@contents[0].as(Colorize::Object(Colorize::Builder)).object + io2.@contents.size.should eq(1) + io2.@contents[0].should eq("foo".colorize.bold) + end + end + + describe "#to_s" do + it "outputs objects" do + io = Colorize::Builder.new + io << "foo" << :foo << 1 + io.to_s.should eq("foofoo1") + end + + it "outputs Colorize::Object" do + io = Colorize::Builder.new + io << "foo".colorize.red << :bar.colorize.blue + io.to_s.should eq("\e[31mfoo\e[0m\e[34mbar\e[0m") + end + + it "outputs mixed objects" do + io = Colorize::Builder.new + io << "foo".colorize.red << :bar << 42 + io.to_s.should eq("\e[31mfoo\e[0mbar42") + end - it "toggles off and on" do - "hello".colorize.toggle(false).black.toggle(true).to_s.should eq("\e[30mhello\e[0m") + it "outputs mixed objects, but colorizes dependeing on io" do + io = Colorize::Builder.new + io << "foo".colorize.red << :bar << 42 + String.build do |str| + io.to_s str.to_colorizable(:never) + end.should eq("foobar42") + end + end + + describe "#to_s_without_colorize" do + it "does not colorize" do + io = Colorize::Builder.new + io << "foo".colorize.red << :bar << 42 + io.to_s_without_colorize.should eq("foobar42") + end + + it "does not change colorize_when" do + io = Colorize::Builder.new + io << "foo".colorize.red << :bar << 42 + mem = IO::Memory.new.to_colorizable(:auto) + mem.colorize_when.should eq(Colorize::When::Auto) + io.to_s_without_colorize mem + mem.io.to_s.should eq("foobar42") + mem.colorize_when.should eq(Colorize::When::Auto) + end + end end end diff --git a/spec/std/spec_spec.cr b/spec/std/spec_spec.cr index 23bb45c83662..f5b066aac012 100644 --- a/spec/std/spec_spec.cr +++ b/spec/std/spec_spec.cr @@ -99,19 +99,3 @@ describe "Spec matchers" do end end end - -describe "Spec" do - describe "use_colors?" do - it "returns if output is colored or not" do - 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 - ensure - Spec.use_colors = saved - end - end - end -end diff --git a/src/colorize.cr b/src/colorize.cr index 5cbc8ad9c396..1219cb3a3431 100644 --- a/src/colorize.cr +++ b/src/colorize.cr @@ -1,316 +1,155 @@ -# 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 `StyleBuilder`, +# 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 +# ``` +# +# To specify color, you can use `String` (via `Colorize.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 -# ``` +# When `::IO` class have a potential to support ANSI escape sequence, this +# `::IO` class includes `ColorizableIO` module. For example, +# `IO::FileDescriptor` includes `ColorizableIO`. +# +# `ColorizableIO` has `colorize_when` property, which value decides to output escape sequence. If this property's value is: +# +# - `:always` (`When::Always`), it outputs escape sequence always. +# - `:never` (`When::Never`), it doesn't output escape sequence. +# - `:auto` (`When::Auto`), it outputs escape sequence when it is TTY. +# +# `IO::FileDescript#colorize_when`'s default value is `:auto`, so we aren't +# careful the program connects pipe or not. # -# 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 +# # If program connects to pipe (like `crystal run foo.cr | cat`), it +# # doesn't output escape sequence. But if program connects to terminal, it +# # outputs escape sequence. +# puts "foo".colorize.red +# +# # In addition, if IO class doesn't include `ColorizableIO`, it outputs +# # escape sequence as default. +# mem = IO::Memory.new +# mem << "foo".colorize.red +# mem.to_s # => "\e[31mfoo\e[0m" +# +# # Create ColorizableIO from IO object by `to_colorizable` method. +# colorizable_io = IO::Memory.new.to_colorizable +# # Default colorize policy is `Always`. +# colorizable_io.colorize_when # => Colorize::When::Always # ``` # -# Available text decorations are: +# Finally, complete example is: +# # ``` -# :bold -# :bright -# :dim -# :underline -# :blink -# :reverse -# :hidden +# require "colorize" +# +# # This program outpus escape sequence always +# STDOUT.colorize_when = :always +# +# # Colorize this block as bold. +# with_color.bold.surround do +# print "Hello " +# +# # But, colorize only "Crystal" as yellow. +# print "Crystal".colorize.yellow +# +# puts " World!" +# end # ``` 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) - end - - def colorize(fore) - Colorize::Object.new(self).fore(fore) - end +# Create a new `Colorize::Style` from given values. +def with_color(fore = nil, back = nil, mode = nil) + Colorize::Style.new fore, back, mode 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 +module IO + include Colorize::IOExtension end diff --git a/src/colorize/builder.cr b/src/colorize/builder.cr new file mode 100644 index 000000000000..873c4b6735c5 --- /dev/null +++ b/src/colorize/builder.cr @@ -0,0 +1,83 @@ +require "./object" +require "./style" + +module Colorize + # Create a new `Builder`, then invoke the block and return this builder. + def self.build + Builder.new.tap do |io| + yield io + end + end + + # `Builder` is a `IO` to colorize. + # + # Its `#colorize_when` is always `When::Always` because it delegates given `::IO` on `#to_s` to colorize the output. + # + # It is useful when we cannot decide the output is colorized on creation, for example `Exception` message. + class Builder + include ::IO + include ColorizableIO + + @colorize_when = When::Always + + # `#colorize_when` is always `When::Always`, so it has no effect. + def colorize_when=(policy) + end + + # Create a new `Builder`. + def initialize + @contents = Array(Object(String) | Object(Builder) | IO::Memory).new + end + + # :nodoc: + def <<(object : Object(String)) + @contents << object + self + end + + # :nodoc: + def <<(object : Object) + self << Object.new(object.object.to_s).style(object) + end + + # :nodoc: + def surround(style) + @contents << Object.new(Builder.new.tap { |io| yield io }).style(style) + end + + # :nodoc: + def write(slice : Bytes) + unless (io = @contents.last?).is_a?(IO::Memory) + io = IO::Memory.new + @contents << io + end + io.write slice + end + + # :nodoc: + def read(slice : Bytes) + raise "Not implemented" + end + + # Output contents to *io*. + # + # Whether to colorize the output depends on *io*'s colorize policy. + def to_s(io) + @contents.each do |content| + io << content + end + end + + # Output contents to *io* without color. + def to_s_without_colorize(io) + IO.new(io).colorize_when(When::Never) do |io| + to_s io + end + end + + # Return contents without color. + def to_s_without_colorize + String.build { |io| to_s_without_colorize io } + end + end +end diff --git a/src/colorize/color.cr b/src/colorize/color.cr new file mode 100644 index 000000000000..91025b35912c --- /dev/null +++ b/src/colorize/color.cr @@ -0,0 +1,221 @@ +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 + + def_equals_and_hash value + 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 + + def_equals_and_hash red, green, blue + end +end diff --git a/src/colorize/io.cr b/src/colorize/io.cr new file mode 100644 index 000000000000..93835f9742a1 --- /dev/null +++ b/src/colorize/io.cr @@ -0,0 +1,181 @@ +require "./style_builder" +require "./when" + +module Colorize + # `ColorizableIO` is a mixin module for the `IO` which accepts escape sequence. + module ColorizableIO + # Whether to output escape sequence. See `When`. + # + # NOTE: `IO::FileDescriptor`'s default value is `When::Auto`. It works fine. + getter colorize_when : When = When::Always + + # Set *colorize_when* to `#colorize_when`. + # + # It parses given value by `When.parse`. Available policies are: + # + # ``` + # :auto + # :always + # :never + # ``` + # + # See `When` for each values details. + def colorize_when=(colorize_when : String | Symbol) + @colorize_when = When.parse?(colorize_when.to_s) || raise ArgumentError.new("unknown policy: #{colorize_when}") + end + + # Set *colorize_when* to `#colorize_when`. + def colorize_when=(@colorize_when) + end + + # Set *colorize_when* to `#colorize_when`, then invoke the block. After it, reset `#colorize_when` as old value. + def colorize_when(colorize_when) + old_when = @colorize_when + begin + self.colorize_when = colorize_when + yield self + ensure + @colorize_when = old_when + end + end + + # It keeps last colorizing style for nesting. + @last_style : StyleBuilder? = nil + + # Return `true` when this `IO` can output escape sequence on its `#colorize_when` policy. + def output_escape_sequence? + @colorize_when.output_escape_sequence?(self) + end + + # Colorize the output in the block with *colorize* 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(style) : self + last_style = @last_style + + if !output_escape_sequence? || !style.enabled? || last_style.try &.same_style? style + yield self + else + must_reset = colorize_write style, reset: !(last_style.nil? || last_style.all_default?) + @last_style = style + + begin + yield self + ensure + @last_style = last_style + if must_reset + if last_style + colorize_write last_style, reset: !style.all_default? + else + colorize_reset + end + end + end + end + + self + end + + # Output escape sequence to reset. + + # :nodoc: + def colorize_reset + self << "\e[0m" + end + + # Write escape sequence to colorize with *style*. + # If *reset* is `true`, it outputs reset escape sequence before applying *style*. + # + # It returns `true` if it outputs some escape sequence, otherwise returns `false`. + + # :nodoc: + def colorize_write(style, reset = false) + return false if style.all_default? && !reset + + self << "\e[" + + printed = false + + if reset + self << "0" + printed = true + end + + unless style.fore.default? + self << ";" if printed + self << style.fore.fore_code + printed = true + end + + unless style.back.default? + self << ";" if printed + self << style.back.back_code + printed = true + end + + unless style.mode.none? + style.mode.codes do |code| + self << ";" if printed + self << code + printed = true + end + end + + self << "m" + + true + end + end + + # `IO` wraps given `::IO` to colorize. + # + # It is usual created by `IOExtension#to_colorizable`. + class IO + include ::IO + include ColorizableIO + + # Return wrapped `::IO` object + getter io + + # Return *io* itself if *io* is `ColorizableIO` already. + def self.new(io : ColorizableIO, colorize_when = When::Always) + io + end + + # Wrap a given *io* with *colorize_when* policy. + def initialize(@io : ::IO, colorize_when = When::Always) + self.colorize_when = colorize_when + end + + # Delegate to `#io`. + def write(slice : Bytes) + @io.write slice + end + + # Delegate to `#io`. + def read(slice : Bytes) + @io.read slice + end + end + + # `IOExtension` is a mixin module for `::IO`. + # + # It adds `#to_colorizable` method to create a new `ColorizableIO` instance. + module IOExtension + # Return `ColorizableIO` to colorize the output to `self`. + # + # If `self` is not `ColorizableIO`, it returns a new `IO` instance with *when_for_new* policy. + # If `self` is `ColorizableIO` already, it returns itself. + def to_colorizable(when_for_new = When::Always) + Colorize::IO.new self, when_for_new + end + end +end + +class IO::FileDescriptor + include Colorize::ColorizableIO + + @colorize_when = Colorize::When::Auto +end diff --git a/src/colorize/mode.cr b/src/colorize/mode.cr new file mode 100644 index 000000000000..9ce1cf0e1090 --- /dev/null +++ b/src/colorize/mode.cr @@ -0,0 +1,17 @@ +# `Mode` represents a text decorations on a terminal. +@[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..4540e4d2ca47 --- /dev/null +++ b/src/colorize/object.cr @@ -0,0 +1,44 @@ +require "./io" + +module Colorize + # `Object` wraps given object to colorize with a style on terminal. + # + # It is usual created by `ObjectExtension#colorize`. + struct Object(T) + include StyleBuilder + + # Wrap a *object* to colorize with given values. + def initialize(@object : T, fore = nil, back = nil, mode = nil) + style fore, back, mode + end + + # Return wrapped object. + getter object + + # Overload for `ColorizableIO`. See `IO#to_s`. + def to_s(io : ColorizableIO) + io.surround(self) do |io| + io << object + end + end + + # Output `object` with this style. + # + # NOTE: When you use this method, you can't get auto TTY detection feature. + def to_s(io : ::IO) + self.to_s IO.new(io) + end + + def_equals_and_hash fore, back, mode, enabled?, object + end + + # `ObjectExtension` is a mixin module for `::Object`. + # + # It adds `#colorize` method to create a new `Object` instance. + module ObjectExtensions + # Return a new `Object` instance to colorize with given values. + def colorize(fore = nil, back = nil, mode = nil) + Object.new(self, fore, back, mode) + end + end +end diff --git a/src/colorize/style.cr b/src/colorize/style.cr new file mode 100644 index 000000000000..f6f5111c2963 --- /dev/null +++ b/src/colorize/style.cr @@ -0,0 +1,50 @@ +require "./io" + +# `Style` represents a colorize style on the terminal. +# +# ``` +# # Create a new style by the constructor. +# style = Colorize::Style.new(fore: :red, back: :blue, mode: :underline) +# +# # Or, we can use `StyleBuilder`'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 StyleBuilder + + # Creates a new instance with *fore*, *back* and *mode*. + def initialize(fore = nil, back = nil, mode = nil) + style fore, back, mode + end + + # Output escape sequence of this style. + def to_s(io) + IO.new(io).colorize_write style, reset: false + end + + # Colorize the content in the block with this style. + # + # It is short hand for `IO#surround`. + def surround(io = STDOUT) + IO.new(io).surround(self) { |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, enabled? +end diff --git a/src/colorize/style_builder.cr b/src/colorize/style_builder.cr new file mode 100644 index 000000000000..e424b7fe822a --- /dev/null +++ b/src/colorize/style_builder.cr @@ -0,0 +1,217 @@ +require "./color" +require "./mode" + +# `StyleBuilder` is a mixin module for `Style` and `Object`. +# +# It provides builder methods for construct a style on a terminal. +module Colorize::StyleBuilder + # 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 + + # Colorize only if this value is `true`. It is changed by `#toggle` method. + property? enabled = true + + {% 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 %} + + # 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 style, then return `self`. + def style(style : StyleBuilder) + style style.fore, style.back, style.mode + end + + # :ditto: + def style(fore = nil, back = nil, mode = nil) + fore fore + back back + mode mode + end + + # Set *flag* to `#enabled`, then return `self`. + def toggle(flag) + @enabled = !!flag + self + end + + # Return `true` if `#fore`, `#back` and `#mode` are still default. + def all_default? + fore.default? && back.default? && mode.none? + end + + # Return `true` if `#fore`, `#back` and `#mode` are same as `other''s. + def same_style?(other) + fore == other.fore && back == other.back && mode == other.mode + end +end diff --git a/src/colorize/when.cr b/src/colorize/when.cr new file mode 100644 index 000000000000..b3320dc8123d --- /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` when given *io* can output escape sequence on this policy. + # See `Auto`, `Always` and `Never`. + def output_escape_sequence?(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..cf6d486b3e4d 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -52,7 +52,6 @@ class Crystal::Command private getter options def initialize(@options : Array(String)) - @color = true @stats = @time = false end @@ -114,7 +113,6 @@ class Crystal::Command rescue ex : Crystal::ToolException error ex.message rescue ex : Crystal::Exception - ex.color = @color if @config.try(&.output_format) == "json" puts ex.to_json else @@ -344,9 +342,15 @@ class Crystal::Command end end + opts.on("--color auto|always|never", "Colorize the output") do |policy| + color = Colorize::When.parse policy + STDOUT.colorize_when = color + STDERR.colorize_when = color + end + opts.on("--no-color", "Disable colored output") do - @color = false - compiler.color = false + STDOUT.colorize_when = Colorize::When::Never + STDERR.colorize_when = Colorize::When::Never end unless no_codegen @@ -477,9 +481,14 @@ class Crystal::Command puts opts exit end + opts.on("--color auto|always|never", "Colorize the output") do |policy| + color = Colorize::When.parse policy + STDOUT.colorize_when = color + STDERR.colorize_when = color + end opts.on("--no-color", "Disable colored output") do - @color = false - compiler.color = false + STDOUT.colorize_when = Colorize::When::Never + STDERR.colorize_when = Colorize::When::Never end opts.invalid_option { } end @@ -495,7 +504,11 @@ 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") - Crystal.error msg, @color, exit_code: exit_code + if ARGV.includes?("--no-color") + STDOUT.colorize_when = Colorize::When::Never + STDERR.colorize_when = Colorize::When::Never + end + + Crystal.error msg, exit_code: exit_code end end diff --git a/src/compiler/crystal/command/format.cr b/src/compiler/crystal/command/format.cr index 7c942e16bd4a..6317262d61ea 100644 --- a/src/compiler/crystal/command/format.cr +++ b/src/compiler/crystal/command/format.cr @@ -34,8 +34,15 @@ class Crystal::Command exit end + opts.on("--color auto|always|never", "Colorize the output") do |policy| + color = Colorize::When.parse policy + STDOUT.colorize_when = color + STDERR.colorize_when = color + end + opts.on("--no-color", "Disable colored output") do - @color = false + STDOUT.colorize_when = Colorize::When::Never + STDERR.colorize_when = Colorize::When::Never end end @@ -86,8 +93,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.red.bold + print "source is not a valid Crystal source file: ".colorize.bold puts ex.message exit 1 rescue ex : Crystal::SyntaxException @@ -114,8 +121,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.red.bold + print "file '#{Crystal.relative_filename(filename)}' is not a valid Crystal source file: ".colorize.bold puts ex.message exit 1 rescue ex : Crystal::SyntaxException @@ -162,21 +169,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) << " " << 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.red.bold + print "file '#{Crystal.relative_filename(filename)}' is not a valid Crystal source file: ".colorize.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) << " " << ex.message << " at " << filename << ":" << ex.line_number << ":" << ex.column_number << "\n" end rescue ex if check_files @@ -190,7 +197,7 @@ class Crystal::Command end private def couldnt_format(file) - STDERR << "Error:".colorize(:red).toggle(@color) << ", " << + STDERR << "Error:".colorize(:red) << ", " << "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..e2373fbc4a46 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 STDOUT.colorize_when.auto? + options << "--color=#{STDOUT.colorize_when.to_s.underscore}" + 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 7413984a06e3..e216cad1b87e 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -57,9 +57,6 @@ 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 - # If `true`, skip cleanup process on semantic analysis. property? no_cleanup = false @@ -169,7 +166,6 @@ module Crystal program.flags << "debug" unless debug.none? program.flags.concat @flags program.wants_doc = wants_doc? - program.color = color? program.stdout = stdout program.show_error_trace = show_error_trace? program.wants_stats = @stats @@ -201,8 +197,8 @@ module Crystal parser.wants_doc = wants_doc? parser.parse rescue ex : InvalidByteSequenceError - stdout.print colorize("Error: ").red.bold - stdout.print colorize("file '#{Crystal.relative_filename(source.filename)}' is not a valid Crystal source file: ").bold + stdout.print "Error: ".colorize.red.bold + stdout.print "file '#{Crystal.relative_filename(source.filename)}' is not a valid Crystal source file: ".colorize.bold stdout.puts ex.message exit 1 end @@ -398,9 +394,8 @@ module Crystal TargetMachine.create(triple, @mcpu || "", @mattr || "", @release) end rescue ex : ArgumentError - stdout.print colorize("Error: ").red.bold - stdout.print "llc: " - stdout.puts ex.message + stdout << "Error: ".colorize.red.bold << "llc: " << ex.message + stdout.puts exit 1 end @@ -454,11 +449,7 @@ module Crystal end private def error(msg, exit_code = 1) - Crystal.error msg, @color, exit_code, stderr: stderr - end - - private def colorize(obj) - obj.colorize.toggle(@color) + Crystal.error msg, exit_code, stderr: stderr 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..5e5ab91feadd 100644 --- a/src/compiler/crystal/exception.cr +++ b/src/compiler/crystal/exception.cr @@ -3,16 +3,14 @@ require "colorize" module Crystal abstract class Exception < ::Exception - property? color = false - @filename : String | VirtualFile | Nil + abstract def to_s_with_source(source, io) + def to_s(io) - to_s_with_source(nil, io) + to_s_with_source(nil, io.to_colorizable(Colorize::When::Never)) end - abstract def to_s_with_source(source, io) - def to_json(json : JSON::Builder) json.array do to_json_single(json) @@ -38,7 +36,7 @@ module Crystal def to_s_with_source(source) String.build do |io| - to_s_with_source source, io + to_s_with_source source, io.to_colorizable(Colorize::When::Never) end end @@ -46,14 +44,6 @@ module Crystal Crystal.relative_filename(filename) end - def colorize(obj) - obj.colorize.toggle(@color) - end - - def with_color - ::with_color.toggle(@color) - end - def replace_leading_tabs_with_spaces(line) found_non_space = false line.gsub do |char| diff --git a/src/compiler/crystal/macros/macros.cr b/src/compiler/crystal/macros/macros.cr index 345accaf6864..473f505f6ff1 100644 --- a/src/compiler/crystal/macros/macros.cr +++ b/src/compiler/crystal/macros/macros.cr @@ -75,15 +75,20 @@ class Crystal::Program generated_node = yield parser normalize(generated_node, inside_exp: inside_exp) rescue ex : Crystal::SyntaxException - expanded_source = String.build do |str| - str << ("=" * 80) << '\n' - str << ("-" * 80) << '\n' - str << Crystal.with_line_numbers(generated_source) << '\n' - str << ("-" * 80) << '\n' - str << ex.to_s_with_source(generated_source) << '\n' - str << ("=" * 80) - end - node.raise "macro didn't expand to a valid program, it expanded to:\n\n#{expanded_source}" + node.raise(Colorize.build do |str| + str.puts "macro didn't expand to a valid program, it expanded to:" + str.puts + str.puts "=" * 80 + str.puts "-" * 80 + with_color.surround(str) do |str| + Crystal.with_line_numbers(generated_source, ex.line_number, str) + end + str.puts "-" * 80 + with_color.surround(str) do |str| + ex.to_s_with_source(generated_source, str) + end + str.puts "=" * 80 + end) end end diff --git a/src/compiler/crystal/program.cr b/src/compiler/crystal/program.cr index f25db2421807..50925f5dc1a5 100644 --- a/src/compiler/crystal/program.cr +++ b/src/compiler/crystal/program.cr @@ -67,9 +67,6 @@ 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 - # All required files. The set stores absolute files. This way # files loaded by `require` nodes are only processed once. getter requires = Set(String).new @@ -497,11 +494,17 @@ module Crystal "__temp_#{@temp_var_counter}" end - # Colorizes the given object, depending on whether this program - # is configured to use colors. - def colorize(obj) - obj.colorize.toggle(@color) - end + # # Colorizes the given object, depending on whether this program + # # is configured to use colors. + # def colorize(obj) + # if (stdout = stdout()).is_a?(Colorize::IO) && stdout.output_escape_sequence? + # String.build do |io| + # io.colorizable << yield(obj.colorize) + # end + # else + # obj + # end + # end private def abstract_value_type(type) type.abstract = true diff --git a/src/compiler/crystal/semantic/call_error.cr b/src/compiler/crystal/semantic/call_error.cr index 6c56e28bcfbc..ad3e6185ca6b 100644 --- a/src/compiler/crystal/semantic/call_error.cr +++ b/src/compiler/crystal/semantic/call_error.cr @@ -29,7 +29,7 @@ class Crystal::Path similar_name = type.lookup_similar_path(self) if similar_name - self.raise("undefined constant #{self} #{type.program.colorize("(did you mean '#{similar_name}')").yellow.bold}") + self.raise Colorize::Builder{"undefined constant #{self}", " (did you mean '#{similar_name}')".colorize.yellow.bold} else self.raise("undefined constant #{self}") end @@ -82,7 +82,7 @@ class Crystal::Call owner_trace = obj.try &.find_owner_trace(owner.program, owner) similar_name = owner.lookup_similar_def_name(def_name, self.args.size, block) - error_msg = String.build do |msg| + error_msg = Colorize.build do |msg| if obj && owner != program msg << "undefined method '#{def_name}' for #{owner}" elsif convert_to_logical_operator(def_name) @@ -101,15 +101,15 @@ class Crystal::Call end if obj && obj.type != owner - msg << colorize(" (compile-time type is #{obj.type})").yellow.bold + msg << " (compile-time type is #{obj.type})".colorize.yellow.bold end if similar_name if similar_name == def_name # This check is for the case `a if a = 1` - msg << colorize(" (If you declared '#{def_name}' in a suffix if, declare it in a regular if for this to work. If the variable was declared in a macro it's not visible outside it)").yellow.bold + msg << " (If you declared '#{def_name}' in a suffix if, declare it in a regular if for this to work. If the variable was declared in a macro it's not visible outside it)".colorize.yellow.bold else - msg << colorize(" (did you mean '#{similar_name}'?)").yellow.bold + msg << " (did you mean '#{similar_name}'?)".colorize.yellow.bold end end @@ -121,9 +121,9 @@ class Crystal::Call if deps && deps.size == 1 && deps.first.same?(program.nil_var) similar_name = scope.lookup_similar_instance_var_name(ivar.name) if similar_name - msg << colorize(" (#{ivar.name} was never assigned a value, did you mean #{similar_name}?)").yellow.bold + msg << " (#{ivar.name} was never assigned a value, did you mean #{similar_name}?)".colorize.yellow.bold else - msg << colorize(" (#{ivar.name} was never assigned a value)").yellow.bold + msg << " (#{ivar.name} was never assigned a value)".colorize.yellow.bold end end end @@ -135,7 +135,7 @@ class Crystal::Call # If it's on an initialize method and there's a similar method name, it's probably a typo if (def_name == "initialize" || def_name == "new") && (similar_def = owner.instance_type.lookup_similar_def("initialize", self.args.size, block)) - inner_msg = colorize("do you maybe have a typo in this '#{similar_def.name}' method?").yellow.bold.to_s + inner_msg = Colorize.build &.<< "do you maybe have a typo in this '#{similar_def.name}' method?".colorize.yellow.bold inner_exception = TypeException.for_node(similar_def, inner_msg) end @@ -387,10 +387,10 @@ class Crystal::Call str << "\n - " append_def_full_name a_def.owner, a_def, arg_types, str if defs.size > 1 && a_def.same?(matched_def) - str << colorize(" (trying this one)").blue + str << " (trying this one)".colorize.blue end if a_def.args.any? { |arg| arg.default_value && arg.external_name == argument_name } - str << colorize(" (did you mean this one?)").yellow.bold + str << " (did you mean this one?)".colorize.yellow.bold end end end @@ -545,12 +545,12 @@ class Crystal::Call elsif !a_def.double_splat similar_name = Levenshtein.find(named_arg.name, a_def.args.select(&.default_value).map(&.external_name)) - msg = String.build do |str| + msg = Colorize.build do |str| str << "no argument named '" str << named_arg.name str << "'" if similar_name - str << colorize(" (did you mean '#{similar_name}'?)").yellow.bold + str << " (did you mean '#{similar_name}'?)".colorize.yellow.bold end defs = owner.lookup_defs(a_def.name) @@ -652,8 +652,4 @@ class Crystal::Call "#{owner}##{method_name}" end end - - private def colorize(obj) - program.colorize(obj) - end end diff --git a/src/compiler/crystal/semantic/exception.cr b/src/compiler/crystal/semantic/exception.cr index e0fd78d836c2..d5626b872bee 100644 --- a/src/compiler/crystal/semantic/exception.cr +++ b/src/compiler/crystal/semantic/exception.cr @@ -9,10 +9,7 @@ module Crystal @column : Int32 @size : Int32 - def color=(color) - @color = !!color - inner.try &.color=(color) - end + @colorized_message : Colorize::Builder? def self.for_node(node, message, inner = nil) location = node.location @@ -30,6 +27,10 @@ module Crystal end end + def initialize(@colorized_message : Colorize::Builder, @line, @column : Int32, @filename, @size, @inner = nil) + initialize colorized_message.to_s_without_colorize, @line, @column, @filename, @size, @inner + end + def initialize(message, @line, @column : Int32, @filename, @size, @inner = nil) # If the inner exception is a macro raise, we replace this exception's # message with that message. In this way the error message will @@ -84,9 +85,9 @@ module Crystal # If the inner exception has no location it means that they came from virtual nodes. # In that case, get the deepest error message and only show that. if inner && !inner.has_location? - msg = deepest_error_message.to_s + msg = deepest_error_message else - msg = @message.to_s + msg = @colorized_message || @message end is_macro = false @@ -104,7 +105,7 @@ module Crystal end when VirtualFile io << "in macro '#{filename.macro.name}' #{filename.macro.location.try &.filename}:#{filename.macro.location.try &.line_number}, line #{@line}:\n\n" - io << Crystal.with_line_numbers(filename.source, @line, @color) + Crystal.with_line_numbers(filename.source, @line, io) is_macro = true else lines = source ? source.lines.to_a : nil @@ -117,7 +118,7 @@ module Crystal io << replace_leading_tabs_with_spaces(line.chomp) io << "\n" io << (" " * (@column - 1)) - with_color.green.bold.surround(io) do + with_color.green.bold.surround(io) do |io| io << "^" if @size > 0 io << ("~" * (@size - 1)) @@ -128,7 +129,7 @@ module Crystal if is_macro io << "\n" - append_error_message io, @message + append_error_message io, @colorized_message || @message end if inner && inner.has_location? @@ -138,10 +139,10 @@ module Crystal end def append_error_message(io, msg) - if @inner + if (inner = @inner) && inner.has_location? io << msg else - io << colorize(msg).bold + io << msg.colorize.bold end end @@ -157,7 +158,7 @@ module Crystal if inner = @inner inner.deepest_error_message else - @message + @colorized_message || @message end end end @@ -224,14 +225,16 @@ module Crystal end def print_nil_reason(nil_reason, io) - io << colorize("Error: ").bold - case nil_reason.reason - when :used_before_initialized - io << colorize("instance variable '#{nil_reason.name}' was used before it was initialized in one of the 'initialize' methods, rendering it nilable").bold - when :used_self_before_initialized - io << colorize("'self' was used before initializing instance variable '#{nil_reason.name}', rendering it nilable").bold - when :initialized_in_rescue - io << colorize("instance variable '#{nil_reason.name}' is initialized inside a begin-rescue, so it can potentially be left uninitialized if an exception is raised and rescued").bold + with_color.bold.surround(io) do |io| + io << "Error: " + case nil_reason.reason + when :used_before_initialized + io << "instance variable '#{nil_reason.name}' was used before it was initialized in one of the 'initialize' methods, rendering it nilable" + when :used_self_before_initialized + io << "'self' was used before initializing instance variable '#{nil_reason.name}', rendering it nilable" + when :initialized_in_rescue + io << "instance variable '#{nil_reason.name}' is initialized inside a begin-rescue, so it can potentially be left uninitialized if an exception is raised and rescued" + end end end @@ -272,7 +275,7 @@ module Crystal io << " " io << (" " * (name_column - 1)) - with_color.green.bold.surround(io) do + with_color.green.bold.surround(io) do |io| io << "^" if name_size > 0 io << ("~" * (name_size - 1)) if name_size @@ -296,14 +299,14 @@ module Crystal class Program def undefined_global_variable(node, similar_name) - common = String.build do |str| + common = Colorize.build do |str| str << "Can't infer the type of global variable '#{node.name}'" if similar_name - str << colorize(" (did you mean #{similar_name}?)").yellow.bold.to_s + str << " (did you mean #{similar_name}?)".colorize.yellow.bold end end - msg = String.build do |str| + msg = Colorize.build do |str| str << common str << "\n\n" str << undefined_variable_message("global", node.name) @@ -314,14 +317,14 @@ module Crystal end def undefined_class_variable(node, owner, similar_name) - common = String.build do |str| + common = Colorize.build do |str| str << "Can't infer the type of class variable '#{node.name}' of #{owner.devirtualize}" if similar_name - str << colorize(" (did you mean #{similar_name}?)").yellow.bold.to_s + str << " (did you mean #{similar_name}?)".colorize.yellow.bold end end - msg = String.build do |str| + msg = Colorize.build do |str| str << common str << "\n\n" str << undefined_variable_message("class", node.name) @@ -332,14 +335,14 @@ module Crystal end def undefined_instance_variable(node, owner, similar_name) - common = String.build do |str| + common = Colorize.build do |str| str << "Can't infer the type of instance variable '#{node.name}' of #{owner.devirtualize}" if similar_name - str << colorize(" (did you mean #{similar_name}?)").yellow.bold.to_s + str << " (did you mean #{similar_name}?)".colorize.yellow.bold end end - msg = String.build do |str| + msg = Colorize.build do |str| str << common str << "\n\n" str << undefined_variable_message("instance", node.name) diff --git a/src/compiler/crystal/syntax/exception.cr b/src/compiler/crystal/syntax/exception.cr index e6991defe64a..bab641be49dd 100644 --- a/src/compiler/crystal/syntax/exception.cr +++ b/src/compiler/crystal/syntax/exception.cr @@ -27,9 +27,9 @@ module Crystal def append_to_s(source, io) if @filename - io << "Syntax error in #{relative_filename(@filename)}:#{@line_number}: #{colorize(@message).bold}" + io << "Syntax error in #{relative_filename(@filename)}:#{@line_number}: " << @message.colorize.bold else - io << "Syntax error in line #{@line_number}: #{colorize(@message).bold}" + io << "Syntax error in line #{@line_number}: " << @message.colorize.bold end source = fetch_source(source) @@ -45,7 +45,7 @@ module Crystal (@column_number - 1).times do io << " " end - with_color.green.bold.surround(io) do + with_color.green.bold.surround(io) do |io| io << "^" if size = @size io << ("~" * (size - 1)) diff --git a/src/compiler/crystal/tools/init.cr b/src/compiler/crystal/tools/init.cr index e4e942599d5f..a6c5b0f24c16 100644 --- a/src/compiler/crystal/tools/init.cr +++ b/src/compiler/crystal/tools/init.cr @@ -131,11 +131,11 @@ module Crystal def render Dir.mkdir_p(File.dirname(full_path)) File.write(full_path, to_s) - puts log_message unless config.silent + log_message unless config.silent end def log_message - " #{"create".colorize(:light_green)} #{full_path}" + STDOUT << " " << "create".colorize(:light_green) << " #{full_path}" end def module_name diff --git a/src/compiler/crystal/tools/playground/server.cr b/src/compiler/crystal/tools/playground/server.cr index 0518c2cdbafe..7b1a2d1cf9a1 100644 --- a/src/compiler/crystal/tools/playground/server.cr +++ b/src/compiler/crystal/tools/playground/server.cr @@ -29,6 +29,10 @@ module Crystal::Playground prelude = %( require "compiler/crystal/tools/playground/agent" + require "colorize/io" + + STDOUT.colorize_when = Colorize::When::Always + STDERR.colorize_when = Colorize::When::Always class Crystal::Playground::Agent @@instance = Crystal::Playground::Agent.new("ws://localhost:#{@port}/agent/#{@session_key}/#{tag}", #{tag}) @@ -49,7 +53,6 @@ module Crystal::Playground ] output_filename = tempfile "play-#{@session_key}-#{tag}" compiler = Compiler.new - compiler.color = false 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 3ea03e50b1fe..0e74d7584eba 100644 --- a/src/compiler/crystal/tools/print_hierarchy.cr +++ b/src/compiler/crystal/tools/print_hierarchy.cr @@ -27,7 +27,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 @@ -126,7 +126,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)" @@ -176,7 +176,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 @@ -211,13 +211,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,10 +261,6 @@ module Crystal yield @indents.pop end - - def with_color - ::with_color.toggle(@program.color?) - end end class JSONHierarchyPrinter < HierarchyPrinter diff --git a/src/compiler/crystal/util.cr b/src/compiler/crystal/util.cr index db90541c37cc..c859234f1391 100644 --- a/src/compiler/crystal/util.cr +++ b/src/compiler/crystal/util.cr @@ -17,9 +17,9 @@ module Crystal filename end - def self.error(msg, color, exit_code = 1, stderr = STDERR) - stderr.print "Error: ".colorize.toggle(color).red.bold - stderr.puts msg.colorize.toggle(color).bright + def self.error(msg, exit_code = 1, stderr = STDERR) + stderr << "Error: ".colorize.red.bold << msg.colorize.bright + stderr.puts exit(exit_code) if exit_code end @@ -47,18 +47,21 @@ module Crystal CacheDir.instance.join("crystal-run-#{basename}.tmp") end - def self.with_line_numbers(source : String, highlight_line_number = nil, color = false) - source.lines.map_with_index do |line, i| - str = "#{"%4d" % (i + 1)}. #{line.to_s.chomp}" - target = i + 1 == highlight_line_number - if target - if color - str = ">".colorize.green.bold.to_s + str[1..-1].colorize.bold.to_s - else - str = ">" + str[1..-1] - end + def self.with_line_numbers(source : String, highlight_line_number, io) + source.lines.each_with_index do |line, i| + str = "#{"%4d" % (i + 1)}: #{line}" + if i + 1 == highlight_line_number + io << ">".colorize.green.bold << str[1..-1].colorize.bold + else + io << str end - str + io.puts + end + end + + def self.with_line_numbers(source : String) + source.lines.map_with_index do |line, i| + "#{"%3d" % (i + 1)}: #{line}" end.join "\n" end end diff --git a/src/kernel.cr b/src/kernel.cr index 235e770affb5..8e15655cde8c 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -132,7 +132,8 @@ module AtExitHandlers begin handler.call status rescue handler_ex - STDERR.puts "Error running at_exit handler: #{handler_ex}" + STDERR << "Error running at_exit handler: " << handler_ex + STDERR.puts end end end diff --git a/src/spec.cr b/src/spec.cr index cbc605a6c812..5076363d0cf0 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| + STDOUT.colorize_when = Colorize::When.parse color + end opts.on("--no-color", "Disable colored output") do - Spec.use_colors = false + STDOUT.colorize_when = Colorize::When::Never end opts.unknown_args do |args| end diff --git a/src/spec/context.cr b/src/spec/context.cr index 39e57733de4c..130792f77614 100644 --- a/src/spec/context.cr +++ b/src/spec/context.cr @@ -111,10 +111,8 @@ module Spec 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}" + STDOUT << " " << res_elapsed.colorize.red << " seconds #{Spec.relative_file(res.file)}:#{res.line}" + puts end end diff --git a/src/spec/dsl.cr b/src/spec/dsl.cr index 0ca2168a9004..7d166a7c2afb 100644 --- a/src/spec/dsl.cr +++ b/src/spec/dsl.cr @@ -17,24 +17,9 @@ module Spec pending: '*', } - @@use_colors = true - # :nodoc: def self.color(str, status) - if use_colors? - str.colorize(COLORS[status]) - else - str - end - end - - # :nodoc: - def self.use_colors? - @@use_colors - end - - # :nodoc: - def self.use_colors=(@@use_colors) + str.colorize(fore: COLORS[status]) end # :nodoc: