diff --git a/lib/console/buffer.rb b/lib/console/buffer.rb deleted file mode 100644 index b68975a..0000000 --- a/lib/console/buffer.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. - -require 'stringio' - -module Console - class Buffer < StringIO - def initialize(prefix = nil) - @prefix = prefix - - super() - end - - def puts(*args, prefix: @prefix) - args.each do |arg| - self.write(prefix) if prefix - super(arg) - end - end - - alias << puts - end -end diff --git a/lib/console/compatible/logger.rb b/lib/console/compatible/logger.rb index ef52fd7..816ea31 100644 --- a/lib/console/compatible/logger.rb +++ b/lib/console/compatible/logger.rb @@ -7,6 +7,7 @@ module Console module Compatible + # A compatible interface for {::Logger} which can be used with {Console}. class Logger < ::Logger class LogDevice def initialize(subject, output) diff --git a/lib/console/event/failure.rb b/lib/console/event/failure.rb index 2243df3..841cd6c 100644 --- a/lib/console/event/failure.rb +++ b/lib/console/event/failure.rb @@ -4,80 +4,54 @@ # Copyright, 2019-2022, by Samuel Williams. # Copyright, 2021, by Robert Schulze. -require_relative 'generic' - module Console module Event - class Failure < Generic - def self.current_working_directory + # Represents a failure event. + # + # ```ruby + # Console.error(self, **Console::Event::Failure.for(exception)) + # ```` + class Failure + def self.default_root Dir.getwd rescue # e.g. Errno::EMFILE nil end def self.for(exception) - self.new(exception, self.current_working_directory) + self.new(exception, self.default_root) end - def initialize(exception, root = nil) + def initialize(exception, root) @exception = exception @root = root end - attr :exception - attr :root - - def self.register(terminal) - terminal[:exception_title] ||= terminal.style(:red, nil, :bold) - terminal[:exception_detail] ||= terminal.style(:yellow) - terminal[:exception_backtrace] ||= terminal.style(:red) - terminal[:exception_backtrace_other] ||= terminal.style(:red, nil, :faint) - terminal[:exception_message] ||= terminal.style(:default) - end - - def to_h - {exception: @exception, root: @root} - end - - def format(output, terminal, verbose) - format_exception(@exception, nil, output, terminal, verbose) - end - - if Exception.method_defined?(:detailed_message) - def detailed_message(exception) - exception.detailed_message - end - else - def detailed_message(exception) - exception.message + def to_hash + Hash.new.tap do |hash| + hash[:event] = :failure + hash[:root] = @root if @root + extract(@exception, hash) end end - def format_exception(exception, prefix, output, terminal, verbose) - lines = detailed_message(exception).lines.map(&:chomp) - - output.puts " #{prefix}#{terminal[:exception_title]}#{exception.class}#{terminal.reset}: #{lines.shift}" + private + + def extract(exception, hash) + hash[:title] = exception.class - lines.each do |line| - output.puts " #{terminal[:exception_detail]}#{line}#{terminal.reset}" + if exception.respond_to?(:detailed_message) + hash[:message] = exception.detailed_message + else + hash[:message] = exception.message end - root_pattern = /^#{@root}\// if @root + hash[:backtrace] = exception.backtrace - exception.backtrace&.each_with_index do |line, index| - path, offset, message = line.split(":", 3) - style = :exception_backtrace - - # Make the path a bit more readable - if root_pattern and path.sub!(root_pattern, "").nil? - style = :exception_backtrace_other + if cause = exception.cause + hash[:cause] = Hash.new.tap do |cause_hash| + extract(cause, cause_hash) end - - output.puts " #{index == 0 ? "→" : " "} #{terminal[style]}#{path}:#{offset}#{terminal[:exception_message]} #{message}#{terminal.reset}" - end - - if exception.cause - format_exception(exception.cause, "Caused by ", output, terminal, verbose) end end end diff --git a/lib/console/event/generic.rb b/lib/console/event/generic.rb deleted file mode 100644 index dbb151f..0000000 --- a/lib/console/event/generic.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2022, by Samuel Williams. - -module Console - module Event - class Generic - def self.register(terminal) - end - - def to_h - end - - def to_json(*arguments) - JSON.generate([self.class, to_h], *arguments) - end - - def format(buffer, terminal) - end - end - end -end diff --git a/lib/console/event/progress.rb b/lib/console/event/progress.rb index 45b6990..a92c991 100644 --- a/lib/console/event/progress.rb +++ b/lib/console/event/progress.rb @@ -1,59 +1,116 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2020-2022, by Samuel Williams. +# Copyright, 2020-2023, by Samuel Williams. +# Copyright, 2022, by Anton Sozontov. -require_relative 'generic' +require_relative '../clock' module Console module Event - class Progress < Generic - BLOCK = [ - " ", - "▏", - "▎", - "▍", - "▌", - "▋", - "▊", - "▉", - "█", - ] - - def initialize(current, total) - @current = current + class Progress + def self.now + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + def initialize(output, subject, total = 0, minimum_output_duration: 0.1, **options) + @output = output + @subject = subject + @options = options + + @start_time = Progress.now + + @last_output_time = nil + @minimum_output_duration = minimum_output_duration + + @current = 0 @total = total end + attr :subject attr :current attr :total - def value - @current.to_f / @total.to_f + def duration + Progress.now - @start_time + end + + def ratio + Rational(@current.to_f, @total.to_f) end - def bar(value = self.value, width = 70) - blocks = width * value - full_blocks = blocks.floor - partial_block = ((blocks - full_blocks) * BLOCK.size).floor + def remaining + @total - @current + end + + def average_duration + if @current > 0 + duration / @current + end + end + + def estimated_remaining_time + if average_duration = self.average_duration + average_duration * remaining + end + end + + def to_hash + Hash.new.tap do |hash| + hash[:event] = :progress + hash[:current] = @current + hash[:total] = @total + end + end + + def increment(amount = 1) + @current += amount - if partial_block.zero? - BLOCK.last * full_blocks - else - "#{BLOCK.last * full_blocks}#{BLOCK[partial_block]}" - end.ljust(width) + if output? + @output.info(@subject, self.to_s, **@options, **self) + @last_output_time = Progress.now + end + + return self end - def self.register(terminal) - terminal[:progress_bar] ||= terminal.style(:blue, :white) + def resize(total) + @total = total + + @output.call(@subject, self.to_s, **@options, **self) + @last_output_time = Progress.now + + return self end - def to_h - {current: @current, total: @total} + def mark(...) + @output.call(@subject, ...) end - def format(output, terminal, verbose) - output.puts "#{terminal[:progress_bar]}#{self.bar}#{terminal.reset} #{sprintf('%6.2f', self.value * 100)}%" + def to_s + if estimated_remaining_time = self.estimated_remaining_time + "#{@current}/#{@total} completed in #{Clock.formatted_duration(self.duration)}, #{Clock.formatted_duration(estimated_remaining_time)} remaining." + else + "#{@current}/#{@total} completed, waiting for estimate..." + end + end + + private + + def duration_since_last_output + if @last_output_time + Progress.now - @last_output_time + end + end + + def output? + if remaining.zero? + return true + elsif duration = duration_since_last_output + return duration > @minimum_output_duration + else + return true + end end end end diff --git a/lib/console/event/spawn.rb b/lib/console/event/spawn.rb index a4972aa..762f162 100644 --- a/lib/console/event/spawn.rb +++ b/lib/console/event/spawn.rb @@ -3,11 +3,14 @@ # Released under the MIT License. # Copyright, 2019-2022, by Samuel Williams. -require_relative 'generic' - module Console module Event - class Spawn < Generic + # Represents a spawn event. + # + # ```ruby + # Console.info(self, **Console::Event::Spawn.for("ls", "-l")) + # ``` + class Spawn def self.for(*arguments, **options) # Extract out the command environment: if arguments.first.is_a?(Hash) @@ -24,42 +27,14 @@ def initialize(environment, arguments, options) @options = options end - attr :environment - attr :arguments - attr :options - - def chdir_string(options) - if options and chdir = options[:chdir] - " in #{chdir}" - end - end - - def self.register(terminal) - terminal[:shell_command] ||= terminal.style(:blue, nil, :bold) - end - - def to_h + def to_hash Hash.new.tap do |hash| + hash[:event] = :spawn hash[:environment] = @environment if @environment&.any? hash[:arguments] = @arguments if @arguments&.any? hash[:options] = @options if @options&.any? end end - - def format(output, terminal, verbose) - arguments = @arguments.flatten.collect(&:to_s) - - output.puts " #{terminal[:shell_command]}#{arguments.join(' ')}#{terminal.reset}#{chdir_string(options)}" - - if verbose and @environment - @environment.each do |key, value| - output.puts " export #{key}=#{value}" - end - end - end end end - - # Deprecated. - Shell = Event::Spawn end diff --git a/lib/console/filter.rb b/lib/console/filter.rb index 863bf8e..8c10198 100644 --- a/lib/console/filter.rb +++ b/lib/console/filter.rb @@ -6,8 +6,6 @@ # Copyright, 2020, by Michael Adams. # Copyright, 2021, by Robert Schulze. -require_relative 'buffer' - module Console UNKNOWN = 'unknown' diff --git a/lib/console/logger.rb b/lib/console/logger.rb index be171f5..95f43eb 100644 --- a/lib/console/logger.rb +++ b/lib/console/logger.rb @@ -7,7 +7,7 @@ require_relative 'output' require_relative 'filter' -require_relative 'progress' +require_relative 'event' require_relative 'resolver' require_relative 'terminal/logger' @@ -69,12 +69,22 @@ def initialize(output, **options) end def progress(subject, total, **options) - Progress.new(self, subject, total, **options) + options[:severity] ||= :info + + Event::Progress.new(self, subject, total, **options) + end + + def error(subject, *arguments, **options, &block) + if arguments.first.is_a?(Exception) + exception = arguments.shift + options.merge!(Event::Failure.for(exception)) + end + + super end - # @deprecated Use `fatal` instead. - def failure(subject, exception, *arguments, &block) - self.fatal(subject, exception, *arguments, &block) + def failure(subject, exception, *arguments, **options, &block) + self.error(subject, *arguments, **options, **Event::Failure.for(exception), &block) end end end diff --git a/lib/console/progress.rb b/lib/console/progress.rb deleted file mode 100644 index 7851bf1..0000000 --- a/lib/console/progress.rb +++ /dev/null @@ -1,107 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2020-2023, by Samuel Williams. -# Copyright, 2022, by Anton Sozontov. - -require_relative 'event/progress' -require_relative 'clock' - -module Console - class Progress - def self.now - Process.clock_gettime(Process::CLOCK_MONOTONIC) - end - - def initialize(output, subject, total = 0, minimum_output_duration: 0.1) - @output = output - @subject = subject - - @start_time = Progress.now - - @last_output_time = nil - @minimum_output_duration = minimum_output_duration - - @current = 0 - @total = total - end - - attr :subject - attr :current - attr :total - - def duration - Progress.now - @start_time - end - - def ratio - Rational(@current.to_f, @total.to_f) - end - - def remaining - @total - @current - end - - def average_duration - if @current > 0 - duration / @current - end - end - - def estimated_remaining_time - if average_duration = self.average_duration - average_duration * remaining - end - end - - def increment(amount = 1) - @current += amount - - if output? - @output.info(@subject, self) {Event::Progress.new(@current, @total)} - @last_output_time = Progress.now - end - - return self - end - - def resize(total) - @total = total - - @output.info(@subject, self) {Event::Progress.new(@current, @total)} - @last_output_time = Progress.now - - return self - end - - def mark(...) - @output.info(@subject, ...) - end - - def to_s - if estimated_remaining_time = self.estimated_remaining_time - "#{@current}/#{@total} completed in #{Clock.formatted_duration(self.duration)}, #{Clock.formatted_duration(estimated_remaining_time)} remaining." - else - "#{@current}/#{@total} completed, waiting for estimate..." - end - end - - private - - def duration_since_last_output - if @last_output_time - Progress.now - @last_output_time - end - end - - def output? - if remaining.zero? - return true - elsif duration = duration_since_last_output - return duration > @minimum_output_duration - else - return true - end - end - end -end diff --git a/lib/console/serialized/logger.rb b/lib/console/serialized/logger.rb index 9482975..7ef812b 100644 --- a/lib/console/serialized/logger.rb +++ b/lib/console/serialized/logger.rb @@ -3,7 +3,6 @@ # Released under the MIT License. # Copyright, 2019-2023, by Samuel Williams. -require_relative '../buffer' require_relative '../filter' require_relative '../format' diff --git a/lib/console/terminal/buffer.rb b/lib/console/terminal/buffer.rb new file mode 100644 index 0000000..dae2773 --- /dev/null +++ b/lib/console/terminal/buffer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2022, by Samuel Williams. + +require 'stringio' + +module Console + module Terminal + class Buffer < StringIO + def initialize(prefix = nil) + @prefix = prefix + + super() + end + + attr :prefix + + def puts(*args, prefix: @prefix) + args.each do |arg| + self.write(prefix) if prefix + super(arg) + end + end + + alias << puts + end + end +end diff --git a/lib/console/terminal/formatter/failure.rb b/lib/console/terminal/formatter/failure.rb new file mode 100644 index 0000000..86719b7 --- /dev/null +++ b/lib/console/terminal/formatter/failure.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2022, by Samuel Williams. +# Copyright, 2021, by Robert Schulze. + +module Console + module Terminal + module Formatter + class Failure + KEY = :failure + + def initialize(terminal, root: Dir.pwd) + @terminal = terminal + @root = root + + @terminal[:exception_title] ||= @terminal.style(:red, nil, :bold) + @terminal[:exception_detail] ||= @terminal.style(:yellow) + @terminal[:exception_backtrace] ||= @terminal.style(:red) + @terminal[:exception_backtrace_other] ||= @terminal.style(:red, nil, :faint) + @terminal[:exception_message] ||= @terminal.style(:default) + end + + def format(event, output, prefix: nil, verbose: false, width: 80) + title = event[:title] + message = event[:message] + backtrace = event[:backtrace] + + lines = message.lines.map(&:chomp) + + output.puts " #{prefix}#{@terminal[:exception_title]}#{title}#{@terminal.reset}: #{lines.shift}" + + lines.each do |line| + output.puts " #{@terminal[:exception_detail]}#{line}#{@terminal.reset}" + end + + root_pattern = /^#{@root}\// if @root + + backtrace&.each_with_index do |line, index| + path, offset, message = line.split(":", 3) + style = :exception_backtrace + + # Make the path a bit more readable: + if root_pattern and path.sub!(root_pattern, "").nil? + style = :exception_backtrace_other + end + + output.puts " #{index == 0 ? "→" : " "} #{@terminal[style]}#{path}:#{offset}#{@terminal[:exception_message]} #{message}#{@terminal.reset}" + end + + if cause = event[:cause] + format(cause, output, prefix: "Caused by ", verbose: verbose, width: width) + end + end + end + end + end +end diff --git a/lib/console/terminal/formatter/progress.rb b/lib/console/terminal/formatter/progress.rb new file mode 100644 index 0000000..b984b23 --- /dev/null +++ b/lib/console/terminal/formatter/progress.rb @@ -0,0 +1,56 @@ +# Released under the MIT License. +# Copyright, 2020-2022, by Samuel Williams. + +module Console + module Terminal + module Formatter + class Progress + KEY = :progress + + BLOCK = [ + " ", + "▏", + "▎", + "▍", + "▌", + "▋", + "▊", + "▉", + "█", + ] + + def initialize(terminal) + @terminal = terminal + @terminal[:progress_bar] ||= terminal.style(:blue, :white) + end + + def format(event, output, verbose: false, width: 80) + current = event[:current].to_f + total = event[:total].to_f + value = current / total + + # Clamp value to 1.0 to avoid rendering issues: + if value > 1.0 + value = 1.0 + end + + output.puts "#{@terminal[:progress_bar]}#{self.bar(value, width-10)}#{@terminal.reset} #{sprintf('%6.2f', value * 100)}%" + end + + private + + def bar(value, width) + blocks = width * value + full_blocks = blocks.floor + partial_block = ((blocks - full_blocks) * BLOCK.size).floor + + if partial_block.zero? + BLOCK.last * full_blocks + else + "#{BLOCK.last * full_blocks}#{BLOCK[partial_block]}" + end.ljust(width) + end + end + end + end +end diff --git a/lib/console/terminal/formatter/spawn.rb b/lib/console/terminal/formatter/spawn.rb new file mode 100644 index 0000000..4d84fab --- /dev/null +++ b/lib/console/terminal/formatter/spawn.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2022, by Samuel Williams. + +module Console + module Terminal + module Formatter + # Format spawn events. + class Spawn + KEY = :spawn + + def initialize(terminal) + @terminal = terminal + @terminal[:spawn_command] ||= @terminal.style(:blue, nil, :bold) + end + + def format(event, output, verbose: false, width: 80) + environment, arguments, options = event.values_at(:environment, :arguments, :options) + + arguments = arguments.flatten.collect(&:to_s) + + output.puts "#{@terminal[:spawn_command]}#{arguments.join(' ')}#{@terminal.reset}#{chdir_string(options)}" + + if verbose and environment + environment.each do |key, value| + output.puts "export #{key}=#{value}" + end + end + end + + private + + def chdir_string(options) + if options and chdir = options[:chdir] + " in #{chdir}" + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/console/terminal/logger.rb b/lib/console/terminal/logger.rb index c26ab4e..532540a 100644 --- a/lib/console/terminal/logger.rb +++ b/lib/console/terminal/logger.rb @@ -4,8 +4,7 @@ # Copyright, 2019-2023, by Samuel Williams. # Copyright, 2021, by Robert Schulze. -require_relative '../buffer' -require_relative '../event' +require_relative 'buffer' require_relative '../clock' require_relative 'text' @@ -15,6 +14,10 @@ require 'fiber' require 'fiber/annotation' +require_relative 'formatter/progress' +require_relative 'formatter/failure' +require_relative 'formatter/spawn' + module Console module Terminal # This, and all related methods, is considered private. @@ -66,7 +69,8 @@ def initialize(io = $stderr, verbose: nil, start_at: Terminal.start_at!, format: @terminal[:annotation] = @terminal.reset @terminal[:value] = @terminal.style(:blue) - self.register_defaults(@terminal) + @formatters = {} + self.register_defaults end attr :io @@ -80,20 +84,23 @@ def verbose!(value = true) @verbose = value end - def register_defaults(terminal) - Event.constants.each do |constant| - klass = Event.const_get(constant) - klass.register(terminal) + def register_defaults + Formatter.constants.each do |formatter| + formatter = Formatter.const_get(formatter) + @formatters[formatter::KEY] = formatter.new(@terminal) end end UNKNOWN = :unknown - def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, **options, &block) + def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, event: nil, **options, &block) + width = @terminal.width + prefix = build_prefix(name || severity.to_s) indent = " " * prefix.size buffer = Buffer.new("#{indent}| ") + indent_size = buffer.prefix.size format_subject(severity, prefix, subject, buffer) @@ -109,7 +116,9 @@ def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, **options, &bl end end - if options&.any? + if event and formatter = @formatters[event] + formatter.format(options, buffer, verbose: @verbose, width: width - indent_size) + elsif options&.any? format_options(options, buffer) end @@ -123,15 +132,8 @@ def format_options(options, output) end def format_argument(argument, output) - case argument - when Exception - Event::Failure.for(argument).format(output, @terminal, @verbose) - when Event::Generic - argument.format(output, @terminal, @verbose) - else - argument.to_s.each_line do |line| - output.puts line - end + argument.to_s.each_line do |line| + output.puts line end end diff --git a/lib/console/terminal/text.rb b/lib/console/terminal/text.rb index 7499cad..a011c06 100644 --- a/lib/console/terminal/text.rb +++ b/lib/console/terminal/text.rb @@ -26,6 +26,10 @@ def colors? false end + def width + 80 + end + def style(foreground, background = nil, *attributes) end diff --git a/lib/console/terminal/xterm.rb b/lib/console/terminal/xterm.rb index f4b68ec..de0f9e6 100644 --- a/lib/console/terminal/xterm.rb +++ b/lib/console/terminal/xterm.rb @@ -43,6 +43,10 @@ def size @output.winsize end + def width + size.last + end + def style(foreground, background = nil, *attributes) tokens = []