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)

Defined Under Namespace

Classes: VerbosityLogger

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.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/toys/utils/standard_ui.rb', line 63

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" => [],
  }
end

Instance Attribute Details

#log_header_severity_styles ⇒ Hash{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>})


95
96
97
# File 'lib/toys/utils/standard_ui.rb', line 95

def log_header_severity_styles
  @log_header_severity_styles
end

#terminal ⇒ Toys::Utils::Terminal (readonly)

The terminal underlying this UI



85
86
87
# File 'lib/toys/utils/standard_ui.rb', line 85

def terminal
  @terminal
end

Instance Method Details

#cli_args ⇒ Hash

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)


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

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 VerbosityLogger, which receives the verbosity of the run from the Runner. The logger writes to the UI's terminal. It formats each entry using #format_verbose_log_entry if #verbose_log_format? returns true for the verbosity, or #format_simple_log_entry otherwise. It sets the level to Logger::WARN by default. Either this method or the helper methods can be overridden to change this behavior.

If #format_log_entry is overridden, however, the override formats every entry regardless of verbosity. That behavior is deprecated.

Parameters:

Returns:

  • (Logger)


186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/toys/utils/standard_ui.rb', line 186

def create_logger(_tool)
  logger = VerbosityLogger.new(@terminal)
  format_log_entry_method = method(:format_log_entry)
  logger.formatter =
    if format_log_entry_method.owner == StandardUI
      proc do |severity, time, progname, msg|
        if verbose_log_format?(logger.verbosity)
          format_verbose_log_entry(severity, time, progname, msg)
        else
          format_simple_log_entry(severity, time, progname, msg)
        end
      end
    else
      message = "Overriding StandardUI#format_log_entry is deprecated. Override " \
                "format_verbose_log_entry and/or format_simple_log_entry instead."
      source_loc = format_log_entry_method.source_location
      message = "#{source_loc[0]}:#{source_loc[1]}: #{message}" if source_loc
      Warnings.warn(:format_log_entry_deprecated, message)
      format_log_entry_method.to_proc
    end
  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:



268
269
270
271
272
273
274
275
276
# File 'lib/toys/utils/standard_ui.rb', line 268

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)


245
246
247
248
249
250
251
252
# File 'lib/toys/utils/standard_ui.rb', line 245

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_proc ⇒ Proc

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)


118
119
120
# File 'lib/toys/utils/standard_ui.rb', line 118

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



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/toys/utils/standard_ui.rb', line 224

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

Deprecated.

A legacy formatter for loggers created by this UI's logger factory. This interface is defined by the standard Logger class.

The base implementation calls #format_verbose_log_entry, and is not used by loggers created by this UI's logger factory unless it is overridden. If a subclass overrides this method, the override is used to format every entry regardless of verbosity, and #verbose_log_format?, #format_verbose_log_entry, and #format_simple_log_entry are not called by the logger. The override may call super to get the verbose format.

Parameters:

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

Returns:

  • (String)


364
365
366
# File 'lib/toys/utils/standard_ui.rb', line 364

def format_log_entry(severity, time, progname, msg)
  format_verbose_log_entry(severity, time, progname, msg)
end

#format_simple_log_entry(severity, _time, _progname, msg) ⇒ String

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

The base implementation prefixes the first line of the message with the severity, styled using #log_header_severity_styles. If the message is an exception, it includes the exception message and class, but omits the backtrace.

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)


313
314
315
316
# File 'lib/toys/utils/standard_ui.rb', line 313

def format_simple_log_entry(severity, _time, _progname, msg)
  header = style_log_header("#{severity}:", severity)
  "#{header} #{log_message_string(msg, backtrace: false)}\n"
end

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

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

The base implementation prefixes the message with a header containing the timestamp and severity, styled using #log_header_severity_styles. If the message is an exception, it includes the exception message and class, followed by the backtrace.

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)


337
338
339
340
341
# File 'lib/toys/utils/standard_ui.rb', line 337

def format_verbose_log_entry(severity, time, _progname, msg)
  timestr = time.strftime("%Y-%m-%d %H:%M:%S")
  header = style_log_header(format("[%<time>s %<sev>5s]", time: timestr, sev: severity), severity)
  "#{header}  #{log_message_string(msg, backtrace: true)}\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



151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/toys/utils/standard_ui.rb', line 151

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_proc ⇒ Proc

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)


129
130
131
# File 'lib/toys/utils/standard_ui.rb', line 129

def logger_factory_proc
  method(:create_logger).to_proc
end

#verbose_log_format?(verbosity) ⇒ boolean

Determines whether loggers created by this UI's logger factory use the verbose format, #format_verbose_log_entry, rather than the simple format, #format_simple_log_entry, for the given verbosity.

The base implementation returns true if the verbosity is positive. This method can be overridden to change the behavior of loggers created by this UI.

Parameters:

  • verbosity (Integer) —

    The verbosity of the run.

Returns:

  • (boolean)


290
291
292
# File 'lib/toys/utils/standard_ui.rb', line 290

def verbose_log_format?(verbosity)
  verbosity.positive?
end