Class: Toys::Utils::Terminal

Inherits:
Object
  • Object
show all
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[XXm form.
  • An array of ANSI codes as integers.

Defined Under Namespace

Classes: TerminalError

Constant Summary collapse

CLEAR_CODE =

ANSI style code to clear styles

Returns:

  • (String)
"\e[0m"
BUILTIN_STYLE_NAMES =

Standard ANSI style codes by name.

Returns:

  • (Hash{Symbol => Array<Integer>})
{
  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.

Returns:

  • (Float)
0.1
DEFAULT_SPINNER_FRAMES =

Default set of spinner frames.

Returns:

  • (Array<String>)
["-", "\\", "|", "/"].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin, output: $stdout, styled: nil) ⇒ Terminal

Create a terminal.

Parameters:

  • input (IO, nil) (defaults to: $stdin) —

    Input stream.

  • output (IO, Logger, nil) (defaults to: $stdout) —

    Output stream or logger.

  • styled (boolean, nil) (defaults to: nil) —

    Whether to output ansi styles. If nil, the setting is inferred from the output and the environment. See infer_styled.



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

Returns:

  • (IO, nil)


187
188
189
# File 'lib/toys/utils/terminal.rb', line 187

def input
  @input
end

#output ⇒ IO, ... (readonly)

Output stream or logger

Returns:

  • (IO, Logger, nil)


181
182
183
# File 'lib/toys/utils/terminal.rb', line 181

def output
  @output
end

#styled ⇒ boolean (readonly)

Whether output is styled

Returns:

  • (boolean)


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:

  1. If the FORCE_COLOR environment variable is set to 0 or false (in any case), output is not styled. This follows Node.js and the chalk library, which are the most common consumers of FORCE_COLOR, rather than the force-color.org standard, which would force styling on for these values.
  2. If the FORCE_COLOR environment variable is set to any other nonempty value, output is styled. Following the force-color.org standard, this overrides NO_COLOR, so that color can be forced for one run even when NO_COLOR is set as a general preference. An empty FORCE_COLOR is treated as if it were unset, also following the force-color.org standard.
  3. If the NO_COLOR environment variable is set to a nonempty value, output is not styled, following the no-color.org standard.
  4. If the TERM environment variable is set to dumb, output is not styled.
  5. 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.

Parameters:

  • output (IO, Logger, nil) —

    The output stream.

  • env (Hash{String=>String}) (defaults to: ::ENV) —

    The environment. Defaults to ENV.

Returns:

  • (boolean) —

    Whether output should be styled.



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.

Parameters:

  • str (String) —

    Input string

Returns:

  • (String) —

    String with styles 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.

Parameters:

  • str (String) —

    The line to write

Returns:

  • (self)


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.

Parameters:

  • str (String) —

    String to style

  • styles (Symbol, String, Array<Integer>...) —

    Styles to apply

Returns:

  • (String) —

    The styled string



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.

Parameters:

  • prompt (String) —

    Required prompt string.

  • styles (Symbol, String, Array<Integer>...) —

    Styles to apply to the prompt.

  • default (String, nil) (defaults to: nil) —

    Default value, or nil for no default. Uses nil if not specified.

  • trailing_text (:default, String, nil) (defaults to: :default) —

    Trailing text appended to the prompt, nil for none, or :default to show the default.

Returns:

  • (String)


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.

Parameters:

  • prompt (String) (defaults to: "Proceed? ") —

    Prompt string. Defaults to "Proceed?".

  • styles (Symbol, String, Array<Integer>...) —

    Styles to apply to the prompt.

  • default (boolean, nil) (defaults to: nil) —

    Default value, or nil for no default. Uses nil if not specified.

Returns:

  • (boolean)


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.

Parameters:

  • name (Symbol) —

    The name for the style

  • styles (Symbol, String, Array<Integer>...)

Returns:

  • (self)


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

Returns:

  • (Integer)


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.

Returns:

  • (self)


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.

Parameters:

  • str (String) (defaults to: "") —

    The line to write

  • styles (Symbol, String, Array<Integer>...) —

    Styles to apply to the entire line.

Returns:

  • (self)


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.

Returns:

  • (String) —

    the entire string including the temrinating newline

  • (nil) —

    if the input is closed or at eof, or there is no input



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.

Returns:

  • (Array(Integer,Integer))


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.

Parameters:

  • leading_text (String) (defaults to: "") —

    Optional leading string to display to the left of the spinner. Default is the empty string.

  • frame_length (Float) (defaults to: nil) —

    Length of a single frame, in seconds. Defaults to DEFAULT_SPINNER_FRAME_LENGTH.

  • frames (Array<String>) (defaults to: nil) —

    An array of frames. Defaults to DEFAULT_SPINNER_FRAMES.

  • style (Symbol, Array<Symbol>) (defaults to: nil) —

    A terminal style or array of styles to apply to all frames in the spinner. Defaults to empty,

  • final_text (String) (defaults to: "") —

    Optional final string to display when the spinner is complete. Default is the empty string. A common practice is to set this to newline.

Returns:

  • (Object) —

    The return value of the block.



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

Returns:

  • (Integer)


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.

Parameters:

  • str (String) (defaults to: "") —

    The line to write

  • styles (Symbol, String, Array<Integer>...) —

    Styles to apply to the partial line.

Returns:

  • (self)


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