console: only color outputs if stdout isa TTY
[nit.git] / lib / console.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Defines some ANSI Terminal Control Escape Sequences.
16 #
17 # The color methods (e.g. `Text::green`) format the text to appear colored
18 # in a ANSI/VT100 terminal. By default, this coloring is skipped if stdout
19 # is not a TTY, but it can be forced by setting `force_console_colors = true`.
20 module console
21
22 # A ANSI/VT100 escape sequence.
23 abstract class TermEscape
24 # The US-ASCII ESC character.
25 protected fun esc: Char do return 27.code_point
26
27 # The Control Sequence Introducer (CSI).
28 protected fun csi: String do return "{esc}["
29 end
30
31 # Abstract class of the ANSI/VT100 escape sequences for directional moves.
32 abstract class TermDirectionalMove
33 super TermEscape
34
35 # The length of the move.
36 var magnitude: Int = 1 is protected writable
37
38 redef fun to_s do
39 if magnitude == 1 then return "{csi}{code}"
40 return "{csi}{magnitude}{code}"
41 end
42
43 # The code of the command.
44 protected fun code: String is abstract
45 end
46
47 # ANSI/VT100 code to move the cursor up by `magnitude` rows (CUU).
48 class TermMoveUp
49 super TermDirectionalMove
50
51 # Move by the specified number of cells.
52 init by(magnitude: Int) do self.magnitude = magnitude
53
54 redef fun code do return "A"
55 end
56
57 # ANSI/VT100 code to move the cursor down by `magnitude` rows (CUD).
58 class TermMoveDown
59 super TermDirectionalMove
60
61 # Move by the specified number of cells.
62 init by(magnitude: Int) do self.magnitude = magnitude
63
64 redef fun code do return "B"
65 end
66
67 # ANSI/VT100 code to move the cursor forward by `magnitude` columns (CUF).
68 class TermMoveFoward
69 super TermDirectionalMove
70
71 # Move by the specified number of cells.
72 init by(magnitude: Int) do self.magnitude = magnitude
73
74 redef fun code do return "C"
75 end
76
77 # ANSI/VT100 code to move the cursor backward by `magnitude` columns (CUB).
78 class TermMoveBackward
79 super TermDirectionalMove
80
81 # Move by the specified number of cells.
82 init by(magnitude: Int) do self.magnitude = magnitude
83
84 redef fun code do return "D"
85 end
86
87 # ANSI/VT100 code to move the cursor at the specified position (CUP).
88 class TermMove
89 super TermEscape
90
91 # Vertical position.
92 #
93 # 1 is the top.
94 var row: Int = 1
95
96 # Horizontal position.
97 #
98 # 1 is the left.
99 var column: Int = 1
100
101 # Move at the specified position.
102 #
103 # (1, 1) is the top-left corner of the display.
104 init at(row: Int, column: Int) do
105 self.row = row
106 self.column = column
107 end
108
109 redef fun to_s do
110 if row == 1 then
111 if column == 1 then return "{csi}H"
112 return "{csi};{column}H"
113 else
114 if column == 1 then return "{csi}{row}H"
115 return "{csi}{row};{column}H"
116 end
117 end
118 end
119
120 # ANSI/VT100 code to clear from the cursor to the end of the screen (ED 0).
121 class TermEraseDisplayDown
122 super TermEscape
123 redef fun to_s do return "{csi}J"
124 end
125
126 # ANSI/VT100 code to clear from the cursor to the beginning of the screen (ED 1).
127 class TermEraseDisplayUp
128 super TermEscape
129 redef fun to_s do return "{csi}1J"
130 end
131
132 # ANSI/VT100 code to clear the entire display and move the cursor to the top left of screen (ED 2).
133 #
134 # Note: Some terminals always move the cursor when the screen is cleared. So we
135 # force this behaviour to ensure interoperability of the code.
136 class TermClearDisplay
137 super TermEscape
138 redef fun to_s do return "{csi}2J{csi}H"
139 end
140
141 # ANSI/VT100 code to erase anything after the cursor in the line (EL 0).
142 class TermEraseLineFoward
143 super TermEscape
144 redef fun to_s do return "{csi}K"
145 end
146
147 # ANSI/VT100 code to erase anything before the cursor in the line (EL 1).
148 class TermEraseLineBackward
149 super TermEscape
150 redef fun to_s do return "{csi}1K"
151 end
152
153 # ANSI/VT100 code to clear everything in the current line (EL 2).
154 class TermClearLine
155 super TermEscape
156 redef fun to_s do return "{csi}2K"
157 end
158
159 # ANSI/VT100 code to save the current cursor position (SCP).
160 class TermSaveCursor
161 super TermEscape
162 redef fun to_s do return "{csi}s"
163 end
164
165 # ANSI/VT100 code to restore the current cursor position (RCP).
166 class TermRestoreCursor
167 super TermEscape
168 redef fun to_s do return "{csi}u"
169 end
170
171 # ANSI/VT100 code to change character look (SGR).
172 #
173 # By default, resets everything to the terminal’s defaults.
174 #
175 # Note:
176 #
177 # The escape sequence inserted at the end of the string by terminal-related
178 # methods of `String` resets all character attributes to the terminal’s
179 # defaults. So, when combining format `a` and `b`, something like
180 # `("foo".a + " bar").b` will not work as expected, but `"foo".a.b + " bar".b`
181 # will. You may also use `TermCharFormat` (this class).
182 #
183 # Usage example:
184 #
185 # print "{(new TermCharFormat).yellow_fg.bold}a{(new TermCharFormat).yellow_fg}b{new TermCharFormat}"
186 class TermCharFormat
187 super TermEscape
188
189 private var attributes: Array[String] = new Array[String]
190
191 # Copies the attributes from the specified format.
192 init from(format: TermCharFormat) do
193 attributes.add_all(format.attributes)
194 end
195
196 redef fun to_s do return "{csi}{attributes.join(";")}m"
197
198 # Apply the specified SGR and return `self`.
199 private fun apply(sgr: String): TermCharFormat do
200 attributes.add(sgr)
201 return self
202 end
203
204 # Apply normal (default) format and return `self`.
205 fun default: TermCharFormat do return apply("0")
206
207 # Apply bold weight and return `self`.
208 fun bold: TermCharFormat do return apply("1")
209
210 # Apply underlining and return `self`.
211 fun underline: TermCharFormat do return apply("4")
212
213 # Apply blinking or bold weight and return `self`.
214 fun blink: TermCharFormat do return apply("5")
215
216 # Apply reverse video and return `self`.
217 fun inverse: TermCharFormat do return apply("7")
218
219 # Apply normal weight and return `self`.
220 fun normal_weight: TermCharFormat do return apply("22")
221
222 # Add the attribute that disable underlining and return `self`.
223 fun not_underlined: TermCharFormat do return apply("24")
224
225 # Add the attribute that disable blinking and return `self`.
226 fun steady: TermCharFormat do return apply("25")
227
228 # Add the attribute that disable reverse video and return `self`.
229 fun positive: TermCharFormat do return apply("27")
230
231 # Apply a black foreground and return `self`.
232 fun black_fg: TermCharFormat do return apply("30")
233
234 # Apply a red foreground and return `self`.
235 fun red_fg: TermCharFormat do return apply("31")
236
237 # Apply a green foreground and return `self`.
238 fun green_fg: TermCharFormat do return apply("32")
239
240 # Apply a yellow foreground and return `self`.
241 fun yellow_fg: TermCharFormat do return apply("33")
242
243 # Apply a blue foreground and return `self`.
244 fun blue_fg: TermCharFormat do return apply("34")
245
246 # Apply a magenta foreground and return `self`.
247 fun magenta_fg: TermCharFormat do return apply("35")
248
249 # Apply a cyan foreground and return `self`.
250 fun cyan_fg: TermCharFormat do return apply("36")
251
252 # Apply a white foreground and return `self`.
253 fun white_fg: TermCharFormat do return apply("37")
254
255 # Apply the default foreground and return `self`.
256 fun default_fg: TermCharFormat do return apply("39")
257
258 # Apply a black background and return `self`.
259 fun black_bg: TermCharFormat do return apply("40")
260
261 # Apply a red background and return `self`.
262 fun red_bg: TermCharFormat do return apply("41")
263
264 # Apply a green background and return `self`.
265 fun green_bg: TermCharFormat do return apply("42")
266
267 # Apply a yellow background and return `self`.
268 fun yellow_bg: TermCharFormat do return apply("43")
269
270 # Apply a blue background and return `self`.
271 fun blue_bg: TermCharFormat do return apply("44")
272
273 # Apply a magenta background and return `self`.
274 fun magenta_bg: TermCharFormat do return apply("45")
275
276 # Apply a cyan background and return `self`.
277 fun cyan_bg: TermCharFormat do return apply("46")
278
279 # Apply a white background and return `self`.
280 fun white_bg: TermCharFormat do return apply("47")
281
282 # Apply the default background and return `self`.
283 fun default_bg: TermCharFormat do return apply("49")
284 end
285
286 # Services to color terminal output
287 redef class Text
288 private fun apply_format(f: TermCharFormat): String do
289 if stdout_isatty or force_console_colors then
290 return "{f}{self}{normal}"
291 else return to_s
292 end
293
294 private fun normal: TermCharFormat do return new TermCharFormat
295
296 # Make the text appear in dark gray (or black) in a ANSI/VT100 terminal.
297 #
298 # SEE: `TermCharFormat`
299 fun gray: String do return apply_format(normal.black_fg)
300
301 # Make the text appear in red in a ANSI/VT100 terminal.
302 #
303 # SEE: `TermCharFormat`
304 fun red: String do return apply_format(normal.red_fg)
305
306 # Make the text appear in green in a ANSI/VT100 terminal.
307 #
308 # SEE: `TermCharFormat`
309 fun green: String do return apply_format(normal.green_fg)
310
311 # Make the text appear in yellow in a ANSI/VT100 terminal.
312 #
313 # SEE: `TermCharFormat`
314 fun yellow: String do return apply_format(normal.yellow_fg)
315
316 # Make the text appear in blue in a ANSI/VT100 terminal.
317 #
318 # SEE: `TermCharFormat`
319 fun blue: String do return apply_format(normal.blue_fg)
320
321 # Make the text appear in magenta in a ANSI/VT100 terminal.
322 #
323 # SEE: `TermCharFormat`
324 fun purple: String do return apply_format(normal.magenta_fg)
325
326 # Make the text appear in cyan in a ANSI/VT100 terminal.
327 #
328 # SEE: `TermCharFormat`
329 fun cyan: String do return apply_format(normal.cyan_fg)
330
331 # Make the text appear in light gray (or white) in a ANSI/VT100 terminal.
332 #
333 # SEE: `TermCharFormat`
334 fun light_gray: String do return apply_format(normal.white_fg)
335
336 # Make the text appear in bold in a ANSI/VT100 terminal.
337 #
338 # SEE: `TermCharFormat`
339 fun bold: String do return apply_format(normal.bold)
340
341 # Make the text underlined in a ANSI/VT100 terminal.
342 #
343 # SEE: `TermCharFormat`
344 fun underline: String do return apply_format(normal.underline)
345 end
346
347 # A dynamic progress bar displayable in console.
348 #
349 # Example:
350 # ~~~nitish
351 # var max = 10
352 # var current = 0
353 # var pb = new TermProgress(max, current)
354 #
355 # pb.display
356 # for i in [current + 1 .. max] do
357 # nanosleep(1, 0)
358 # pb.update(i)
359 # end
360 #
361 # print "\ndone"
362 # ~~~
363 #
364 # Progress bar can accept metadata to display a small amount of data.
365 #
366 # Example with metadata:
367 # ~~~nitish
368 # var pb = new TermProgress(10, 0)
369 # for i in [0..10] do
370 # pb.update(i, "Step {i}")
371 # end
372 # ~~~
373 class TermProgress
374
375 # Max value of the progress bar (business value).
376 var max_value: Int
377
378 # Current value of the progress bar (business value).
379 var current_value: Int
380
381 # Number of columns used to display the progress bar.
382 var max_columns = 70 is writable
383
384 # Get the current percent value.
385 fun current_percentage: Int do
386 return current_value * 100 / max_value
387 end
388
389 # Display the progress bar.
390 #
391 # `metadata` can be used to pass a small amount of data to display after
392 # the progress bar.
393 fun display(metadata: nullable String) do
394 var percent = current_percentage
395 var p = current_value * max_columns / max_value
396 printn "\r{percent}% ["
397 for i in [1..max_columns] do
398 if i < p then
399 printn "="
400 else if i == p then
401 printn ">"
402 else
403 printn " "
404 end
405 end
406 printn "]"
407 if metadata != null then printn " ({metadata})"
408 end
409
410 # Update and display the progress bar.
411 #
412 # See `display`.
413 fun update(new_current: Int, metadata: nullable String) do
414 current_value = new_current
415 display(metadata)
416 end
417 end
418
419 redef class Sys
420 private var stdout_isatty: Bool = 1.isatty is lazy
421
422 # Force coloring terminal output, even if stdout is not a TTY?
423 #
424 # Defaults to `false`.
425 var force_console_colors = false is writable
426 end