diff --git a/samples/2048.cr b/samples/2048.cr index a76eccf9a59b..33052fe939a1 100644 --- a/samples/2048.cr +++ b/samples/2048.cr @@ -3,24 +3,26 @@ require "colorize" module Screen + alias Color = Colorize::ColorANSI + TILES = { - 0 => {:white, nil}, - 2 => {:black, :white}, - 4 => {:blue, :white}, - 8 => {:black, :yellow}, - 16 => {:white, :red}, - 32 => {:black, :red}, - 64 => {:white, :magenta}, - 128 => {:red, :yellow}, - 256 => {:magenta, :yellow}, - 512 => {:white, :yellow}, - 1024 => {:white, :yellow}, - 2048 => {:white, :yellow}, - 4096 => {:white, :black}, - 8192 => {:white, :black}, - 16384 => {:white, :black}, - 32768 => {:white, :black}, - 65536 => {:white, :black}, + 0 => {Color::White, nil}, + 2 => {Color::Black, Color::White}, + 4 => {Color::Blue, Color::White}, + 8 => {Color::Black, Color::Yellow}, + 16 => {Color::White, Color::Red}, + 32 => {Color::Black, Color::Red}, + 64 => {Color::White, Color::Magenta}, + 128 => {Color::Red, Color::Yellow}, + 256 => {Color::Magenta, Color::Yellow}, + 512 => {Color::White, Color::Yellow}, + 1024 => {Color::White, Color::Yellow}, + 2048 => {Color::White, Color::Yellow}, + 4096 => {Color::White, Color::Black}, + 8192 => {Color::White, Color::Black}, + 16384 => {Color::White, Color::Black}, + 32768 => {Color::White, Color::Black}, + 65536 => {Color::White, Color::Black}, } def self.colorize_for(tile) diff --git a/spec/std/colorize_spec.cr b/spec/std/colorize_spec.cr index bfe39f9613da..568fc5e4cfc8 100644 --- a/spec/std/colorize_spec.cr +++ b/spec/std/colorize_spec.cr @@ -1,8 +1,8 @@ require "spec" require "colorize" -private def colorize(obj, *args) - obj.colorize(*args).toggle(true) +private def colorize(obj) + obj.colorize.toggle(true) end private def with_color_wrap(*args) @@ -42,11 +42,11 @@ describe "colorize" do end it "colorizes foreground with 8-bit color" do - colorize("hello").fore(Colorize::Color256.new(123u8)).to_s.should eq("\e[38;5;123mhello\e[0m") + colorize("hello").fore(Colorize::Color256.new(123)).to_s.should eq("\e[38;5;123mhello\e[0m") end it "colorizes foreground with true color" do - colorize("hello").fore(Colorize::ColorRGB.new(12u8, 34u8, 56u8)).to_s.should eq("\e[38;2;12;34;56mhello\e[0m") + colorize("hello").fore(Colorize::ColorRGB.new(12, 34, 56)).to_s.should eq("\e[38;2;12;34;56mhello\e[0m") end it "colorizes background" do @@ -69,16 +69,15 @@ describe "colorize" do end it "colorizes background with 8-bit color" do - colorize("hello").back(Colorize::Color256.new(123u8)).to_s.should eq("\e[48;5;123mhello\e[0m") + colorize("hello").back(Colorize::Color256.new(123)).to_s.should eq("\e[48;5;123mhello\e[0m") end it "colorizes background with true color" do - colorize("hello").back(Colorize::ColorRGB.new(12u8, 34u8, 56u8)).to_s.should eq("\e[48;2;12;34;56mhello\e[0m") + colorize("hello").back(Colorize::ColorRGB.new(12, 34, 56)).to_s.should eq("\e[48;2;12;34;56mhello\e[0m") end it "colorizes mode" do colorize("hello").bold.to_s.should eq("\e[1mhello\e[0m") - colorize("hello").bright.to_s.should eq("\e[1mhello\e[0m") colorize("hello").dim.to_s.should eq("\e[2mhello\e[0m") colorize("hello").underline.to_s.should eq("\e[4mhello\e[0m") colorize("hello").blink.to_s.should eq("\e[5mhello\e[0m") @@ -99,7 +98,7 @@ describe "colorize" do end it "colorizes foreground with symbol" do - colorize("hello", :red).to_s.should eq("\e[31mhello\e[0m") + colorize("hello").fore(:red).to_s.should eq("\e[31mhello\e[0m") colorize("hello").fore(:red).to_s.should eq("\e[31mhello\e[0m") end @@ -107,26 +106,8 @@ describe "colorize" do colorize("hello").mode(:bold).to_s.should eq("\e[1mhello\e[0m") end - it "raises on unknown foreground color" do - expect_raises ArgumentError, "Unknown color: brown" do - colorize("hello", :brown) - end - end - - it "raises on unknown background color" do - expect_raises ArgumentError, "Unknown color: brown" do - colorize("hello").back(:brown) - end - end - - it "raises on unknown mode" do - expect_raises ArgumentError, "Unknown mode: bad" do - colorize("hello").mode(:bad) - end - end - it "inspects" do - colorize("hello", :red).inspect.should eq("\e[31m\"hello\"\e[0m") + colorize("hello").fore(:red).inspect.should eq("\"\\e[31mhello\\e[0m\"") end it "colorizes with surround" do diff --git a/src/colorize.cr b/src/colorize.cr index 792faa55e229..659a460ca685 100644 --- a/src/colorize.cr +++ b/src/colorize.cr @@ -1,9 +1,10 @@ -# With Colorize you can change the fore- and background colors and text decorations when rendering text +# 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. # # Its first argument changes the foreground color: +# # ``` # require "colorize" # @@ -13,6 +14,7 @@ # ``` # # There are alternative ways to change the foreground color: +# # ``` # require "colorize" # @@ -21,6 +23,7 @@ # ``` # # To change the background color, the following methods are available: +# # ``` # require "colorize" # @@ -30,13 +33,19 @@ # ``` # # You can also pass an RGB color to `colorize`: +# # ``` # require "colorize" # +# "foo".colorize(0, 255, 255) # => "foo" in aqua +# +# # This is the same as: +# # "foo".colorize(Colorize::ColorRGB.new(0, 255, 255)) # => "foo" in aqua # ``` # # Or an 8-bit color: +# # ``` # require "colorize" # @@ -44,6 +53,7 @@ # ``` # # It's also possible to change the text decoration: +# # ``` # require "colorize" # @@ -53,6 +63,7 @@ # # The `colorize` method returns a `Colorize::Object` instance, # which allows chaining methods together: +# # ``` # require "colorize" # @@ -61,6 +72,7 @@ # # 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: +# # ``` # require "colorize" # @@ -68,70 +80,51 @@ # "foo".colorize(:red).toggle(false).toggle(true) # => "foo" in red # ``` # -# The color `:default` will just leave the object as it is (but it's an `Colorize::Object(String)` then). -# That's handy in for example conditions: -# ``` -# require "colorize" +# The color `:default` leaves the object's representation as it is but the object is a `Colorize::Object` then +# which is handy in conditions such as: # -# "foo".colorize(some_bool ? :green : :default) # ``` +# require "colorize" # -# Available colors are: -# ``` -# :default -# :black -# :red -# :green -# :yellow -# :blue -# :magenta -# :cyan -# :light_gray -# :dark_gray -# :light_red -# :light_green -# :light_yellow -# :light_blue -# :light_magenta -# :light_cyan -# :white +# "foo".colorize(Random::DEFAULT.next_bool ? :green : :default) # ``` # -# Available text decorations are: -# ``` -# :bold -# :bright -# :dim -# :underline -# :blink -# :reverse -# :hidden -# ``` +# See `Colorize::ColorANSI` and `Colorize::Mode` for all available colors and text decorations. module Colorize - # If this value is `true`, `Colorize::Object` is enabled by default. - # But if this value is `false`, `Colorize::Object` is disabled. - # - # The default value is `true`. + # Objects will only be colored if this is `true`. # # ``` # require "colorize" # # Colorize.enabled = true - # "hello".colorize.red.to_s # => "\e[31mhello\e[0m" + # "hello".colorize.red.to_s # => "hello" in red # # Colorize.enabled = false # "hello".colorize.red.to_s # => "hello" # ``` - class_property? enabled : Bool = true + # + # NOTE: This is by default disabled on non-TTY devices because they likely don't support ANSI escape codes. + # This is also be disabled if the environment variable `TERM` is "dumb". + class_property? enabled : Bool = STDOUT.tty? && STDERR.tty? && ENV["TERM"]? != "dumb" # Makes `Colorize.enabled` `true` if and only if both of `STDOUT.tty?` # and `STDERR.tty?` are `true` and the tty is not considered a dumb terminal. # This is determined by the environment variable called `TERM`. # If `TERM=dumb`, color won't be enabled. + @[Deprecated("This is now done by default and there is no longer any need to use this manually.")] def self.on_tty_only! self.enabled = STDOUT.tty? && STDERR.tty? && ENV["TERM"]? != "dumb" end + # Resets the color and text decoration of the *io*. + # + # ``` + # with_color.green.surround(io) do + # io << "green" + # Colorize.reset + # io << " default" + # end + # ``` def self.reset(io = STDOUT) io << "\e[0m" if enabled? end @@ -151,13 +144,20 @@ module Colorize end module Colorize::ObjectExtensions + # Turns `self` into a `Colorize::Object`. def colorize Colorize::Object.new(self) end - def colorize(fore) + # Turns `self` into a `Colorize::Object` and colors it with a color. + def colorize(fore : Color) Colorize::Object.new(self).fore(fore) end + + # Turns `self` into a `Colorize::Object` and colors it with an RGB color. + def colorize(red : UInt8, green : UInt8, blue : UInt8) + colorize(Colorize::ColorRGB.new(red, green, blue)) + end end class Object @@ -167,6 +167,7 @@ end module Colorize alias Color = ColorANSI | Color256 | ColorRGB + # One color of a fixed set of colors. enum ColorANSI Default = 39 Black = 30 @@ -195,6 +196,7 @@ module Colorize end end + # An 8-bit color. record Color256, value : UInt8 do def fore(io : IO) : Nil @@ -208,6 +210,7 @@ module Colorize end end + # An RGB color. record ColorRGB, red : UInt8, green : UInt8, @@ -226,120 +229,147 @@ module Colorize io << blue end end + + # A text decoration. + # + # Note that not all text decorations are supported in all terminals. + # When a text decoration is not supported, it will leave the text unaffected. + @[Flags] + enum Mode + # Makes the text bold. + Bold = 1 + # Makes the text color bright. + @[Deprecated("Please use `bold` instead.")] + Bright = 1 + # Dims the text color. + Dim + # Underlines the text. + Underline + # Makes the text blink slowly. + Blink + # Swaps the foreground and background colors of the text. + Reverse + # Makes the text invisible. + Hidden + end end +# A colorized object. Colors and text decorations can be modified. struct Colorize::Object(T) - private MODE_DEFAULT = '0' + private MODE_NONE = '0' private MODE_BOLD = '1' - private MODE_BRIGHT = '1' + private MODE_BRIGHT = '1' # TODO: Remove this in the future private MODE_DIM = '2' private MODE_UNDERLINE = '4' private MODE_BLINK = '5' private MODE_REVERSE = '7' private MODE_HIDDEN = '8' - private MODE_BOLD_FLAG = 1 - private MODE_BRIGHT_FLAG = 1 - private MODE_DIM_FLAG = 2 - private MODE_UNDERLINE_FLAG = 4 - private MODE_BLINK_FLAG = 8 - private MODE_REVERSE_FLAG = 16 - private MODE_HIDDEN_FLAG = 32 - - private COLORS = %w(default black red green yellow blue magenta cyan light_gray dark_gray light_red light_green light_yellow light_blue light_magenta light_cyan white) - private MODES = %w(bold bright dim underline blink reverse hidden) - - @fore : Color - @back : Color - def initialize(@object : T) @fore = ColorANSI::Default @back = ColorANSI::Default - @mode = 0 + @mode = Mode::None @enabled = Colorize.enabled? end - {% for name in COLORS %} - def {{name.id}} - @fore = ColorANSI::{{name.camelcase.id}} + {% for color in ColorANSI.constants.reject { |constant| constant == "All" || constant == "None" } %} + def {{color.underscore.id}} + @fore = ColorANSI::{{color.id}} self end - def on_{{name.id}} - @back = ColorANSI::{{name.camelcase.id}} + def on_{{color.underscore.id}} + @back = ColorANSI::{{color.id}} self end {% end %} - {% for name in MODES %} - def {{name.id}} - @mode |= MODE_{{name.upcase.id}}_FLAG + def on(color : Color) + back color + end + + {% for mode in Mode.constants.reject { |constant| constant == "All" || constant == "None" } %} + def {{mode.underscore.id}} + @mode |= Mode::{{mode.id}} self end {% end %} - def fore(color : Symbol) - {% for name in COLORS %} - if color == :{{name.id}} - @fore = ColorANSI::{{name.camelcase.id}} - return self - end - {% end %} - - raise ArgumentError.new "Unknown color: #{color}" - end - - def fore(@fore : Color) + @[Deprecated("Please use `bold` instead.")] + def bright + @mode |= Mode::Bold self end - def back(color : Symbol) - {% for name in COLORS %} - if color == :{{name.id}} - @back = ColorANSI::{{name.camelcase.id}} - return self - end - {% end %} - - raise ArgumentError.new "Unknown color: #{color}" + # Sets the foreground color of the object to *color*. + def fore(color : Color) + @fore = color + self end - def back(@back : Color) + # Sets the background color of the object to *color*. + def back(color : Color) + @back = color self end - def mode(mode : Symbol) - {% for name in MODES %} - if mode == :{{name.id}} - @mode |= MODE_{{name.upcase.id}}_FLAG - return self - end - {% end %} + @@warning_printed = false - raise ArgumentError.new "Unknown mode: #{mode}" - end + # Adds *mode* to the text's decorations. + def mode(mode : Mode) + # TODO: Remove this in the future + if mode == Mode::Bright + puts "Warning: The text decoration `bright` is deprecated. Please use `bold` instead.".colorize(:light_yellow) unless @@warning_printed + @@warning_printed = true + end - def on(color : Symbol) - back color + @mode |= mode + self end + # Enables or disables colors and text decoration on this object. def toggle(flag) @enabled = !!flag self end + # Appends this object colored and with text decoration to *io*. def to_s(io : IO) : Nil surround(io) do io << @object end end + # Inspects this object and makes the ANSI escape codes visible. def inspect(io : IO) : Nil - surround(io) do - @object.inspect(io) + internal_io = IO::Memory.new + surround(internal_io) do + @object.to_s(internal_io) end + io << internal_io.to_s.inspect end + # Surrounds *io* by the ANSI escape codes and lets you build colored strings: + # + # ``` + # require "colorize" + # + # io = IO::Memory.new + # + # with_color.red.surround(io) do + # io << "colorful" + # with_color.green.bold.surround(io) do + # io << " hello " + # end + # with_color.blue.surround(io) do + # io << "world" + # end + # io << " string" + # end + # + # io.to_s # => "colorful hello world string" + # # Where "colorful" is red, "hello" green, "world" blue and " string" red again + # ``` def surround(io = STDOUT) return yield io unless @enabled @@ -348,7 +378,8 @@ struct Colorize::Object(T) end end - private def to_named_tuple + # :nodoc: + def to_named_tuple { fore: @fore, back: @back, @@ -359,7 +390,7 @@ struct Colorize::Object(T) @@last_color = { fore: ColorANSI::Default.as(Color), back: ColorANSI::Default.as(Color), - mode: 0, + mode: Mode::None, } protected def self.surround(io, color) @@ -375,11 +406,12 @@ struct Colorize::Object(T) end end - private def self.append_start(io, color) + # :nodoc: + def self.append_start(io, color) last_color_is_default = @@last_color[:fore] == ColorANSI::Default && @@last_color[:back] == ColorANSI::Default && - @@last_color[:mode] == 0 + @@last_color[:mode] == Mode::None fore = color[:fore] back = color[:back] @@ -387,9 +419,9 @@ struct Colorize::Object(T) fore_is_default = fore == ColorANSI::Default back_is_default = back == ColorANSI::Default - mode_is_default = mode == 0 + no_mode = mode == Mode::None - if fore_is_default && back_is_default && mode_is_default && last_color_is_default || @@last_color == color + if @@last_color == color || fore_is_default && back_is_default && no_mode && last_color_is_default false else io << "\e[" @@ -397,7 +429,7 @@ struct Colorize::Object(T) printed = false unless last_color_is_default - io << MODE_DEFAULT + io << MODE_NONE printed = true end @@ -413,12 +445,14 @@ struct Colorize::Object(T) 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.bits_set? MODE_{{name.upcase.id}}_FLAG + unless no_mode + # TODO: replace this by + # {% for mode in Mode.constants.reject { |constant| constant == "All" || constant == "None" } %} + # when bright gets removed + {% for mode in %w(Bold Dim Underline Blink Reverse Hidden) %} + if mode.includes? Mode::{{mode.id}} io << ';' if printed - io << MODE_{{name.upcase.id}} + io << MODE_{{mode.upcase.id}} printed = true end {% end %} diff --git a/src/compiler/crystal/tools/print_hierarchy.cr b/src/compiler/crystal/tools/print_hierarchy.cr index 931cd2112733..2db3c3626617 100644 --- a/src/compiler/crystal/tools/print_hierarchy.cr +++ b/src/compiler/crystal/tools/print_hierarchy.cr @@ -225,7 +225,7 @@ module Crystal print " bytes)" end else - print "MISSING".colorize.red.bright + print "MISSING".colorize.red.bold end end puts diff --git a/src/compiler/crystal/util.cr b/src/compiler/crystal/util.cr index 578396c0c888..4be8babe13e4 100644 --- a/src/compiler/crystal/util.cr +++ b/src/compiler/crystal/util.cr @@ -17,7 +17,7 @@ module Crystal def self.error(msg, color, exit_code = 1, stderr = STDERR, leading_error = true) stderr.print "Error: ".colorize.toggle(color).red.bold if leading_error - stderr.puts msg.colorize.toggle(color).bright + stderr.puts msg.colorize.toggle(color).bold exit(exit_code) if exit_code end diff --git a/src/spec.cr b/src/spec.cr index 1df81a9b7b8c..7815aa9d1648 100644 --- a/src/spec.cr +++ b/src/spec.cr @@ -93,8 +93,6 @@ require "./spec/cli" module Spec end -Colorize.on_tty_only! - # :nodoc: # # Implement formatter configuration. diff --git a/src/spec/dsl.cr b/src/spec/dsl.cr index 084e1a07c7b4..91b2b4e9921f 100644 --- a/src/spec/dsl.cr +++ b/src/spec/dsl.cr @@ -3,13 +3,13 @@ require "option_parser" module Spec private COLORS = { - success: :green, - fail: :red, - error: :red, - pending: :yellow, - comment: :cyan, - focus: :cyan, - order: :cyan, + success: Colorize::ColorANSI::Green, + fail: Colorize::ColorANSI::Red, + error: Colorize::ColorANSI::Red, + pending: Colorize::ColorANSI::Yellow, + comment: Colorize::ColorANSI::Cyan, + focus: Colorize::ColorANSI::Cyan, + order: Colorize::ColorANSI::Cyan, } private LETTERS = {