class Toys::Utils::Terminal

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.

Constants

BUILTIN_STYLE_NAMES

Standard ANSI style codes by name. @return [Hash{Symbol => Array<Integer>}]

CLEAR_CODE

ANSI style code to clear styles @return [String]

DEFAULT_SPINNER_FRAMES

Default set of spinner frames. @return [Array<String>]

DEFAULT_SPINNER_FRAME_LENGTH

Default length of a single spinner frame, in seconds. @return [Float]

Attributes

input[R]

Input stream @return [IO,nil]

output[R]

Output stream or logger @return [IO,Logger,nil]

styled[R]

Whether output is styled @return [Boolean]

Public Class Methods

new(input: $stdin, output: $stdout, styled: nil) click to toggle source

Create a terminal.

@param input [IO,nil] Input stream. @param output [IO,Logger,nil] Output stream or logger. @param styled [Boolean,nil] Whether to output ansi styles. If `nil`, the

setting is inferred from whether the output has a tty.
# File lib/toys/utils/terminal.rb, line 118
def initialize(input: $stdin, output: $stdout, styled: nil)
  @input = input
  @output = output
  @styled =
    if styled.nil?
      output.respond_to?(:tty?) && output.tty?
    else
      styled ? true : false
    end
  @named_styles = BUILTIN_STYLE_NAMES.dup
  @output_mutex = ::Monitor.new
  @input_mutex = ::Monitor.new
end
remove_style_escapes(str) click to toggle source

Returns a copy of the given string with all ANSI style codes removed.

@param str [String] Input string @return [String] String with styles removed

# File lib/toys/utils/terminal.rb, line 106
def self.remove_style_escapes(str)
  str.gsub(/\e\[\d+(;\d+)*m/, "")
end

Public Instance Methods

<<(str) click to toggle source

Write a line, appending a newline if one is not already present.

@param str [String] The line to write @return [self]

# File lib/toys/utils/terminal.rb, line 214
def <<(str)
  puts(str)
end
apply_styles(str, *styles) click to toggle source

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.

@param str [String] String to style @param styles [Symbol,String,Array<Integer>…] Styles to apply @return [String] The styled string

# File lib/toys/utils/terminal.rb, line 377
def apply_styles(str, *styles)
  if styled
    prefix = escape_styles(*styles)
    suffix = prefix.empty? || str.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) click to toggle source

Ask a question and get a response.

@param prompt [String] Required prompt string. @param styles [Symbol,String,Array<Integer>…] Styles to apply to the

prompt.

@param default [String,nil] Default value, or `nil` for no default.

Uses `nil` if not specified.

@param trailing_text [:default,String,nil] Trailing text appended to

the prompt, `nil` for none, or `:default` to show the default.

@return [String]

# File lib/toys/utils/terminal.rb, line 238
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.partition(/\s+$/)
    prompt = "#{ptext} #{trailing_text}#{pspaces}"
  end
  write(prompt, *styles)
  resp = readline.to_s.chomp
  resp.empty? ? default.to_s : resp
end
close() click to toggle source

This method is defined so that `::Logger` will recognize a terminal as a log device target, but it does not actually close anything.

# File lib/toys/utils/terminal.rb, line 190
def close
  nil
end
confirm(prompt = "Proceed? ", *styles, default: nil) click to toggle source

Confirm with the user.

@param prompt [String] Prompt string. Defaults to `“Proceed?”`. @param styles [Symbol,String,Array<Integer>…] Styles to apply to the

prompt.

@param default [Boolean,nil] Default value, or `nil` for no default.

Uses `nil` if not specified.

@return [Boolean]

# File lib/toys/utils/terminal.rb, line 261
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) click to toggle source

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.

@param name [Symbol] The name for the style @param styles [Symbol,String,Array<Integer>…] @return [self]

# File lib/toys/utils/terminal.rb, line 362
def define_style(name, *styles)
  @named_styles[name] = resolve_styles(*styles)
  self
end
height() click to toggle source

Return the terminal height

@return [Integer]

# File lib/toys/utils/terminal.rb, line 347
def height
  size[1]
end
newline() click to toggle source

Write a newline and flush the current line. @return [self]

# File lib/toys/utils/terminal.rb, line 222
def newline
  puts
end
puts(str = "", *styles) click to toggle source

Write a line, appending a newline if one is not already present.

@param str [String] The line to write @param styles [Symbol,String,Array<Integer>…] Styles to apply to the

entire line.

@return [self]

# File lib/toys/utils/terminal.rb, line 202
def puts(str = "", *styles)
  str = "#{str}\n" unless str.end_with?("\n")
  write(str, *styles)
end
Also aliased as: say
readline() click to toggle source

Read a line, blocking until one is available.

@return [String] the entire string including the temrinating newline @return [nil] if the input is closed or at eof, or there is no input

# File lib/toys/utils/terminal.rb, line 176
def readline
  @input_mutex.synchronize do
    begin
      input&.gets
    rescue ::IOError
      nil
    end
  end
end
say(str = "", *styles)
Alias for: puts
size() click to toggle source

Return the terminal size as an array of width, height.

@return [Array(Integer,Integer)]

# File lib/toys/utils/terminal.rb, line 325
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) { || ... } click to toggle source

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.

@param leading_text [String] Optional leading string to display to the

left of the spinner. Default is the empty string.

@param frame_length [Float] Length of a single frame, in seconds.

Defaults to {DEFAULT_SPINNER_FRAME_LENGTH}.

@param frames [Array<String>] An array of frames. Defaults to

{DEFAULT_SPINNER_FRAMES}.

@param style [Symbol,Array<Symbol>] A terminal style or array of styles

to apply to all frames in the spinner. Defaults to empty,

@param final_text [String] Optional final string to display when the

spinner is complete. Default is the empty string. A common practice
is to set this to newline.

@return [Object] The return value of the block.

# File lib/toys/utils/terminal.rb, line 305
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.empty?
  spin = SpinDriver.new(self, frames, Array(style), frame_length)
  begin
    yield
  ensure
    spin.stop
    write(final_text) unless final_text.empty?
  end
end
width() click to toggle source

Return the terminal width

@return [Integer]

# File lib/toys/utils/terminal.rb, line 338
def width
  size[0]
end
write(str = "", *styles) click to toggle source

Write a partial line without appending a newline.

@param str [String] The line to write @param styles [Symbol,String,Array<Integer>…] Styles to apply to the

partial line.

@return [self]

# File lib/toys/utils/terminal.rb, line 158
def write(str = "", *styles)
  @output_mutex.synchronize do
    begin
      output&.write(apply_styles(str, *styles))
      output&.flush
    rescue ::IOError
      nil
    end
  end
  self
end

Private Instance Methods

escape_styles(*styles) click to toggle source

Resolve a style to an ANSI style escape sequence.

# File lib/toys/utils/terminal.rb, line 392
def escape_styles(*styles)
  codes = resolve_styles(*styles)
  codes.empty? ? "" : "\e[#{codes.join(';')}m"
end
interpret_style_string(style) click to toggle source

Transform various style string formats into a list of style codes.

# File lib/toys/utils/terminal.rb, line 421
def interpret_style_string(style)
  case style
  when /^[0-9a-fA-F]{6}$/
    rgb = style.to_i(16)
    [38, 2, rgb >> 16, (rgb & 0xff00) >> 8, rgb & 0xff]
  when /^[0-9a-fA-F]{3}$/
    rgb = style.to_i(16)
    [38, 2, (rgb >> 8) * 0x11, ((rgb & 0xf0) >> 4) * 0x11, (rgb & 0xf) * 0x11]
  when /^\e\[([\d;]+)m$/
    ::Regexp.last_match(1).split(";").map(&:to_i)
  end
end
resolve_styles(*styles) click to toggle source

Resolve a style to an array of ANSI style codes (integers).

# File lib/toys/utils/terminal.rb, line 400
def resolve_styles(*styles)
  result = []
  styles.each do |style|
    codes =
      case style
      when ::Array
        style
      when ::String
        interpret_style_string(style)
      when ::Symbol
        @named_styles[style]
      end
    raise ::ArgumentError, "Unknown style code: #{s.inspect}" unless codes
    result.concat(codes)
  end
  result
end