diff --git a/docsite/source/index.html.md b/docsite/source/index.html.md
index 67318aa..4241a07 100644
--- a/docsite/source/index.html.md
+++ b/docsite/source/index.html.md
@@ -12,6 +12,7 @@ sections:
- variadic-arguments
- commands-with-subcommands-and-params
- callbacks
+ - styling-your-output
---
`dry-cli` is a general-purpose framework for developing Command Line Interface (CLI) applications. It represents commands as objects that can be registered and offers support for arguments, options and forwarding variadic arguments to a sub-command.
diff --git a/docsite/source/styling-your-output.html.md b/docsite/source/styling-your-output.html.md
new file mode 100644
index 0000000..bcec9dc
--- /dev/null
+++ b/docsite/source/styling-your-output.html.md
@@ -0,0 +1,59 @@
+---
+title: Styling your output
+layout: gem-single
+name: dry-cli
+---
+
+`dry-cli` comes with some functions to help you style text in the terminal. The program bellow demonstrate all available styles:
+
+```ruby
+#!/usr/bin/env ruby
+require "bundler/setup"
+require "dry/cli"
+
+module StylesDemo
+ extend Dry::CLI::Registry
+
+ class Print < Dry::CLI::Command
+ desc "Demonstrate all available styles"
+
+ # rubocop:disable Metrics/AbcSize
+ def call
+ demo = <<~DEMO
+ `stylize("This is bold").bold` #=> #{stylize("This is bold").bold}
+ `stylize("This is dim").dim` #=> #{stylize("This is dim").dim}
+ `stylize("This is italic").italic` #=> #{stylize("This is italic").italic}
+ `stylize("This is underline").underline` #=> #{stylize("This is underline").underline}
+ `stylize("This blinks").blink` #=> #{stylize("This blinks").blink}
+ `stylize("This was reversed").reverse` #=> #{stylize("This was reversed").reverse}
+ `stylize("This is invisible").invisible` #=> #{stylize("This is invisible").invisible} (you can't see it, right?)
+ `stylize("This is black").black` #=> #{stylize("This is black").black}
+ `stylize("This is red").red` #=> #{stylize("This is red").red}
+ `stylize("This is green").green` #=> #{stylize("This is green").green}
+ `stylize("This is yellow").yellow` #=> #{stylize("This is yellow").yellow}
+ `stylize("This is blue").blue` #=> #{stylize("This is blue").blue}
+ `stylize("This is magenta").magenta` #=> #{stylize("This is magenta").magenta}
+ `stylize("This is cyan").cyan` #=> #{stylize("This is cyan").cyan}
+ `stylize("This is white").white` #=> #{stylize("This is white").white}
+ `stylize("This is black").on_black` #=> #{stylize("This is black").on_black}
+ `stylize("This is red").on_red` #=> #{stylize("This is red").on_red}
+ `stylize("This is green").on_green` #=> #{stylize("This is green").on_green}
+ `stylize("This is yellow").on_yellow` #=> #{stylize("This is yellow").on_yellow}
+ `stylize("This is blue").on_blue` #=> #{stylize("This is blue").on_blue}
+ `stylize("This is magenta").on_magenta` #=> #{stylize("This is magenta").on_magenta}
+ `stylize("This is cyan").on_cyan` #=> #{stylize("This is cyan").on_cyan}
+ `stylize("This is white").on_white` #=> #{stylize("This is white").on_white}
+ `stylize("This is bold red").bold.red #=> #{stylize("This is bold red").bold.red}
+ `stylize("This is bold on green").bold.on_green` #=> #{stylize("This is bold on green").bold.on_green}
+ `stylize("This is bold red on green").bold.red.on_green` #=> #{stylize("This is bold red on green").bold.red.on_green}
+ DEMO
+ puts demo
+ end
+ # rubocop:enable Metrics/AbcSize
+ end
+
+ register "print", Print
+end
+
+Dry.CLI(StylesDemo).call
+```
diff --git a/lib/dry/cli/command.rb b/lib/dry/cli/command.rb
index 3e6a601..2a53680 100644
--- a/lib/dry/cli/command.rb
+++ b/lib/dry/cli/command.rb
@@ -2,6 +2,7 @@
require "forwardable"
require "dry/cli/option"
+require "dry/cli/styles"
module Dry
class CLI
@@ -9,6 +10,8 @@ class CLI
#
# @since 0.1.0
class Command
+ include Styles
+
# @since 0.1.0
# @api private
def self.inherited(base)
diff --git a/lib/dry/cli/styles.rb b/lib/dry/cli/styles.rb
new file mode 100644
index 0000000..3917958
--- /dev/null
+++ b/lib/dry/cli/styles.rb
@@ -0,0 +1,195 @@
+# frozen_string_literal: true
+
+module Dry
+ class CLI
+ # Collection of functions to style text
+ #
+ # @since 1.3.0
+ module Styles
+ RESET = 0
+ BOLD = 1
+ DIM = 2
+ ITALIC = 3
+ UNDERLINE = 4
+ BLINK = 5
+ REVERSE = 7
+ INVISIBLE = 8
+ BLACK = 30
+ RED = 31
+ GREEN = 32
+ YELLOW = 33
+ BLUE = 34
+ MAGENTA = 35
+ CYAN = 36
+ WHITE = 37
+ ON_BLACK = 40
+ ON_RED = 41
+ ON_GREEN = 42
+ ON_YELLOW = 43
+ ON_BLUE = 44
+ ON_MAGENTA = 45
+ ON_CYAN = 46
+ ON_WHITE = 47
+
+ # Returns a text that can be styled
+ #
+ # @param text [String] text to be styled
+ #
+ # @since 1.3.0
+ def stylize(text)
+ StyledText.new(text)
+ end
+
+ # Styled text
+ #
+ # @since 1.3.0
+ class StyledText
+ def initialize(text, escape_code = nil)
+ @text = text
+ @escape_code = escape_code
+ end
+
+ # Makes `StyledText` printable
+ #
+ # @since 1.3.0
+ def to_s
+ text + escape_code
+ end
+
+ # since 1.3.0
+ def bold
+ chainable_update!(BOLD, text)
+ end
+
+ # since 1.3.0
+ def dim
+ chainable_update!(DIM, text)
+ end
+
+ # since 1.3.0
+ def italic
+ chainable_update!(ITALIC, text)
+ end
+
+ # since 1.3.0
+ def underline
+ chainable_update!(UNDERLINE, text)
+ end
+
+ # since 1.3.0
+ def blink
+ chainable_update!(BLINK, text)
+ end
+
+ # since 1.3.0
+ def reverse
+ chainable_update!(REVERSE, text)
+ end
+
+ # since 1.3.0
+ def invisible
+ chainable_update!(INVISIBLE, text)
+ end
+
+ # since 1.3.0
+ def black
+ chainable_update!(BLACK, text)
+ end
+
+ # since 1.3.0
+ def red
+ chainable_update!(RED, text)
+ end
+
+ # since 1.3.0
+ def green
+ chainable_update!(GREEN, text)
+ end
+
+ # since 1.3.0
+ def yellow
+ chainable_update!(YELLOW, text)
+ end
+
+ # since 1.3.0
+ def blue
+ chainable_update!(BLUE, text)
+ end
+
+ # since 1.3.0
+ def magenta
+ chainable_update!(MAGENTA, text)
+ end
+
+ # since 1.3.0
+ def cyan
+ chainable_update!(CYAN, text)
+ end
+
+ # since 1.3.0
+ def white
+ chainable_update!(WHITE, text)
+ end
+
+ # since 1.3.0
+ def on_black
+ chainable_update!(ON_BLACK, text)
+ end
+
+ # since 1.3.0
+ def on_red
+ chainable_update!(ON_RED, text)
+ end
+
+ # since 1.3.0
+ def on_green
+ chainable_update!(ON_GREEN, text)
+ end
+
+ # since 1.3.0
+ def on_yellow
+ chainable_update!(ON_YELLOW, text)
+ end
+
+ # since 1.3.0
+ def on_blue
+ chainable_update!(ON_BLUE, text)
+ end
+
+ # since 1.3.0
+ def on_magenta
+ chainable_update!(ON_MAGENTA, text)
+ end
+
+ # since 1.3.0
+ def on_cyan
+ chainable_update!(ON_CYAN, text)
+ end
+
+ # since 1.3.0
+ def on_white
+ chainable_update!(ON_WHITE, text)
+ end
+
+ private
+
+ attr_reader :text, :escape_code
+
+ # @since 1.3.0
+ # @api private
+ def chainable_update!(style, new_text)
+ StyledText.new(
+ select_graphic_rendition(style) + new_text,
+ select_graphic_rendition(RESET)
+ )
+ end
+
+ # @since 1.3.0
+ # @api private
+ def select_graphic_rendition(code)
+ "\e[#{code}m"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/integration/rendering_spec.rb b/spec/integration/rendering_spec.rb
index 3b0209d..9bf70f6 100644
--- a/spec/integration/rendering_spec.rb
+++ b/spec/integration/rendering_spec.rb
@@ -28,4 +28,38 @@
expect(stderr).to eq(expected)
end
+
+ it "prints styled text" do
+ stdout, = Open3.capture3("styles print")
+
+ expected = <<~OUT
+ `stylize(\"This is bold\").bold` #=> \e[1mThis is bold\e[0m
+ `stylize(\"This is dim\").dim` #=> \e[2mThis is dim\e[0m
+ `stylize(\"This is italic\").italic` #=> \e[3mThis is italic\e[0m
+ `stylize(\"This is underline\").underline` #=> \e[4mThis is underline\e[0m
+ `stylize(\"This blinks\").blink` #=> \e[5mThis blinks\e[0m
+ `stylize(\"This was reversed\").reverse` #=> \e[7mThis was reversed\e[0m
+ `stylize(\"This is invisible\").invisible` #=> \e[8mThis is invisible\e[0m (you can't see it, right?)
+ `stylize(\"This is black\").black` #=> \e[30mThis is black\e[0m
+ `stylize(\"This is red\").red` #=> \e[31mThis is red\e[0m
+ `stylize(\"This is green\").green` #=> \e[32mThis is green\e[0m
+ `stylize(\"This is yellow\").yellow` #=> \e[33mThis is yellow\e[0m
+ `stylize(\"This is blue\").blue` #=> \e[34mThis is blue\e[0m
+ `stylize(\"This is magenta\").magenta` #=> \e[35mThis is magenta\e[0m
+ `stylize(\"This is cyan\").cyan` #=> \e[36mThis is cyan\e[0m
+ `stylize(\"This is white\").white` #=> \e[37mThis is white\e[0m
+ `stylize(\"This is black\").on_black` #=> \e[40mThis is black\e[0m
+ `stylize(\"This is red\").on_red` #=> \e[41mThis is red\e[0m
+ `stylize(\"This is green\").on_green` #=> \e[42mThis is green\e[0m
+ `stylize(\"This is yellow\").on_yellow` #=> \e[43mThis is yellow\e[0m
+ `stylize(\"This is blue\").on_blue` #=> \e[44mThis is blue\e[0m
+ `stylize(\"This is magenta\").on_magenta` #=> \e[45mThis is magenta\e[0m
+ `stylize(\"This is cyan\").on_cyan` #=> \e[46mThis is cyan\e[0m
+ `stylize(\"This is white\").on_white` #=> \e[47mThis is white\e[0m
+ `stylize(\"This is bold red\").bold.red #=> \e[31m\e[1mThis is bold red\e[0m
+ `stylize(\"This is bold on green\").bold.on_green` #=> \e[42m\e[1mThis is bold on green\e[0m
+ `stylize(\"This is bold red on green\").bold.red.on_green` #=> \e[42m\e[31m\e[1mThis is bold red on green\e[0m
+ OUT
+ expect(stdout).to eq(expected)
+ end
end
diff --git a/spec/support/fixtures/foo b/spec/support/fixtures/foo
index bf37e0f..7ad392b 100755
--- a/spec/support/fixtures/foo
+++ b/spec/support/fixtures/foo
@@ -35,7 +35,7 @@ module Foo
]
def call(engine: nil, **)
- puts "console - engine: #{engine}"
+ puts stylize("console - engine: ").blue.bold, stylize(engine.to_s).magenta
end
end
diff --git a/spec/support/fixtures/styles b/spec/support/fixtures/styles
new file mode 100755
index 0000000..b71c0ba
--- /dev/null
+++ b/spec/support/fixtures/styles
@@ -0,0 +1,51 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+$LOAD_PATH.unshift "#{__dir__}/../../../lib"
+require "dry/cli"
+
+module StylesDemo
+ extend Dry::CLI::Registry
+
+ class Print < Dry::CLI::Command
+ desc "Demonstrate all available styles"
+
+ # rubocop:disable Metrics/AbcSize
+ def call
+ demo = <<~DEMO
+ `stylize("This is bold").bold` #=> #{stylize("This is bold").bold}
+ `stylize("This is dim").dim` #=> #{stylize("This is dim").dim}
+ `stylize("This is italic").italic` #=> #{stylize("This is italic").italic}
+ `stylize("This is underline").underline` #=> #{stylize("This is underline").underline}
+ `stylize("This blinks").blink` #=> #{stylize("This blinks").blink}
+ `stylize("This was reversed").reverse` #=> #{stylize("This was reversed").reverse}
+ `stylize("This is invisible").invisible` #=> #{stylize("This is invisible").invisible} (you can't see it, right?)
+ `stylize("This is black").black` #=> #{stylize("This is black").black}
+ `stylize("This is red").red` #=> #{stylize("This is red").red}
+ `stylize("This is green").green` #=> #{stylize("This is green").green}
+ `stylize("This is yellow").yellow` #=> #{stylize("This is yellow").yellow}
+ `stylize("This is blue").blue` #=> #{stylize("This is blue").blue}
+ `stylize("This is magenta").magenta` #=> #{stylize("This is magenta").magenta}
+ `stylize("This is cyan").cyan` #=> #{stylize("This is cyan").cyan}
+ `stylize("This is white").white` #=> #{stylize("This is white").white}
+ `stylize("This is black").on_black` #=> #{stylize("This is black").on_black}
+ `stylize("This is red").on_red` #=> #{stylize("This is red").on_red}
+ `stylize("This is green").on_green` #=> #{stylize("This is green").on_green}
+ `stylize("This is yellow").on_yellow` #=> #{stylize("This is yellow").on_yellow}
+ `stylize("This is blue").on_blue` #=> #{stylize("This is blue").on_blue}
+ `stylize("This is magenta").on_magenta` #=> #{stylize("This is magenta").on_magenta}
+ `stylize("This is cyan").on_cyan` #=> #{stylize("This is cyan").on_cyan}
+ `stylize("This is white").on_white` #=> #{stylize("This is white").on_white}
+ `stylize("This is bold red").bold.red #=> #{stylize("This is bold red").bold.red}
+ `stylize("This is bold on green").bold.on_green` #=> #{stylize("This is bold on green").bold.on_green}
+ `stylize("This is bold red on green").bold.red.on_green` #=> #{stylize("This is bold red on green").bold.red.on_green}
+ DEMO
+ puts demo
+ end
+ # rubocop:enable Metrics/AbcSize
+ end
+
+ register "print", Print
+end
+
+Dry.CLI(StylesDemo).call