Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ed66def

Browse files
committedJan 4, 2025
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
1 parent 11cb3fa commit ed66def

File tree

6 files changed

+268
-192
lines changed

6 files changed

+268
-192
lines changed
 

‎docsite/source/styling-your-output.html.md

+26-26
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,32 @@ module StylesDemo
2020
# rubocop:disable Metrics/AbcSize
2121
def call
2222
demo = <<~DEMO
23-
`bold` #{bold("This is bold")}
24-
`dim` #{dim("This is dim")}
25-
`italic` #{italic("This is italic")}
26-
`underline` #{underline("This is underline")}
27-
`blink` #{blink("This blinks")}
28-
`reverse` #{reverse("This was reversed")}
29-
`invisible` #{invisible("This is invisible")} (you can't see it, right?)
30-
`black` #{black("This is black")}
31-
`red` #{red("This is red")}
32-
`green` #{green("This is green")}
33-
`yellow` #{yellow("This is yellow")}
34-
`blue` #{blue("This is blue")}
35-
`magenta` #{magenta("This is magenta")}
36-
`cyan` #{cyan("This is cyan")}
37-
`white` #{white("This is white")}
38-
`on_black` #{on_black("This is black")}
39-
`on_red` #{on_red("This is red")}
40-
`on_green` #{on_green("This is green")}
41-
`on_yellow` #{on_yellow("This is yellow")}
42-
`on_blue` #{on_blue("This is blue")}
43-
`on_magenta` #{on_magenta("This is magenta")}
44-
`on_cyan` #{on_cyan("This is cyan")}
45-
`on_white` #{on_white("This is white")}
46-
`bold`+`red`: #{bold(red("This is bold red"))}
47-
`bold`+`on_green`: #{bold(on_green("This is bold on green"))}
48-
`bold`+`red`+`on_green`: #{bold(red(on_green("This is bold red on green")))}
23+
`stylize("This is bold").bold` #=> #{stylize("This is bold").bold}
24+
`stylize("This is dim").dim` #=> #{stylize("This is dim").dim}
25+
`stylize("This is italic").italic` #=> #{stylize("This is italic").italic}
26+
`stylize("This is underline").underline` #=> #{stylize("This is underline").underline}
27+
`stylize("This blinks").blink` #=> #{stylize("This blinks").blink}
28+
`stylize("This was reversed").reverse` #=> #{stylize("This was reversed").reverse}
29+
`stylize("This is invisible").invisible` #=> #{stylize("This is invisible").invisible} (you can't see it, right?)
30+
`stylize("This is black").black` #=> #{stylize("This is black").black}
31+
`stylize("This is red").red` #=> #{stylize("This is red").red}
32+
`stylize("This is green").green` #=> #{stylize("This is green").green}
33+
`stylize("This is yellow").yellow` #=> #{stylize("This is yellow").yellow}
34+
`stylize("This is blue").blue` #=> #{stylize("This is blue").blue}
35+
`stylize("This is magenta").magenta` #=> #{stylize("This is magenta").magenta}
36+
`stylize("This is cyan").cyan` #=> #{stylize("This is cyan").cyan}
37+
`stylize("This is white").white` #=> #{stylize("This is white").white}
38+
`stylize("This is black").on_black` #=> #{stylize("This is black").on_black}
39+
`stylize("This is red").on_red` #=> #{stylize("This is red").on_red}
40+
`stylize("This is green").on_green` #=> #{stylize("This is green").on_green}
41+
`stylize("This is yellow").on_yellow` #=> #{stylize("This is yellow").on_yellow}
42+
`stylize("This is blue").on_blue` #=> #{stylize("This is blue").on_blue}
43+
`stylize("This is magenta").on_magenta` #=> #{stylize("This is magenta").on_magenta}
44+
`stylize("This is cyan").on_cyan` #=> #{stylize("This is cyan").on_cyan}
45+
`stylize("This is white").on_white` #=> #{stylize("This is white").on_white}
46+
`stylize("This is bold red").bold.red #=> #{stylize("This is bold red").bold.red}
47+
`stylize("This is bold on green").bold.on_green` #=> #{stylize("This is bold on green").bold.on_green}
48+
`stylize("This is bold red on green").bold.red.on_green` #=> #{stylize("This is bold red on green").bold.red.on_green}
4949
DEMO
5050
puts demo
5151
end

‎lib/dry/cli/styles.rb

+181-122
Original file line numberDiff line numberDiff line change
@@ -2,134 +2,193 @@
22

33
module Dry
44
class CLI
5-
# Collection of functions to style text.
5+
# Collection of functions to style text
66
#
77
# @since 1.3.0
88
module Styles
9-
# since 1.3.0
10-
def bold(text)
11-
ensure_clean_sequence("\e[1m#{text}")
12-
end
13-
14-
# since 1.3.0
15-
def dim(text)
16-
ensure_clean_sequence("\e[2m#{text}")
17-
end
18-
19-
# since 1.3.0
20-
def italic(text)
21-
ensure_clean_sequence("\e[3m#{text}")
22-
end
23-
24-
# since 1.3.0
25-
def underline(text)
26-
ensure_clean_sequence("\e[4m#{text}")
27-
end
28-
29-
# since 1.3.0
30-
def blink(text)
31-
ensure_clean_sequence("\e[5m#{text}")
32-
end
33-
34-
# since 1.3.0
35-
def reverse(text)
36-
ensure_clean_sequence("\e[7m#{text}")
37-
end
38-
39-
# since 1.3.0
40-
def invisible(text)
41-
ensure_clean_sequence("\e[8m#{text}")
42-
end
43-
44-
# since 1.3.0
45-
def black(text)
46-
ensure_clean_sequence("\e[30m#{text}")
47-
end
48-
49-
# since 1.3.0
50-
def red(text)
51-
ensure_clean_sequence("\e[31m#{text}")
52-
end
53-
54-
# since 1.3.0
55-
def green(text)
56-
ensure_clean_sequence("\e[32m#{text}")
57-
end
58-
59-
# since 1.3.0
60-
def yellow(text)
61-
ensure_clean_sequence("\e[33m#{text}")
62-
end
63-
64-
# since 1.3.0
65-
def blue(text)
66-
ensure_clean_sequence("\e[34m#{text}")
67-
end
68-
69-
# since 1.3.0
70-
def magenta(text)
71-
ensure_clean_sequence("\e[35m#{text}")
72-
end
73-
74-
# since 1.3.0
75-
def cyan(text)
76-
ensure_clean_sequence("\e[36m#{text}")
77-
end
78-
79-
# since 1.3.0
80-
def white(text)
81-
ensure_clean_sequence("\e[37m#{text}")
82-
end
83-
84-
# since 1.3.0
85-
def on_black(text)
86-
ensure_clean_sequence("\e[40m#{text}")
87-
end
88-
89-
# since 1.3.0
90-
def on_red(text)
91-
ensure_clean_sequence("\e[41m#{text}")
92-
end
93-
94-
# since 1.3.0
95-
def on_green(text)
96-
ensure_clean_sequence("\e[42m#{text}")
97-
end
98-
99-
# since 1.3.0
100-
def on_yellow(text)
101-
ensure_clean_sequence("\e[43m#{text}")
102-
end
103-
104-
# since 1.3.0
105-
def on_blue(text)
106-
ensure_clean_sequence("\e[44m#{text}")
107-
end
108-
109-
# since 1.3.0
110-
def on_magenta(text)
111-
ensure_clean_sequence("\e[45m#{text}")
112-
end
113-
114-
# since 1.3.0
115-
def on_cyan(text)
116-
ensure_clean_sequence("\e[46m#{text}")
117-
end
118-
119-
# since 1.3.0
120-
def on_white(text)
121-
ensure_clean_sequence("\e[47m#{text}")
9+
RESET = 0
10+
BOLD = 1
11+
DIM = 2
12+
ITALIC = 3
13+
UNDERLINE = 4
14+
BLINK = 5
15+
REVERSE = 7
16+
INVISIBLE = 8
17+
BLACK = 30
18+
RED = 31
19+
GREEN = 32
20+
YELLOW = 33
21+
BLUE = 34
22+
MAGENTA = 35
23+
CYAN = 36
24+
WHITE = 37
25+
ON_BLACK = 40
26+
ON_RED = 41
27+
ON_GREEN = 42
28+
ON_YELLOW = 43
29+
ON_BLUE = 44
30+
ON_MAGENTA = 45
31+
ON_CYAN = 46
32+
ON_WHITE = 47
33+
34+
# Returns a text that can be styled
35+
#
36+
# @param text [String] text to be styled
37+
#
38+
# @since 1.3.0
39+
def stylize(text)
40+
StyledText.new(text)
12241
end
12342

124-
private
125-
43+
# Styled text
44+
#
12645
# @since 1.3.0
127-
# @api private
128-
def ensure_clean_sequence(text)
129-
clen_text = text
130-
clear = "\e[0m"
131-
clen_text += clear unless text.end_with?(clear)
132-
clen_text
46+
class StyledText
47+
def initialize(text, escape_code = nil)
48+
@text = text
49+
@escape_code = escape_code
50+
end
51+
52+
# Makes `StyledText` printable
53+
#
54+
# @since 1.3.0
55+
def to_s
56+
text + escape_code
57+
end
58+
59+
# since 1.3.0
60+
def bold
61+
chainable_update!(BOLD, text)
62+
end
63+
64+
# since 1.3.0
65+
def dim
66+
chainable_update!(DIM, text)
67+
end
68+
69+
# since 1.3.0
70+
def italic
71+
chainable_update!(ITALIC, text)
72+
end
73+
74+
# since 1.3.0
75+
def underline
76+
chainable_update!(UNDERLINE, text)
77+
end
78+
79+
# since 1.3.0
80+
def blink
81+
chainable_update!(BLINK, text)
82+
end
83+
84+
# since 1.3.0
85+
def reverse
86+
chainable_update!(REVERSE, text)
87+
end
88+
89+
# since 1.3.0
90+
def invisible
91+
chainable_update!(INVISIBLE, text)
92+
end
93+
94+
# since 1.3.0
95+
def black
96+
chainable_update!(BLACK, text)
97+
end
98+
99+
# since 1.3.0
100+
def red
101+
chainable_update!(RED, text)
102+
end
103+
104+
# since 1.3.0
105+
def green
106+
chainable_update!(GREEN, text)
107+
end
108+
109+
# since 1.3.0
110+
def yellow
111+
chainable_update!(YELLOW, text)
112+
end
113+
114+
# since 1.3.0
115+
def blue
116+
chainable_update!(BLUE, text)
117+
end
118+
119+
# since 1.3.0
120+
def magenta
121+
chainable_update!(MAGENTA, text)
122+
end
123+
124+
# since 1.3.0
125+
def cyan
126+
chainable_update!(CYAN, text)
127+
end
128+
129+
# since 1.3.0
130+
def white
131+
chainable_update!(WHITE, text)
132+
end
133+
134+
# since 1.3.0
135+
def on_black
136+
chainable_update!(ON_BLACK, text)
137+
end
138+
139+
# since 1.3.0
140+
def on_red
141+
chainable_update!(ON_RED, text)
142+
end
143+
144+
# since 1.3.0
145+
def on_green
146+
chainable_update!(ON_GREEN, text)
147+
end
148+
149+
# since 1.3.0
150+
def on_yellow
151+
chainable_update!(ON_YELLOW, text)
152+
end
153+
154+
# since 1.3.0
155+
def on_blue
156+
chainable_update!(ON_BLUE, text)
157+
end
158+
159+
# since 1.3.0
160+
def on_magenta
161+
chainable_update!(ON_MAGENTA, text)
162+
end
163+
164+
# since 1.3.0
165+
def on_cyan
166+
chainable_update!(ON_CYAN, text)
167+
end
168+
169+
# since 1.3.0
170+
def on_white
171+
chainable_update!(ON_WHITE, text)
172+
end
173+
174+
private
175+
176+
attr_reader :text, :escape_code
177+
178+
# @since 1.3.0
179+
# @api private
180+
def chainable_update!(style, new_text)
181+
StyledText.new(
182+
select_graphic_rendition(style) + new_text,
183+
select_graphic_rendition(RESET)
184+
)
185+
end
186+
187+
# @since 1.3.0
188+
# @api private
189+
def select_graphic_rendition(code)
190+
"\e[#{code}m"
191+
end
133192
end
134193
end
135194
end

‎spec/integration/rendering_spec.rb

+34
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,38 @@
2828

2929
expect(stderr).to eq(expected)
3030
end
31+
32+
it "prints styled text" do
33+
stdout, = Open3.capture3("styles print")
34+
35+
expected = <<~OUT
36+
`stylize(\"This is bold\").bold` #=> \e[1mThis is bold\e[0m
37+
`stylize(\"This is dim\").dim` #=> \e[2mThis is dim\e[0m
38+
`stylize(\"This is italic\").italic` #=> \e[3mThis is italic\e[0m
39+
`stylize(\"This is underline\").underline` #=> \e[4mThis is underline\e[0m
40+
`stylize(\"This blinks\").blink` #=> \e[5mThis blinks\e[0m
41+
`stylize(\"This was reversed\").reverse` #=> \e[7mThis was reversed\e[0m
42+
`stylize(\"This is invisible\").invisible` #=> \e[8mThis is invisible\e[0m (you can't see it, right?)
43+
`stylize(\"This is black\").black` #=> \e[30mThis is black\e[0m
44+
`stylize(\"This is red\").red` #=> \e[31mThis is red\e[0m
45+
`stylize(\"This is green\").green` #=> \e[32mThis is green\e[0m
46+
`stylize(\"This is yellow\").yellow` #=> \e[33mThis is yellow\e[0m
47+
`stylize(\"This is blue\").blue` #=> \e[34mThis is blue\e[0m
48+
`stylize(\"This is magenta\").magenta` #=> \e[35mThis is magenta\e[0m
49+
`stylize(\"This is cyan\").cyan` #=> \e[36mThis is cyan\e[0m
50+
`stylize(\"This is white\").white` #=> \e[37mThis is white\e[0m
51+
`stylize(\"This is black\").on_black` #=> \e[40mThis is black\e[0m
52+
`stylize(\"This is red\").on_red` #=> \e[41mThis is red\e[0m
53+
`stylize(\"This is green\").on_green` #=> \e[42mThis is green\e[0m
54+
`stylize(\"This is yellow\").on_yellow` #=> \e[43mThis is yellow\e[0m
55+
`stylize(\"This is blue\").on_blue` #=> \e[44mThis is blue\e[0m
56+
`stylize(\"This is magenta\").on_magenta` #=> \e[45mThis is magenta\e[0m
57+
`stylize(\"This is cyan\").on_cyan` #=> \e[46mThis is cyan\e[0m
58+
`stylize(\"This is white\").on_white` #=> \e[47mThis is white\e[0m
59+
`stylize(\"This is bold red\").bold.red #=> \e[31m\e[1mThis is bold red\e[0m
60+
`stylize(\"This is bold on green\").bold.on_green` #=> \e[42m\e[1mThis is bold on green\e[0m
61+
`stylize(\"This is bold red on green\").bold.red.on_green` #=> \e[42m\e[31m\e[1mThis is bold red on green\e[0m
62+
OUT
63+
expect(stdout).to eq(expected)
64+
end
3165
end

‎spec/support/fixtures/foo

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module Foo
3535
]
3636

3737
def call(engine: nil, **)
38-
puts blue(bold("console - engine: ")), magenta(engine.to_s)
38+
puts stylize("console - engine: ").blue.bold, stylize(engine.to_s).magenta
3939
end
4040
end
4141

‎spec/support/fixtures/styles

+26-26
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,32 @@ module StylesDemo
1313
# rubocop:disable Metrics/AbcSize
1414
def call
1515
demo = <<~DEMO
16-
`bold` #{bold("This is bold")}
17-
`dim` #{dim("This is dim")}
18-
`italic` #{italic("This is italic")}
19-
`underline` #{underline("This is underline")}
20-
`blink` #{blink("This blinks")}
21-
`reverse` #{reverse("This was reversed")}
22-
`invisible` #{invisible("This is invisible")} (you can't see it, right?)
23-
`black` #{black("This is black")}
24-
`red` #{red("This is red")}
25-
`green` #{green("This is green")}
26-
`yellow` #{yellow("This is yellow")}
27-
`blue` #{blue("This is blue")}
28-
`magenta` #{magenta("This is magenta")}
29-
`cyan` #{cyan("This is cyan")}
30-
`white` #{white("This is white")}
31-
`on_black` #{on_black("This is black")}
32-
`on_red` #{on_red("This is red")}
33-
`on_green` #{on_green("This is green")}
34-
`on_yellow` #{on_yellow("This is yellow")}
35-
`on_blue` #{on_blue("This is blue")}
36-
`on_magenta` #{on_magenta("This is magenta")}
37-
`on_cyan` #{on_cyan("This is cyan")}
38-
`on_white` #{on_white("This is white")}
39-
`bold`+`red`: #{bold(red("This is bold red"))}
40-
`bold`+`on_green`: #{bold(on_green("This is bold on green"))}
41-
`bold`+`red`+`on_green`: #{bold(red(on_green("This is bold red on green")))}
16+
`stylize("This is bold").bold` #=> #{stylize("This is bold").bold}
17+
`stylize("This is dim").dim` #=> #{stylize("This is dim").dim}
18+
`stylize("This is italic").italic` #=> #{stylize("This is italic").italic}
19+
`stylize("This is underline").underline` #=> #{stylize("This is underline").underline}
20+
`stylize("This blinks").blink` #=> #{stylize("This blinks").blink}
21+
`stylize("This was reversed").reverse` #=> #{stylize("This was reversed").reverse}
22+
`stylize("This is invisible").invisible` #=> #{stylize("This is invisible").invisible} (you can't see it, right?)
23+
`stylize("This is black").black` #=> #{stylize("This is black").black}
24+
`stylize("This is red").red` #=> #{stylize("This is red").red}
25+
`stylize("This is green").green` #=> #{stylize("This is green").green}
26+
`stylize("This is yellow").yellow` #=> #{stylize("This is yellow").yellow}
27+
`stylize("This is blue").blue` #=> #{stylize("This is blue").blue}
28+
`stylize("This is magenta").magenta` #=> #{stylize("This is magenta").magenta}
29+
`stylize("This is cyan").cyan` #=> #{stylize("This is cyan").cyan}
30+
`stylize("This is white").white` #=> #{stylize("This is white").white}
31+
`stylize("This is black").on_black` #=> #{stylize("This is black").on_black}
32+
`stylize("This is red").on_red` #=> #{stylize("This is red").on_red}
33+
`stylize("This is green").on_green` #=> #{stylize("This is green").on_green}
34+
`stylize("This is yellow").on_yellow` #=> #{stylize("This is yellow").on_yellow}
35+
`stylize("This is blue").on_blue` #=> #{stylize("This is blue").on_blue}
36+
`stylize("This is magenta").on_magenta` #=> #{stylize("This is magenta").on_magenta}
37+
`stylize("This is cyan").on_cyan` #=> #{stylize("This is cyan").on_cyan}
38+
`stylize("This is white").on_white` #=> #{stylize("This is white").on_white}
39+
`stylize("This is bold red").bold.red #=> #{stylize("This is bold red").bold.red}
40+
`stylize("This is bold on green").bold.on_green` #=> #{stylize("This is bold on green").bold.on_green}
41+
`stylize("This is bold red on green").bold.red.on_green` #=> #{stylize("This is bold red on green").bold.red.on_green}
4242
DEMO
4343
puts demo
4444
end

‎spec/unit/dry/cli/styles_spec.rb

-17
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.