Class: Toys::Utils::Terminal
- Inherits:
-
Object
- Object
- Toys::Utils::Terminal
- Defined in:
- lib/toys/utils/terminal.rb
Overview
A simple terminal class.
Styles
This class supports ANSI styled output where supported.
Styles may be specified in any of the following forms:
- A symbol indicating the name of a well-known style, or the name of a defined style.
- An rgb string in hex "rgb" or "rrggbb" form.
- An ANSI code string in
\e[XXmform. - An array of ANSI codes as integers.
Defined Under Namespace
Classes: TerminalError
Constant Summary collapse
- CLEAR_CODE =
ANSI style code to clear styles
"\e[0m"- BUILTIN_STYLE_NAMES =
Standard ANSI style codes by name.
{ clear: [0], reset: [0], bold: [1], faint: [2], italic: [3], underline: [4], blink: [5], reverse: [7], black: [30], red: [31], green: [32], yellow: [33], blue: [34], magenta: [35], cyan: [36], white: [37], on_black: [30], on_red: [31], on_green: [32], on_yellow: [33], on_blue: [34], on_magenta: [35], on_cyan: [36], on_white: [37], bright_black: [90], bright_red: [91], bright_green: [92], bright_yellow: [93], bright_blue: [94], bright_magenta: [95], bright_cyan: [96], bright_white: [97], on_bright_black: [100], on_bright_red: [101], on_bright_green: [102], on_bright_yellow: [103], on_bright_blue: [104], on_bright_magenta: [105], on_bright_cyan: [106], on_bright_white: [107], }.freeze
- DEFAULT_SPINNER_FRAME_LENGTH =
Default length of a single spinner frame, in seconds.
0.1- DEFAULT_SPINNER_FRAMES =
Default set of spinner frames.
["-", "\\", "|", "/"].freeze
Instance Attribute Summary collapse
-
#input ⇒ IO?
readonly
Input stream.
-
#output ⇒ IO, ...
readonly
Output stream or logger.
-
#styled ⇒ boolean
readonly
Whether output is styled.
Class Method Summary collapse
-
.infer_styled(output, env: ::ENV) ⇒ boolean
Infers whether styled output should be written to the given output, from the output itself and the environment.
-
.remove_style_escapes(str) ⇒ String
Returns a copy of the given string with all ANSI style codes removed.
Instance Method Summary collapse
-
#<<(str) ⇒ self
Write a line, appending a newline if one is not already present.
-
#apply_styles(str, *styles) ⇒ String
Apply the given styles to the given string, returning the styled string.
-
#ask(prompt, *styles, default: nil, trailing_text: :default) ⇒ String
Ask a question and get a response.
-
#close ⇒ Object
This method is defined so that
::Loggerwill recognize a terminal as a log device target, but it does not actually close anything. -
#confirm(prompt = "Proceed? ", *styles, default: nil) ⇒ boolean
Confirm with the user.
-
#define_style(name, *styles) ⇒ self
Define a named style.
-
#height ⇒ Integer
Return the terminal height.
-
#initialize(input: $stdin, output: $stdout, styled: nil) ⇒ Terminal
constructor
Create a terminal.
-
#newline ⇒ self
Write a newline and flush the current line.
-
#puts(str = "", *styles) ⇒ self
(also: #say)
Write a line, appending a newline if one is not already present.
-
#readline ⇒ String?
Read a line, blocking until one is available.
-
#size ⇒ Array(Integer,Integer)
Return the terminal size as an array of width, height.
-
#spinner(leading_text: "", final_text: "", frame_length: nil, frames: nil, style: nil) ⇒ Object
Display a spinner during a task.
-
#width ⇒ Integer
Return the terminal width.
-
#write(str = "", *styles) ⇒ self
Write a partial line without appending a newline.
Constructor Details
#initialize(input: $stdin, output: $stdout, styled: nil) ⇒ Terminal
Create a terminal.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/toys/utils/terminal.rb', line 162 def initialize(input: $stdin, output: $stdout, styled: nil) require "monitor" @input = input @output = output @styled = if styled.nil? Terminal.infer_styled(output) else styled ? true : false end @named_styles = BUILTIN_STYLE_NAMES.dup @output_mutex = ::Monitor.new @input_mutex = ::Monitor.new end |
Instance Attribute Details
#input ⇒ IO? (readonly)
Input stream
187 188 189 |
# File 'lib/toys/utils/terminal.rb', line 187 def input @input end |
#output ⇒ IO, ... (readonly)
Output stream or logger
181 182 183 |
# File 'lib/toys/utils/terminal.rb', line 181 def output @output end |
#styled ⇒ boolean (readonly)
Whether output is styled
193 194 195 |
# File 'lib/toys/utils/terminal.rb', line 193 def styled @styled end |
Class Method Details
.infer_styled(output, env: ::ENV) ⇒ boolean
Infers whether styled output should be written to the given output, from the output itself and the environment. This is the rule a terminal uses when it is not told explicitly whether to style output, and it is available to tools that write styled output by other means.
The following checks are applied in order, and the first that applies decides the result:
- If the
FORCE_COLORenvironment variable is set to0orfalse(in any case), output is not styled. This follows Node.js and the chalk library, which are the most common consumers ofFORCE_COLOR, rather than the force-color.org standard, which would force styling on for these values. - If the
FORCE_COLORenvironment variable is set to any other nonempty value, output is styled. Following the force-color.org standard, this overridesNO_COLOR, so that color can be forced for one run even whenNO_COLORis set as a general preference. An emptyFORCE_COLORis treated as if it were unset, also following the force-color.org standard. - If the
NO_COLORenvironment variable is set to a nonempty value, output is not styled, following the no-color.org standard. - If the
TERMenvironment variable is set todumb, output is not styled. - Otherwise, output is styled if the output is a tty.
Note that all styling is affected, not only color. Also note that an
explicit choice, such as passing styled: true or styled: false to
#initialize, takes precedence over all of these checks,
because this method is then not called at all.
144 145 146 147 148 149 150 151 |
# File 'lib/toys/utils/terminal.rb', line 144 def self.infer_styled(output, env: ::ENV) force_color = env["FORCE_COLOR"].to_s return false if force_color == "0" || force_color.casecmp?("false") return true unless force_color.empty? return false unless env["NO_COLOR"].to_s.empty? return false if env["TERM"] == "dumb" output.respond_to?(:tty?) && output.tty? end |
.remove_style_escapes(str) ⇒ String
Returns a copy of the given string with all ANSI style codes removed.
103 104 105 |
# File 'lib/toys/utils/terminal.rb', line 103 def self.remove_style_escapes(str) str.to_s.gsub(/\e\[\d+(;\d+)*m/, "") end |
Instance Method Details
#<<(str) ⇒ self
Write a line, appending a newline if one is not already present.
255 256 257 |
# File 'lib/toys/utils/terminal.rb', line 255 def <<(str) puts(str) end |
#apply_styles(str, *styles) ⇒ String
Apply the given styles to the given string, returning the styled string. Honors the styled setting; if styling is disabled, does not add any ANSI style codes and in fact removes any existing codes. If styles were added, ensures that the string ends with a clear code.
418 419 420 421 422 423 424 425 426 |
# File 'lib/toys/utils/terminal.rb', line 418 def apply_styles(str, *styles) if styled prefix = escape_styles(*styles) suffix = prefix.empty? || str.to_s.end_with?(CLEAR_CODE) ? "" : CLEAR_CODE "#{prefix}#{str}#{suffix}" else Terminal.remove_style_escapes(str) end end |
#ask(prompt, *styles, default: nil, trailing_text: :default) ⇒ String
Ask a question and get a response.
279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/toys/utils/terminal.rb', line 279 def ask(prompt, *styles, default: nil, trailing_text: :default) if trailing_text == :default trailing_text = default.nil? ? nil : "[#{default}]" end if trailing_text ptext, pspaces, = prompt.to_s.partition(/\s+$/) prompt = "#{ptext} #{trailing_text}#{pspaces}" end write(prompt, *styles) resp = readline.to_s.chomp resp.empty? ? default.to_s : resp end |
#close ⇒ Object
This method is defined so that ::Logger will recognize a terminal as
a log device target, but it does not actually close anything.
231 232 233 |
# File 'lib/toys/utils/terminal.rb', line 231 def close nil end |
#confirm(prompt = "Proceed? ", *styles, default: nil) ⇒ boolean
Confirm with the user.
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/toys/utils/terminal.rb', line 302 def confirm(prompt = "Proceed? ", *styles, default: nil) default_val, trailing_text = case default when true ["y", "(Y/n)"] when false ["n", "(y/N)"] else [nil, "(y/n)"] end resp = ask(prompt, *styles, default: default_val, trailing_text: trailing_text) return true if resp =~ /^y/i return false if resp =~ /^n/i if resp.nil? && default.nil? raise TerminalError, "Cannot confirm because the input stream is at eof." end if !resp.strip.empty? || default.nil? if input.nil? raise TerminalError, "Cannot confirm because there is no input stream." end confirm('Please answer "y" or "n"', default: default) else default end end |
#define_style(name, *styles) ⇒ self
Define a named style.
Style names must be symbols. The definition of a style may include any valid style specification, including the symbol names of existing defined styles.
403 404 405 406 |
# File 'lib/toys/utils/terminal.rb', line 403 def define_style(name, *styles) @named_styles[name] = resolve_styles(*styles) self end |
#height ⇒ Integer
Return the terminal height
388 389 390 |
# File 'lib/toys/utils/terminal.rb', line 388 def height size[1] end |
#newline ⇒ self
Write a newline and flush the current line.
263 264 265 |
# File 'lib/toys/utils/terminal.rb', line 263 def newline puts end |
#puts(str = "", *styles) ⇒ self Also known as: say
Write a line, appending a newline if one is not already present.
243 244 245 246 |
# File 'lib/toys/utils/terminal.rb', line 243 def puts(str = "", *styles) str = "#{str}\n" unless str.to_s.end_with?("\n") write(str, *styles) end |
#readline ⇒ String?
Read a line, blocking until one is available.
219 220 221 222 223 224 225 |
# File 'lib/toys/utils/terminal.rb', line 219 def readline @input_mutex.synchronize do input&.gets rescue ::IOError nil end end |
#size ⇒ Array(Integer,Integer)
Return the terminal size as an array of width, height.
366 367 368 369 370 371 372 |
# File 'lib/toys/utils/terminal.rb', line 366 def size if output.respond_to?(:tty?) && output.tty? && output.respond_to?(:winsize) output.winsize.reverse else [80, 25] end end |
#spinner(leading_text: "", final_text: "", frame_length: nil, frames: nil, style: nil) ⇒ Object
Display a spinner during a task. You should provide a block that performs the long-running task. While the block is executing, a spinner will be displayed.
346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/toys/utils/terminal.rb', line 346 def spinner(leading_text: "", final_text: "", frame_length: nil, frames: nil, style: nil) return nil unless block_given? frame_length ||= DEFAULT_SPINNER_FRAME_LENGTH frames ||= DEFAULT_SPINNER_FRAMES write(leading_text) unless leading_text.to_s.empty? spin = SpinDriver.new(self, frames, Array(style), frame_length) begin yield ensure spin.stop write(final_text) unless final_text.to_s.empty? end end |
#width ⇒ Integer
Return the terminal width
379 380 381 |
# File 'lib/toys/utils/terminal.rb', line 379 def width size[0] end |
#write(str = "", *styles) ⇒ self
Write a partial line without appending a newline.
203 204 205 206 207 208 209 210 211 |
# File 'lib/toys/utils/terminal.rb', line 203 def write(str = "", *styles) @output_mutex.synchronize do output&.write(apply_styles(str.to_s, *styles)) output&.flush rescue ::IOError nil end self end |