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 stream @return [IO,nil]
Output stream or logger @return [IO,Logger,nil]
Whether output is styled @return [Boolean]
Public Class Methods
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
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
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 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 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
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 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 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
Return the terminal height
@return [Integer]
# File lib/toys/utils/terminal.rb, line 347 def height size[1] end
Write a newline and flush the current line. @return [self]
# File lib/toys/utils/terminal.rb, line 222 def newline puts end
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
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
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
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
Return the terminal width
@return [Integer]
# File lib/toys/utils/terminal.rb, line 338 def width size[0] end
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
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
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 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