From 2e130a0f43d93c2f39a71a6411fe3858125d4a18 Mon Sep 17 00:00:00 2001 From: Gustavo Ribeiro Date: Mon, 2 Dec 2024 14:37:33 -0300 Subject: [PATCH 1/4] Add `Colors` module to help users set text colors --- lib/dry/cli/colors.rb | 106 +++++++++++++++++++++++++++++++ lib/dry/cli/command.rb | 3 + spec/support/fixtures/colors | 49 ++++++++++++++ spec/support/fixtures/foo | 2 +- spec/unit/dry/cli/colors_spec.rb | 26 ++++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 lib/dry/cli/colors.rb create mode 100755 spec/support/fixtures/colors create mode 100644 spec/unit/dry/cli/colors_spec.rb diff --git a/lib/dry/cli/colors.rb b/lib/dry/cli/colors.rb new file mode 100644 index 00000000..4ba4ac24 --- /dev/null +++ b/lib/dry/cli/colors.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Dry + class CLI + # Collection of functions to colorize text. + # + # @since 1.3.0 + module Colors + # since 1.3.0 + def bold(text) + ensure_clean_sequence("\e[1m#{text}") + end + + # since 1.3.0 + def black(text) + ensure_clean_sequence("\e[30m#{text}") + end + + # since 1.3.0 + def red(text) + ensure_clean_sequence("\e[31m#{text}") + end + + # since 1.3.0 + def green(text) + ensure_clean_sequence("\e[32m#{text}") + end + + # since 1.3.0 + def yellow(text) + ensure_clean_sequence("\e[33m#{text}") + end + + # since 1.3.0 + def blue(text) + ensure_clean_sequence("\e[34m#{text}") + end + + # since 1.3.0 + def magenta(text) + ensure_clean_sequence("\e[35m#{text}") + end + + # since 1.3.0 + def cyan(text) + ensure_clean_sequence("\e[36m#{text}") + end + + # since 1.3.0 + def white(text) + ensure_clean_sequence("\e[37m#{text}") + end + + # since 1.3.0 + def on_black(text) + ensure_clean_sequence("\e[40m#{text}") + end + + # since 1.3.0 + def on_red(text) + ensure_clean_sequence("\e[41m#{text}") + end + + # since 1.3.0 + def on_green(text) + ensure_clean_sequence("\e[42m#{text}") + end + + # since 1.3.0 + def on_yellow(text) + ensure_clean_sequence("\e[43m#{text}") + end + + # since 1.3.0 + def on_blue(text) + ensure_clean_sequence("\e[44m#{text}") + end + + # since 1.3.0 + def on_magenta(text) + ensure_clean_sequence("\e[45m#{text}") + end + + # since 1.3.0 + def on_cyan(text) + ensure_clean_sequence("\e[46m#{text}") + end + + # since 1.3.0 + def on_white(text) + ensure_clean_sequence("\e[47m#{text}") + end + + private + + # @since 1.3.0 + # @api private + def ensure_clean_sequence(text) + clen_text = text + clear = "\e[0m" + clen_text += clear unless text.end_with?(clear) + clen_text + end + end + end +end diff --git a/lib/dry/cli/command.rb b/lib/dry/cli/command.rb index 84e50d70..2fb5da6c 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/colors" module Dry class CLI @@ -9,6 +10,8 @@ class CLI # # @since 0.1.0 class Command + include Colors + # @since 0.1.0 # @api private def self.inherited(base) diff --git a/spec/support/fixtures/colors b/spec/support/fixtures/colors new file mode 100755 index 00000000..a6ccb220 --- /dev/null +++ b/spec/support/fixtures/colors @@ -0,0 +1,49 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +$LOAD_PATH.unshift "#{__dir__}/../../../lib" +require "dry/cli" + +module ColorsDemo + extend Dry::CLI::Registry + + class Print < Dry::CLI::Command + desc "Demonstrate all available colors" + + # rubocop:disable Metrics/AbcSize + def call + demo = <<~DEMO + # Style ############################################################### + Bold: #{bold("This is bold")} + # Foreground ########################################################## + Black: #{black("This is black")} + Red: #{red("This is red")} + Green: #{green("This is green")} + Yellow: #{yellow("This is yellow")} + Blue: #{blue("This is blue")} + Magenta: #{magenta("This is magenta")} + Cyan: #{cyan("This is cyan")} + White: #{white("This is white")} + # Background ########################################################## + Black: #{on_black("This is black")} + Red: #{on_red("This is red")} + Green: #{on_green("This is green")} + Yellow: #{on_yellow("This is yellow")} + Blue: #{on_blue("This is blue")} + Magenta: #{on_magenta("This is magenta")} + Cyan: #{on_cyan("This is cyan")} + White: #{on_white("This is white")} + # Combinations ######################################################## + Bold+Foreground: #{bold(red("This is bold red"))} + Bold+Background: #{bold(on_green("This is bold on green"))} + Bold+Foreground+Background: #{bold(red(on_green("This is bold red on green")))} + DEMO + puts demo + end + # rubocop:enable Metrics/AbcSize + end + + register "print", Print +end + +Dry.CLI(ColorsDemo).call diff --git a/spec/support/fixtures/foo b/spec/support/fixtures/foo index c7ddb543..639040ac 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 blue(bold("console - engine: ")), magenta(engine.to_s) end end diff --git a/spec/unit/dry/cli/colors_spec.rb b/spec/unit/dry/cli/colors_spec.rb new file mode 100644 index 00000000..7b6f4335 --- /dev/null +++ b/spec/unit/dry/cli/colors_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.describe "Colors" do + module Dummy + module_function + + include Dry::CLI::Colors + + def only_bold + bold("one") + end + + def bold_and_black + black(bold("two")) + end + end + + it "doesn't duplicate clean escape sequences" do + only_bold = Dummy.only_bold + expect(only_bold.end_with?("\e[0m")).to eq(true) + expect(only_bold.end_with?("\e[0m\e[0m")).to eq(false) + bold_and_black = Dummy.bold_and_black + expect(bold_and_black.end_with?("\e[0m")).to eq(true) + expect(bold_and_black.end_with?("\e[0m\e[0m")).to eq(false) + end +end From 162afdbc47ceb1021571bcac42c78d296bf69298 Mon Sep 17 00:00:00 2001 From: Gustavo Ribeiro Date: Mon, 2 Dec 2024 15:48:13 -0300 Subject: [PATCH 2/4] Document how to colorize `dry-cli` output --- docsite/source/colorizing-your-output.html.md | 55 +++++++++++++++++++ docsite/source/index.html.md | 1 + 2 files changed, 56 insertions(+) create mode 100644 docsite/source/colorizing-your-output.html.md diff --git a/docsite/source/colorizing-your-output.html.md b/docsite/source/colorizing-your-output.html.md new file mode 100644 index 00000000..452947e6 --- /dev/null +++ b/docsite/source/colorizing-your-output.html.md @@ -0,0 +1,55 @@ +--- +title: Colorizing your output +layout: gem-single +name: dry-cli +--- + +`dry-cli` comes with some functions to help you print colored text in the terminal. The program bellow demonstrate all available functions: + +```ruby +#!/usr/bin/env ruby +require "bundler/setup" +require "dry/cli" + +module ColorsDemo + extend Dry::CLI::Registry + + class Print < Dry::CLI::Command + desc "Demonstrate all available colors" + + def call + demo = <<~DEMO + # Style ############################################################### + Bold: #{bold("This is bold")} + # Foreground ########################################################## + Black: #{black("This is black")} + Red: #{red("This is red")} + Green: #{green("This is green")} + Yellow: #{yellow("This is yellow")} + Blue: #{blue("This is blue")} + Magenta: #{magenta("This is magenta")} + Cyan: #{cyan("This is cyan")} + White: #{white("This is white")} + # Background ########################################################## + Black: #{on_black("This is black")} + Red: #{on_red("This is red")} + Green: #{on_green("This is green")} + Yellow: #{on_yellow("This is yellow")} + Blue: #{on_blue("This is blue")} + Magenta: #{on_magenta("This is magenta")} + Cyan: #{on_cyan("This is cyan")} + White: #{on_white("This is white")} + # Combinations ######################################################## + Bold+Foreground: #{bold(red("This is bold red"))} + Bold+Background: #{bold(on_green("This is bold on green"))} + Bold+Foreground+Background: #{bold(red(on_green("This is bold red on green")))} + DEMO + puts demo + end + end + + register "print", Print +end + +Dry.CLI(ColorsDemo).call +``` diff --git a/docsite/source/index.html.md b/docsite/source/index.html.md index 67318aa1..d0e3a652 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 + - colorizing-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. From 8df0785db69153b715c5996e7db0c59f2624a777 Mon Sep 17 00:00:00 2001 From: Gustavo Ribeiro Date: Mon, 2 Dec 2024 16:27:30 -0300 Subject: [PATCH 3/4] Rename `Colors` to `Styles` --- docsite/source/colorizing-your-output.html.md | 55 ----------------- docsite/source/index.html.md | 2 +- docsite/source/styling-your-output.html.md | 59 +++++++++++++++++++ lib/dry/cli/command.rb | 4 +- lib/dry/cli/{colors.rb => styles.rb} | 34 ++++++++++- spec/support/fixtures/colors | 49 --------------- spec/support/fixtures/styles | 51 ++++++++++++++++ spec/unit/dry/cli/colors_spec.rb | 26 -------- spec/unit/dry/cli/styles_spec.rb | 17 ++++++ 9 files changed, 162 insertions(+), 135 deletions(-) delete mode 100644 docsite/source/colorizing-your-output.html.md create mode 100644 docsite/source/styling-your-output.html.md rename lib/dry/cli/{colors.rb => styles.rb} (75%) delete mode 100755 spec/support/fixtures/colors create mode 100755 spec/support/fixtures/styles delete mode 100644 spec/unit/dry/cli/colors_spec.rb create mode 100644 spec/unit/dry/cli/styles_spec.rb diff --git a/docsite/source/colorizing-your-output.html.md b/docsite/source/colorizing-your-output.html.md deleted file mode 100644 index 452947e6..00000000 --- a/docsite/source/colorizing-your-output.html.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Colorizing your output -layout: gem-single -name: dry-cli ---- - -`dry-cli` comes with some functions to help you print colored text in the terminal. The program bellow demonstrate all available functions: - -```ruby -#!/usr/bin/env ruby -require "bundler/setup" -require "dry/cli" - -module ColorsDemo - extend Dry::CLI::Registry - - class Print < Dry::CLI::Command - desc "Demonstrate all available colors" - - def call - demo = <<~DEMO - # Style ############################################################### - Bold: #{bold("This is bold")} - # Foreground ########################################################## - Black: #{black("This is black")} - Red: #{red("This is red")} - Green: #{green("This is green")} - Yellow: #{yellow("This is yellow")} - Blue: #{blue("This is blue")} - Magenta: #{magenta("This is magenta")} - Cyan: #{cyan("This is cyan")} - White: #{white("This is white")} - # Background ########################################################## - Black: #{on_black("This is black")} - Red: #{on_red("This is red")} - Green: #{on_green("This is green")} - Yellow: #{on_yellow("This is yellow")} - Blue: #{on_blue("This is blue")} - Magenta: #{on_magenta("This is magenta")} - Cyan: #{on_cyan("This is cyan")} - White: #{on_white("This is white")} - # Combinations ######################################################## - Bold+Foreground: #{bold(red("This is bold red"))} - Bold+Background: #{bold(on_green("This is bold on green"))} - Bold+Foreground+Background: #{bold(red(on_green("This is bold red on green")))} - DEMO - puts demo - end - end - - register "print", Print -end - -Dry.CLI(ColorsDemo).call -``` diff --git a/docsite/source/index.html.md b/docsite/source/index.html.md index d0e3a652..4241a07d 100644 --- a/docsite/source/index.html.md +++ b/docsite/source/index.html.md @@ -12,7 +12,7 @@ sections: - variadic-arguments - commands-with-subcommands-and-params - callbacks - - colorizing-your-output + - 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 00000000..931657e2 --- /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 + `bold` #{bold("This is bold")} + `dim` #{dim("This is dim")} + `italic` #{italic("This is italic")} + `underline` #{underline("This is underline")} + `blink` #{blink("This blinks")} + `reverse` #{reverse("This was reversed")} + `invisible` #{invisible("This is invisible")} (you can't see it, right?) + `black` #{black("This is black")} + `red` #{red("This is red")} + `green` #{green("This is green")} + `yellow` #{yellow("This is yellow")} + `blue` #{blue("This is blue")} + `magenta` #{magenta("This is magenta")} + `cyan` #{cyan("This is cyan")} + `white` #{white("This is white")} + `on_black` #{on_black("This is black")} + `on_red` #{on_red("This is red")} + `on_green` #{on_green("This is green")} + `on_yellow` #{on_yellow("This is yellow")} + `on_blue` #{on_blue("This is blue")} + `on_magenta` #{on_magenta("This is magenta")} + `on_cyan` #{on_cyan("This is cyan")} + `on_white` #{on_white("This is white")} + `bold`+`red`: #{bold(red("This is bold red"))} + `bold`+`on_green`: #{bold(on_green("This is bold on green"))} + `bold`+`red`+`on_green`: #{bold(red(on_green("This is 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 2fb5da6c..ed0d8b09 100644 --- a/lib/dry/cli/command.rb +++ b/lib/dry/cli/command.rb @@ -2,7 +2,7 @@ require "forwardable" require "dry/cli/option" -require "dry/cli/colors" +require "dry/cli/styles" module Dry class CLI @@ -10,7 +10,7 @@ class CLI # # @since 0.1.0 class Command - include Colors + include Styles # @since 0.1.0 # @api private diff --git a/lib/dry/cli/colors.rb b/lib/dry/cli/styles.rb similarity index 75% rename from lib/dry/cli/colors.rb rename to lib/dry/cli/styles.rb index 4ba4ac24..4df7fa39 100644 --- a/lib/dry/cli/colors.rb +++ b/lib/dry/cli/styles.rb @@ -2,15 +2,45 @@ module Dry class CLI - # Collection of functions to colorize text. + # Collection of functions to style text. # # @since 1.3.0 - module Colors + module Styles # since 1.3.0 def bold(text) ensure_clean_sequence("\e[1m#{text}") end + # since 1.3.0 + def dim(text) + ensure_clean_sequence("\e[2m#{text}") + end + + # since 1.3.0 + def italic(text) + ensure_clean_sequence("\e[3m#{text}") + end + + # since 1.3.0 + def underline(text) + ensure_clean_sequence("\e[4m#{text}") + end + + # since 1.3.0 + def blink(text) + ensure_clean_sequence("\e[5m#{text}") + end + + # since 1.3.0 + def reverse(text) + ensure_clean_sequence("\e[7m#{text}") + end + + # since 1.3.0 + def invisible(text) + ensure_clean_sequence("\e[8m#{text}") + end + # since 1.3.0 def black(text) ensure_clean_sequence("\e[30m#{text}") diff --git a/spec/support/fixtures/colors b/spec/support/fixtures/colors deleted file mode 100755 index a6ccb220..00000000 --- a/spec/support/fixtures/colors +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -$LOAD_PATH.unshift "#{__dir__}/../../../lib" -require "dry/cli" - -module ColorsDemo - extend Dry::CLI::Registry - - class Print < Dry::CLI::Command - desc "Demonstrate all available colors" - - # rubocop:disable Metrics/AbcSize - def call - demo = <<~DEMO - # Style ############################################################### - Bold: #{bold("This is bold")} - # Foreground ########################################################## - Black: #{black("This is black")} - Red: #{red("This is red")} - Green: #{green("This is green")} - Yellow: #{yellow("This is yellow")} - Blue: #{blue("This is blue")} - Magenta: #{magenta("This is magenta")} - Cyan: #{cyan("This is cyan")} - White: #{white("This is white")} - # Background ########################################################## - Black: #{on_black("This is black")} - Red: #{on_red("This is red")} - Green: #{on_green("This is green")} - Yellow: #{on_yellow("This is yellow")} - Blue: #{on_blue("This is blue")} - Magenta: #{on_magenta("This is magenta")} - Cyan: #{on_cyan("This is cyan")} - White: #{on_white("This is white")} - # Combinations ######################################################## - Bold+Foreground: #{bold(red("This is bold red"))} - Bold+Background: #{bold(on_green("This is bold on green"))} - Bold+Foreground+Background: #{bold(red(on_green("This is bold red on green")))} - DEMO - puts demo - end - # rubocop:enable Metrics/AbcSize - end - - register "print", Print -end - -Dry.CLI(ColorsDemo).call diff --git a/spec/support/fixtures/styles b/spec/support/fixtures/styles new file mode 100755 index 00000000..638bffaf --- /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 + `bold` #{bold("This is bold")} + `dim` #{dim("This is dim")} + `italic` #{italic("This is italic")} + `underline` #{underline("This is underline")} + `blink` #{blink("This blinks")} + `reverse` #{reverse("This was reversed")} + `invisible` #{invisible("This is invisible")} (you can't see it, right?) + `black` #{black("This is black")} + `red` #{red("This is red")} + `green` #{green("This is green")} + `yellow` #{yellow("This is yellow")} + `blue` #{blue("This is blue")} + `magenta` #{magenta("This is magenta")} + `cyan` #{cyan("This is cyan")} + `white` #{white("This is white")} + `on_black` #{on_black("This is black")} + `on_red` #{on_red("This is red")} + `on_green` #{on_green("This is green")} + `on_yellow` #{on_yellow("This is yellow")} + `on_blue` #{on_blue("This is blue")} + `on_magenta` #{on_magenta("This is magenta")} + `on_cyan` #{on_cyan("This is cyan")} + `on_white` #{on_white("This is white")} + `bold`+`red`: #{bold(red("This is bold red"))} + `bold`+`on_green`: #{bold(on_green("This is bold on green"))} + `bold`+`red`+`on_green`: #{bold(red(on_green("This is bold red on green")))} + DEMO + puts demo + end + # rubocop:enable Metrics/AbcSize + end + + register "print", Print +end + +Dry.CLI(StylesDemo).call diff --git a/spec/unit/dry/cli/colors_spec.rb b/spec/unit/dry/cli/colors_spec.rb deleted file mode 100644 index 7b6f4335..00000000 --- a/spec/unit/dry/cli/colors_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "Colors" do - module Dummy - module_function - - include Dry::CLI::Colors - - def only_bold - bold("one") - end - - def bold_and_black - black(bold("two")) - end - end - - it "doesn't duplicate clean escape sequences" do - only_bold = Dummy.only_bold - expect(only_bold.end_with?("\e[0m")).to eq(true) - expect(only_bold.end_with?("\e[0m\e[0m")).to eq(false) - bold_and_black = Dummy.bold_and_black - expect(bold_and_black.end_with?("\e[0m")).to eq(true) - expect(bold_and_black.end_with?("\e[0m\e[0m")).to eq(false) - end -end diff --git a/spec/unit/dry/cli/styles_spec.rb b/spec/unit/dry/cli/styles_spec.rb new file mode 100644 index 00000000..7cff1d97 --- /dev/null +++ b/spec/unit/dry/cli/styles_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe "Styles" do + class Dummy + include Dry::CLI::Styles + + def bold_and_black + black(bold("two")) + end + end + + it "doesn't duplicate clean escape sequences" do + bold_and_black = Dummy.new.bold_and_black + expect(bold_and_black.end_with?("\e[0m")).to eq(true) + expect(bold_and_black.end_with?("\e[0m\e[0m")).to eq(false) + end +end From 179527ef64b7ed1ca8bc1f93d3d77532c8381ef1 Mon Sep 17 00:00:00 2001 From: Gustavo Ribeiro Date: Sun, 8 Dec 2024 09:45:18 -0300 Subject: [PATCH 4/4] Apply code review suggestions - Changed the API to `stylize("string").bold.blue.on_white` - Moved magic numbers to constants - Improve code reuse and legibility - Change `StyledText` to be immutable --- docsite/source/styling-your-output.html.md | 52 ++-- lib/dry/cli/styles.rb | 303 ++++++++++++--------- spec/integration/rendering_spec.rb | 34 +++ spec/support/fixtures/foo | 2 +- spec/support/fixtures/styles | 52 ++-- spec/unit/dry/cli/styles_spec.rb | 17 -- 6 files changed, 268 insertions(+), 192 deletions(-) delete mode 100644 spec/unit/dry/cli/styles_spec.rb diff --git a/docsite/source/styling-your-output.html.md b/docsite/source/styling-your-output.html.md index 931657e2..bcec9dc1 100644 --- a/docsite/source/styling-your-output.html.md +++ b/docsite/source/styling-your-output.html.md @@ -20,32 +20,32 @@ module StylesDemo # rubocop:disable Metrics/AbcSize def call demo = <<~DEMO - `bold` #{bold("This is bold")} - `dim` #{dim("This is dim")} - `italic` #{italic("This is italic")} - `underline` #{underline("This is underline")} - `blink` #{blink("This blinks")} - `reverse` #{reverse("This was reversed")} - `invisible` #{invisible("This is invisible")} (you can't see it, right?) - `black` #{black("This is black")} - `red` #{red("This is red")} - `green` #{green("This is green")} - `yellow` #{yellow("This is yellow")} - `blue` #{blue("This is blue")} - `magenta` #{magenta("This is magenta")} - `cyan` #{cyan("This is cyan")} - `white` #{white("This is white")} - `on_black` #{on_black("This is black")} - `on_red` #{on_red("This is red")} - `on_green` #{on_green("This is green")} - `on_yellow` #{on_yellow("This is yellow")} - `on_blue` #{on_blue("This is blue")} - `on_magenta` #{on_magenta("This is magenta")} - `on_cyan` #{on_cyan("This is cyan")} - `on_white` #{on_white("This is white")} - `bold`+`red`: #{bold(red("This is bold red"))} - `bold`+`on_green`: #{bold(on_green("This is bold on green"))} - `bold`+`red`+`on_green`: #{bold(red(on_green("This is bold red on green")))} + `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 diff --git a/lib/dry/cli/styles.rb b/lib/dry/cli/styles.rb index 4df7fa39..3917958e 100644 --- a/lib/dry/cli/styles.rb +++ b/lib/dry/cli/styles.rb @@ -2,134 +2,193 @@ module Dry class CLI - # Collection of functions to style text. + # Collection of functions to style text # # @since 1.3.0 module Styles - # since 1.3.0 - def bold(text) - ensure_clean_sequence("\e[1m#{text}") - end - - # since 1.3.0 - def dim(text) - ensure_clean_sequence("\e[2m#{text}") - end - - # since 1.3.0 - def italic(text) - ensure_clean_sequence("\e[3m#{text}") - end - - # since 1.3.0 - def underline(text) - ensure_clean_sequence("\e[4m#{text}") - end - - # since 1.3.0 - def blink(text) - ensure_clean_sequence("\e[5m#{text}") - end - - # since 1.3.0 - def reverse(text) - ensure_clean_sequence("\e[7m#{text}") - end - - # since 1.3.0 - def invisible(text) - ensure_clean_sequence("\e[8m#{text}") - end - - # since 1.3.0 - def black(text) - ensure_clean_sequence("\e[30m#{text}") - end - - # since 1.3.0 - def red(text) - ensure_clean_sequence("\e[31m#{text}") - end - - # since 1.3.0 - def green(text) - ensure_clean_sequence("\e[32m#{text}") - end - - # since 1.3.0 - def yellow(text) - ensure_clean_sequence("\e[33m#{text}") - end - - # since 1.3.0 - def blue(text) - ensure_clean_sequence("\e[34m#{text}") - end - - # since 1.3.0 - def magenta(text) - ensure_clean_sequence("\e[35m#{text}") - end - - # since 1.3.0 - def cyan(text) - ensure_clean_sequence("\e[36m#{text}") - end - - # since 1.3.0 - def white(text) - ensure_clean_sequence("\e[37m#{text}") - end - - # since 1.3.0 - def on_black(text) - ensure_clean_sequence("\e[40m#{text}") - end - - # since 1.3.0 - def on_red(text) - ensure_clean_sequence("\e[41m#{text}") - end - - # since 1.3.0 - def on_green(text) - ensure_clean_sequence("\e[42m#{text}") - end - - # since 1.3.0 - def on_yellow(text) - ensure_clean_sequence("\e[43m#{text}") - end - - # since 1.3.0 - def on_blue(text) - ensure_clean_sequence("\e[44m#{text}") - end - - # since 1.3.0 - def on_magenta(text) - ensure_clean_sequence("\e[45m#{text}") - end - - # since 1.3.0 - def on_cyan(text) - ensure_clean_sequence("\e[46m#{text}") - end - - # since 1.3.0 - def on_white(text) - ensure_clean_sequence("\e[47m#{text}") + 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 - private - + # Styled text + # # @since 1.3.0 - # @api private - def ensure_clean_sequence(text) - clen_text = text - clear = "\e[0m" - clen_text += clear unless text.end_with?(clear) - clen_text + 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 diff --git a/spec/integration/rendering_spec.rb b/spec/integration/rendering_spec.rb index 8f15c0a4..3a846d66 100644 --- a/spec/integration/rendering_spec.rb +++ b/spec/integration/rendering_spec.rb @@ -29,4 +29,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 639040ac..867a8508 100755 --- a/spec/support/fixtures/foo +++ b/spec/support/fixtures/foo @@ -35,7 +35,7 @@ module Foo ] def call(engine: nil, **) - puts blue(bold("console - engine: ")), magenta(engine.to_s) + 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 index 638bffaf..b71c0ba8 100755 --- a/spec/support/fixtures/styles +++ b/spec/support/fixtures/styles @@ -13,32 +13,32 @@ module StylesDemo # rubocop:disable Metrics/AbcSize def call demo = <<~DEMO - `bold` #{bold("This is bold")} - `dim` #{dim("This is dim")} - `italic` #{italic("This is italic")} - `underline` #{underline("This is underline")} - `blink` #{blink("This blinks")} - `reverse` #{reverse("This was reversed")} - `invisible` #{invisible("This is invisible")} (you can't see it, right?) - `black` #{black("This is black")} - `red` #{red("This is red")} - `green` #{green("This is green")} - `yellow` #{yellow("This is yellow")} - `blue` #{blue("This is blue")} - `magenta` #{magenta("This is magenta")} - `cyan` #{cyan("This is cyan")} - `white` #{white("This is white")} - `on_black` #{on_black("This is black")} - `on_red` #{on_red("This is red")} - `on_green` #{on_green("This is green")} - `on_yellow` #{on_yellow("This is yellow")} - `on_blue` #{on_blue("This is blue")} - `on_magenta` #{on_magenta("This is magenta")} - `on_cyan` #{on_cyan("This is cyan")} - `on_white` #{on_white("This is white")} - `bold`+`red`: #{bold(red("This is bold red"))} - `bold`+`on_green`: #{bold(on_green("This is bold on green"))} - `bold`+`red`+`on_green`: #{bold(red(on_green("This is bold red on green")))} + `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 diff --git a/spec/unit/dry/cli/styles_spec.rb b/spec/unit/dry/cli/styles_spec.rb deleted file mode 100644 index 7cff1d97..00000000 --- a/spec/unit/dry/cli/styles_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "Styles" do - class Dummy - include Dry::CLI::Styles - - def bold_and_black - black(bold("two")) - end - end - - it "doesn't duplicate clean escape sequences" do - bold_and_black = Dummy.new.bold_and_black - expect(bold_and_black.end_with?("\e[0m")).to eq(true) - expect(bold_and_black.end_with?("\e[0m\e[0m")).to eq(false) - end -end