Class: Toys::Utils::StandardUI

Inherits:
Object
  • Object
show all
Defined in:
lib/toys/utils/standard_ui.rb

Overview

An object that implements standard UI elements, such as error reports and logging, as provided by the toys command line. Specifically, it implements pretty formatting of log entries and stack traces, and renders using ANSI coloring where available via Terminal.

This object can be used to implement toys-style behavior when creating a CLI object. For example:

require "toys/utils/standard_ui"
ui = Toys::Utils::StandardUI.new
cli = Toys::CLI.new(**ui.cli_args)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(output: nil, backtrace_omit_prefixes: nil, incomplete_backtrace_message: nil) ⇒ StandardUI

Create a Standard UI.

By default, all output is written to $stderr, and will share a single Terminal object, allowing multiple tools and/or threads to interleave messages without interrupting one another.

Parameters:

  • output (IO, Toys::Utils::Terminal) (defaults to: nil)

    Where to write output. You can pass a terminal object, or an IO stream that will be wrapped in a terminal output. Default is $stderr.

  • backtrace_omit_prefixes (Array<String>) (defaults to: nil)

    An array of directories under which Ruby files should be elided from backtraces. Optional. To elide internal Toys framework files, you can pass Toys.framework_lib_paths.

  • incomplete_backtrace_message (String) (defaults to: nil)

    A message to display when the backtrace has been elided. Optional.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/toys/utils/standard_ui.rb', line 38

def initialize(output: nil, backtrace_omit_prefixes: nil, incomplete_backtrace_message: nil)
  require "toys/utils/terminal"
  @terminal = output || $stderr
  @terminal = Terminal.new(output: @terminal) unless @terminal.is_a?(Terminal)
  @backtrace_omit_prefixes = backtrace_omit_prefixes&.map do |dir|
    dir.end_with?(::File::SEPARATOR) ? dir : "#{dir}#{::File::SEPARATOR}"
  end
  @incomplete_backtrace_message = incomplete_backtrace_message
  @log_header_severity_styles = {
    "FATAL" => [:bright_magenta, :bold, :underline],
    "ERROR" => [:bright_red, :bold],
    "WARN" => [:bright_yellow],
    "INFO" => [:bright_cyan],
    "DEBUG" => [:white],
  }
end

Instance Attribute Details

#log_header_severity_stylesHash{String => Array<Symbol>} (readonly)

A hash that maps severities to styles recognized by Terminal. Used to style the header for each log entry. This hash can be modified in place to adjust the behavior of loggers created by this UI.

Returns:

  • (Hash{String => Array<Symbol>})


70
71
72
# File 'lib/toys/utils/standard_ui.rb', line 70

def log_header_severity_styles
  @log_header_severity_styles
end

#terminalToys::Utils::Terminal (readonly)

The terminal underlying this UI



60
61
62
# File 'lib/toys/utils/standard_ui.rb', line 60

def terminal
  @terminal
end

Instance Method Details

#cli_argsHash

Convenience method that returns a hash of arguments that can be passed to the CLI constructor. Includes the :error_handler and :logger_factory arguments.

Returns:

  • (Hash)


79
80
81
82
83
84
# File 'lib/toys/utils/standard_ui.rb', line 79

def cli_args
  {
    error_handler: error_handler_proc,
    logger_factory: logger_factory_proc,
  }
end

#create_logger(_tool) ⇒ Logger

Implementation of a logger factory. As dictated by the logger factory specification in Runner, this must take a ToolDefinition as an argument, and return a Logger.

The base implementation returns a logger that writes to the UI's terminal, using #format_log_entry as the formatter. It sets the level to Logger::WARN by default. Either this method or the helper methods can be overridden to change this behavior.

Parameters:

Returns:

  • (Logger)


154
155
156
157
158
159
# File 'lib/toys/utils/standard_ui.rb', line 154

def create_logger(_tool)
  logger = ::Logger.new(@terminal)
  logger.formatter = method(:format_log_entry).to_proc
  logger.level = ::Logger::WARN
  logger
end

#display_error_notice(error) ⇒ Object

Displays a default output for an error.

The output format includes the error message itself, a backtrace (possibly with some entries omitted), and the stack of tool calls if available (i.e. if the error is a ContextualError).

This method is used by #handle_error and can be overridden to change the rendering.

Parameters:



219
220
221
222
223
224
225
226
227
# File 'lib/toys/utils/standard_ui.rb', line 219

def display_error_notice(error)
  @terminal.puts
  origin, banner, frames = error_frames(error)
  render_backtrace(origin)
  render_banner(banner)
  frames.each do |frame|
    render_tool_line(frame)
  end
end

#display_signal_notice(error) ⇒ Object

Displays a default output for a signal received.

This method is used by #handle_error and can be overridden to change its behavior.

Parameters:

  • error (SignalException)


196
197
198
199
200
201
202
203
# File 'lib/toys/utils/standard_ui.rb', line 196

def display_signal_notice(error)
  @terminal.puts
  if error.is_a?(::Interrupt)
    @terminal.puts("INTERRUPTED", :bold)
  else
    @terminal.puts("SIGNAL RECEIVED: #{error.signm || error.signo}", :bold)
  end
end

#error_handler_procProc

Convenience method that returns the error handler proc implemented by this UI (in the #handle_error method). This proc can be passed to the :error_handler argument in the CLI constructor.

Returns:

  • (Proc)


93
94
95
# File 'lib/toys/utils/standard_ui.rb', line 93

def error_handler_proc
  method(:handle_error).to_proc
end

#exit_code_for(error) ⇒ Integer

Returns an exit code appropriate for the given exception. Currently, the logic interprets signals (returning the convention of 128 + signo), usage errors (returning the conventional value of 2), and tool not runnable errors (returning the conventional value of 126), and defaults to 1 for all other error types.

This method is used by #handle_error and can be overridden to change its behavior.

Parameters:

  • error (Exception)

    The exception raised. This method expects the original exception, rather than a ContextualError.

Returns:

  • (Integer)

    The appropriate exit code



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

def exit_code_for(error)
  case error
  when ArgParsingError
    2
  when NotRunnableError
    126
  when ::SignalException
    error.signo + 128
  else
    1
  end
end

#format_log_entry(severity, time, _progname, msg) ⇒ String

Implementation of the formatter used by loggers created by this UI's logger factory. This interface is defined by the standard Logger class.

This method can be overridden to change the behavior of loggers created by this UI.

Parameters:

  • severity (String)
  • time (Time)
  • _progname (String)
  • msg (Object)

Returns:

  • (String)


243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/toys/utils/standard_ui.rb', line 243

def format_log_entry(severity, time, _progname, msg)
  msg_str =
    case msg
    when ::String
      msg
    when ::Exception
      "#{msg.message} (#{msg.class})\n" << (msg.backtrace || []).join("\n")
    else
      msg.inspect
    end
  timestr = time.strftime("%Y-%m-%d %H:%M:%S")
  header = format("[%<time>s %<sev>5s]", time: timestr, sev: severity)
  styles = log_header_severity_styles[severity]
  header = @terminal.apply_styles(header, *styles) if styles
  "#{header}  #{msg_str}\n"
end

#handle_error(error) ⇒ Integer

Implementation of an error handler. As dictated by the error handler specification in Runner, this takes the error as its argument, and returns an exit code or raises an exception.

The base implementation uses #display_error_notice and #display_signal_notice to print an appropriate message to the UI's terminal, and uses #exit_code_for to determine the correct exit code. Any of those methods can be overridden by a subclass to alter their behavior, or this main implementation method can be overridden to change the overall behavior.

Parameters:

  • error (Toys::ContextualError, SignalException, StandardError, ScriptError)

    The error received. An unhandled signal arrives unwrapped. Any other error normally arrives as a ContextualError wrapper, but arrives unwrapped if the run disabled error wrapping.

Returns:

  • (Integer)

    The exit code



126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/toys/utils/standard_ui.rb', line 126

def handle_error(error)
  case error
  when ::SignalException
    display_signal_notice(error)
    exit_code_for(error)
  when ContextualError
    display_error_notice(error)
    exit_code_for(error.root_cause)
  else
    display_error_notice(error)
    exit_code_for(error)
  end
end

#logger_factory_procProc

Convenience method that returns the logger factory proc implemented by this UI (in the #create_logger method). This proc can be passed to the :logger_factory argument in the CLI constructor.

Returns:

  • (Proc)


104
105
106
# File 'lib/toys/utils/standard_ui.rb', line 104

def logger_factory_proc
  method(:create_logger).to_proc
end