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