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 whether the output has a tty.



118
119
120
121
122
123
124
125
126
127
128
129
130
# 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? && !::ENV["NO_COLOR"]
    else
      styled ? true : false
    end
  @named_styles = BUILTIN_STYLE_NAMES.dup
  @output_mutex = ::Monitor.new
  @input_mutex = ::Monitor.new
end

Instance Attribute Details

#inputIO? (readonly)

Input stream

Returns:

  • (IO, nil)


142
143
144
# File 'lib/toys/utils/terminal.rb', line 142

def input
  @input
end

#outputIO, ... (readonly)

Output stream or logger

Returns:

  • (IO, Logger, nil)


136
137
138
# File 'lib/toys/utils/terminal.rb', line 136

def output
  @output
end

#styledBoolean (readonly)

Whether output is styled

Returns:

  • (Boolean)


148
149
150
# File 'lib/toys/utils/terminal.rb', line 148

def styled
  @styled
end

Class Method Details

.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



106
107
108
# File 'lib/toys/utils/terminal.rb', line 106

def self.remove_style_escapes(str)
  str.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)


214
215
216
# File 'lib/toys/utils/terminal.rb', line 214

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



377
378
379
380
381
382
383
384
385
# 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) ⇒ 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)


238
239
240
241
242
243
244
245
246
247
248
249
# 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

#closeObject

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



190
191
192
# File 'lib/toys/utils/terminal.rb', line 190

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)


261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# 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) ⇒ 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)


362
363
364
365
# File 'lib/toys/utils/terminal.rb', line 362

def define_style(name, *styles)
  @named_styles[name] = resolve_styles(*styles)
  self
end

#heightInteger

Return the terminal height

Returns:

  • (Integer)


347
348
349
# File 'lib/toys/utils/terminal.rb', line 347

def height
  size[1]
end

#newlineself

Write a newline and flush the current line.

Returns:

  • (self)


222
223
224
# File 'lib/toys/utils/terminal.rb', line 222

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)


202
203
204
205
# File 'lib/toys/utils/terminal.rb', line 202

def puts(str = "", *styles)
  str = "#{str}\n" unless str.end_with?("\n")
  write(str, *styles)
end

#readlineString?

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



176
177
178
179
180
181
182
183
184
# File 'lib/toys/utils/terminal.rb', line 176

def readline
  @input_mutex.synchronize do
    begin
      input&.gets
    rescue ::IOError
      nil
    end
  end
end

#sizeArray(Integer,Integer)

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

Returns:

  • (Array(Integer,Integer))


325
326
327
328
329
330
331
# 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) ⇒ 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.



305
306
307
308
309
310
311
312
313
314
315
316
317
318
# 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

#widthInteger

Return the terminal width

Returns:

  • (Integer)


338
339
340
# File 'lib/toys/utils/terminal.rb', line 338

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)


158
159
160
161
162
163
164
165
166
167
168
# 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