diff --git a/spec/std/colorize_spec.cr b/spec/std/colorize_spec.cr index 78f9d6d4f612..b13aac8545c4 100644 --- a/spec/std/colorize_spec.cr +++ b/spec/std/colorize_spec.cr @@ -2,6 +2,8 @@ require "spec" require "colorize" describe "colorize" do + Colorize.force = true + it "colorizes without change" do "hello".colorize.to_s.should eq("hello") end @@ -60,6 +62,7 @@ describe "colorize" do it "colorizes foreground with background" do "hello".colorize.blue.on_green.to_s.should eq("\e[34;42mhello\e[0m") + "hello".colorize(fore: :blue, back: :green).to_s.should eq("\e[34;42mhello\e[0m") end it "colorizes foreground with background with mode" do @@ -71,6 +74,41 @@ describe "colorize" do "hello".colorize.fore(:red).to_s.should eq("\e[31mhello\e[0m") end + it "colorizes foreground with nil" do + "hello".colorize(nil).to_s.should eq("hello") + "hello".colorize.fore(nil).to_s.should eq("hello") + end + + it "colorizes foreground with 256 color" do + "hello".colorize(Colorize::Color256.new 111).to_s.should eq("\e[38;5;111mhello\e[0m") + "hello".colorize.fore(Colorize::Color256.new 111).to_s.should eq("\e[38;5;111mhello\e[0m") + end + + it "colorizes foreground with RGB" do + "hello".colorize(Colorize::ColorRGB.new 11, 22, 33).to_s.should eq("\e[38;2;11;22;33mhello\e[0m") + "hello".colorize.fore(Colorize::ColorRGB.new 11, 22, 33).to_s.should eq("\e[38;2;11;22;33mhello\e[0m") + end + + it "colorizes background with symbol" do + "hello".colorize(back: :red).to_s.should eq("\e[41mhello\e[0m") + "hello".colorize.back(:red).to_s.should eq("\e[41mhello\e[0m") + end + + it "colorizes background with nil" do + "hello".colorize(back: nil).to_s.should eq("hello") + "hello".colorize.back(nil).to_s.should eq("hello") + end + + it "colorizes background with 256 color" do + "hello".colorize(back: Colorize::Color256.new 111).to_s.should eq("\e[48;5;111mhello\e[0m") + "hello".colorize.back(Colorize::Color256.new 111).to_s.should eq("\e[48;5;111mhello\e[0m") + end + + it "colorizes background with RGB" do + "hello".colorize(back: Colorize::ColorRGB.new 11, 22, 33).to_s.should eq("\e[48;2;11;22;33mhello\e[0m") + "hello".colorize.back(Colorize::ColorRGB.new 11, 22, 33).to_s.should eq("\e[48;2;11;22;33mhello\e[0m") + end + it "colorizes mode with symbol" do "hello".colorize.mode(:bold).to_s.should eq("\e[1mhello\e[0m") end @@ -113,11 +151,11 @@ describe "colorize" do io.to_s.should eq("\e[31mhello\e[0m") end - it "colorizes with push and pop" do + it "colorizes with surround stack" do io = IO::Memory.new - with_color.red.push(io) do + with_color.red.surround(io) do io << "hello" - with_color.green.push(io) do + with_color.green.surround(io) do io << "world" end io << "bye" @@ -125,11 +163,11 @@ describe "colorize" do io.to_s.should eq("\e[31mhello\e[0;32mworld\e[0;31mbye\e[0m") end - it "colorizes with push and pop resets" do + it "colorizes with surround stack resets" do io = IO::Memory.new - with_color.red.push(io) do + with_color.red.surround(io) do io << "hello" - with_color.green.bold.push(io) do + with_color.green.bold.surround(io) do io << "world" end io << "bye" @@ -145,4 +183,32 @@ describe "colorize" do it "toggles off and on" do "hello".colorize.toggle(false).black.toggle(true).to_s.should eq("\e[30mhello\e[0m") end + + Colorize.force = false + + it "doesn't colorize no tty IO" do + "hello".colorize(fore: :red, back: :green, mode: :bold).to_s.should eq("hello") + "hello".colorize(fore: :red, back: :green, mode: :bold).inspect.should eq("\"hello\"") + io = IO::Memory.new + with_color(fore: :red, back: :green, mode: :bold).surround(io) do + io << "hello" + end + io.to_s.should eq("hello") + end + + it "colorize no tty IO with force" do + io = IO::Memory.new + "hello".colorize(fore: :red, back: :green, mode: :bold).to_s(io, force: true) + io.to_s.should eq("\e[31;42;1mhello\e[0m") + io.clear + + "hello".colorize(fore: :red, back: :green, mode: :bold).inspect(io, force: true) + io.to_s.should eq("\e[31;42;1m\"hello\"\e[0m") + io.clear + + with_color(fore: :red, back: :green, mode: :bold).surround(io, force: true) do + io << "hello" + end + io.to_s.should eq("\e[31;42;1mhello\e[0m") + end end diff --git a/src/colorize.cr b/src/colorize.cr index 6e5471eee529..32d367b9286b 100644 --- a/src/colorize.cr +++ b/src/colorize.cr @@ -1,9 +1,12 @@ -# With Colorize you can change the fore- and background colors and text decorations when rendering text +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. # # Its first argument changes the foreground color: +# # ``` # require "colorize" # @@ -13,6 +16,7 @@ # ``` # # There are alternative ways to change the foreground color: +# # ``` # "foo".colorize.fore(:green) # "foo".colorize.green @@ -66,6 +70,8 @@ # :white # ``` # +# and `Colorize::Color256`, `Colorize::ColorRGB`. +# # Available text decorations are: # ``` # :bold @@ -77,240 +83,20 @@ # :hidden # ``` module Colorize -end - -def with_color - "".colorize -end - -def with_color(color : Symbol) - "".colorize(color) -end - -module Colorize::ObjectExtensions - def colorize - Colorize::Object.new(self) + module ObjectExtensions + def colorize(fore = nil, back = nil, mode = nil) + Colorize::Object.new(self) + .fore(fore) + .back(back) + .mode(mode) + end end +end - def colorize(fore) - Colorize::Object.new(self).fore(fore) - end +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 -end diff --git a/src/colorize/builder.cr b/src/colorize/builder.cr new file mode 100644 index 000000000000..66ebb9adb78a --- /dev/null +++ b/src/colorize/builder.cr @@ -0,0 +1,99 @@ +require "./color" +require "./mode" + +# `Builder` is a mixin module for `Style` and `Object`. +# +# It provides builder methods for construct a style on terminal. +module Colorize::Builder + # Foreground color. + property fore = ColorANSI::Default + + # Background color. + property back = ColorANSI::Default + + # Activated modes like `:underscore` and `:blink`. + property mode = Mode::None + + # If this value is `true`, it outputs escape sequence. If `false`, it doesn't output. + property? enabled = true + + {% for name in ColorANSI.constants %} + # Set `ColorANSI::{{name}}` to foreground color. + def {{name.underscore}} + fore ColorANSI::{{name}} + end + + # Set `ColorANSI::{{name}}` to background color. + def on_{{name.underscore}} + back ColorANSI::{{name}} + end + {% end %} + + {% for name in Mode.constants.reject { |name| name == "All" || name == "None" } %} + # Activate `Mode::{{name}}` mode. + def {{name.underscore}} + mode Mode::{{name}} + @mode |= Mode::{{name}} + self + end + {% end %} + + # Set specified *color* to foreground color. + def fore(color : Symbol) + fore ColorANSI.parse?(color.to_s) || raise ArgumentError.new "unknown color: #{color}" + end + + # ditto + def fore(color : Color) + @fore = color + self + end + + # Set default to foreground color. + def fore(color : Nil) + self + end + + # Set specified *color* to background color. + def back(color : Symbol) + back ColorANSI.parse?(color.to_s) || raise ArgumentError.new "unknown color: #{color}" + end + + # ditto + def back(color : Nil) + self + end + + # Set default to background color. + def back(color : Color) + @back = color + self + end + + # Activate specified *mode*. + def mode(mode : Symbol) + mode Mode.parse?(mode.to_s) || raise ArgumentError.new "unknown mode: #{mode}" + end + + # ditto + def mode(mode : Mode) + @mode |= mode + self + end + + # Activate nothing. + def mode(mode : Nil) + self + end + + # Alias for `#back`. + def on(color) + back color + end + + # Toggle `#enabled?` flag. + def toggle(on) + @enabled = !!on + self + end +end diff --git a/src/colorize/color.cr b/src/colorize/color.cr new file mode 100644 index 000000000000..f5a087336809 --- /dev/null +++ b/src/colorize/color.cr @@ -0,0 +1,76 @@ +module Colorize + # `Color` is a union of available colors on terminal. + alias Color = ColorANSI | Color256 | ColorRGB + + 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 + + def fore_code + value.to_s + end + + def back_code + (value + 10).to_s + end + end + + struct Color256 + getter value : UInt8 + + def initialize(value) + @value = value.to_u8 + end + + def fore_code + "38;5;#{value}" + end + + def back_code + "48;5;#{value}" + end + + def default? + false + end + end + + struct ColorRGB + getter red : UInt8 + getter green : UInt8 + getter blue : UInt8 + + def initialize(red, green, blue) + @red = red.to_u8 + @green = green.to_u8 + @blue = blue.to_u8 + end + + def fore_code + "38;2;#{red};#{green};#{blue}" + end + + def back_code + "48;2;#{red};#{green};#{blue}" + end + + def default? + false + end + end +end diff --git a/src/colorize/io.cr b/src/colorize/io.cr new file mode 100644 index 000000000000..0d9b33868e86 --- /dev/null +++ b/src/colorize/io.cr @@ -0,0 +1,86 @@ +module Colorize + # If this value is `true`, it outputs escape sequence even if target `IO` object is not tty. + # + # This value effects below methods: + # + # - `#reset` + # - `Style#surround` + # - `Object#to_s` + # - `Object#inspect` + # + # May the *force* be with you. + class_property? force = false + + # Output reset escape sequence. + def self.reset(io, force = false) + return unless output?(io) || force + io << reset + end + + # Returns reset escape sequence. + def self.reset + "\e[0m" + end + + # :nodoc: + def self.output?(io : IO::FileDescriptor) + io.tty? || force? + end + + # If this result is `true`, given io accepts escape sequence. + def self.output?(io) + force? + end + + # Write escape sequence of *style* to *io* if `#output?` with *io* is `true`. + # If *reset* is `true`, it invoke `#reset` before applying *style*. + # + # It is used by `Style` and `Object` internally. + + # :nodoc: + def self.write_style(style, io, reset = false, force = false) + return false unless output?(io) || force + return false unless style.enabled? + + fore_is_default = style.fore.default? + back_is_default = style.back.default? + mode_is_default = style.mode.none? + + if fore_is_default && back_is_default && mode_is_default && !reset + false + else + io << "\e[" + + printed = false + + if reset + io << "0" + printed = true + end + + unless fore_is_default + io << ";" if printed + io << style.fore.fore_code + printed = true + end + + unless back_is_default + io << ";" if printed + io << style.back.back_code + printed = true + end + + unless mode_is_default + style.mode.codes do |i| + io << ";" if printed + io << i + printed = true + end + end + + io << "m" + + true + end + end +end diff --git a/src/colorize/mode.cr b/src/colorize/mode.cr new file mode 100644 index 000000000000..fb6909f7443e --- /dev/null +++ b/src/colorize/mode.cr @@ -0,0 +1,16 @@ +@[Flags] +enum Colorize::Mode + Bold = 1 << 0 + Bright = Bold + Dim = 1 << 1 + Underline = 1 << 3 + Blink = 1 << 4 + Reverse = 1 << 6 + Hidden = 1 << 7 + + def codes + 8.times do |i| + yield i + 1 unless value & (1 << i) == 0 + end + end +end diff --git a/src/colorize/object.cr b/src/colorize/object.cr new file mode 100644 index 000000000000..8bb663544198 --- /dev/null +++ b/src/colorize/object.cr @@ -0,0 +1,33 @@ +require "./builder" +require "./io" + +# `Object` wraps given object to highlight with a style on terminal. +# +# It is usual created by `ObjectExtension#colorize`. +struct Colorize::Object(T) + include Builder + + # Wrap a *object*. + def initialize(@object : T) + end + + # Output highlighted object. + def to_s(io, force = false) + surround(io, force) do + io << @object + end + end + + # Output highlighted object by using `Object#inspect`. + def inspect(io, force = false) + surround(io, force) do + @object.inspect(io) + end + end + + private def surround(io, force) + must_reset = Colorize.write_style self, io, force: force + yield + Colorize.reset io, force: force if must_reset + end +end diff --git a/src/colorize/style.cr b/src/colorize/style.cr new file mode 100644 index 000000000000..6b3cda699a3c --- /dev/null +++ b/src/colorize/style.cr @@ -0,0 +1,72 @@ +require "./builder" +require "./io" + +# `Style` represents a style on the terminal. +# +# ``` +# # Create a new style +# style = Colorize::Style.new(:red, :blue, :underline) +# +# # Or, we can use `Builder`'s methods for construction +# style = Colorize::Style.new +# .fore(:red) +# .back(:blue) +# .mode(:underline) +# +# # Get escape sequence +# style.to_s # => "\e[31;44;4m" +# +# # Highlight the content in a block with this style +# style.surround do +# puts "Hello, World!" +# end +# ``` +struct Colorize::Style + include Builder + + # Creates a new instance with *fore*, *back* and *mode*. + # + # All parameter is passed to each setter methods. + def initialize(fore = nil, back = nil, mode = nil) + fore fore + back back + mode mode + end + + # Get escape sequence text. + # + # NOTE: Thie methods outputs escape sequence if *io* is not tty. This behavior is difference from other `Colorize` methods. + def to_s(io) + Colorize.write_style self, io, force: true + end + + private STACK = [] of Colorize::Style + + # Highlight the content in the block with this style + # + # It outputs escape sequence, then invoke the block. After all, it outputs reset escape sequence if needed. + # + # This method has a stack internally, so it keeps highlight if nested. + def surround(io = STDOUT, force = false) : Nil + last_style = STACK.last? + must_reset = Colorize.write_style self, io, reset: !!last_style, force: force + STACK.push self + + yield io + + STACK.pop + if must_reset + if last_style + Colorize.write_style last_style, io, reset: true, force: force + else + Colorize.reset io, force: force + end + end + end + + # DEPRECATED: use `Colorize::Style#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 +end diff --git a/src/compiler/crystal/tools/print_hierarchy.cr b/src/compiler/crystal/tools/print_hierarchy.cr index 3882f14c47e5..b9dba3c837a6 100644 --- a/src/compiler/crystal/tools/print_hierarchy.cr +++ b/src/compiler/crystal/tools/print_hierarchy.cr @@ -25,7 +25,7 @@ module Crystal compute_targets(@program.types, exp, false) end - with_color.light_gray.bold.push(STDOUT) do + with_color.light_gray.bold.surround(STDOUT) do print_type @program.object end end @@ -124,7 +124,7 @@ module Crystal if (type.is_a?(NonGenericClassType) || type.is_a?(GenericClassInstanceType)) && !type.is_a?(PointerInstanceType) && !type.is_a?(ProcInstanceType) size = @llvm_typer.size_of(@llvm_typer.llvm_struct_type(type)) - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print " (" print size.to_s print " bytes)" @@ -174,7 +174,7 @@ module Crystal print " " end - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print name.ljust(max_name_size) print " : " print var @@ -209,13 +209,13 @@ module Crystal print " " end - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print ivar.name.ljust(max_name_size) print " : " if ivar_type = ivar.type? print ivar_type.to_s.ljust(max_type_size) size = @llvm_typer.size_of(@llvm_typer.llvm_embedded_type(ivar_type)) - with_color.light_gray.push(STDOUT) do + with_color.light_gray.surround(STDOUT) do print " (" print size.to_s.rjust(max_bytes_size) print " bytes)"