From 580234a6473eeac4eb7fa587113c9c820507bf29 Mon Sep 17 00:00:00 2001 From: Petrik Date: Tue, 1 Aug 2023 21:34:54 +0200 Subject: [PATCH] Extract print methods to seperate classes The printing methods are already pretty large, making it difficult to add extra functionality. Extracting them to seperate classes allows refactoring them for easier maintainability. A lot of the functionality of calculating the terminal width can be extracted to a separate object as well. --- lib/thor/shell/basic.rb | 152 +++--------------------------- lib/thor/shell/column_printer.rb | 29 ++++++ lib/thor/shell/table_printer.rb | 95 +++++++++++++++++++ lib/thor/shell/terminal.rb | 42 +++++++++ lib/thor/shell/wrapped_printer.rb | 38 ++++++++ spec/shell/basic_spec.rb | 3 +- spec/thor_spec.rb | 2 +- 7 files changed, 217 insertions(+), 144 deletions(-) create mode 100644 lib/thor/shell/column_printer.rb create mode 100644 lib/thor/shell/table_printer.rb create mode 100644 lib/thor/shell/terminal.rb create mode 100644 lib/thor/shell/wrapped_printer.rb diff --git a/lib/thor/shell/basic.rb b/lib/thor/shell/basic.rb index 4b196844..0708ca59 100644 --- a/lib/thor/shell/basic.rb +++ b/lib/thor/shell/basic.rb @@ -1,8 +1,10 @@ +require_relative "column_printer" +require_relative "table_printer" +require_relative "wrapped_printer" + class Thor module Shell class Basic - DEFAULT_TERMINAL_WIDTH = 80 - attr_accessor :base attr_reader :padding @@ -161,16 +163,8 @@ def no?(statement, color = nil) # Array[String, String, ...] # def print_in_columns(array) - return if array.empty? - colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2 - array.each_with_index do |value, index| - # Don't output trailing spaces when printing the last column - if ((((index + 1) % (terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length - stdout.puts value - else - stdout.printf("%-#{colwidth}s", value) - end - end + printer = ColumnPrinter.new(stdout) + printer.print(array) end # Prints a table. @@ -183,56 +177,8 @@ def print_in_columns(array) # colwidth:: Force the first column to colwidth spaces wide. # def print_table(array, options = {}) # rubocop:disable Metrics/MethodLength - return if array.empty? - - formats = [] - indent = options[:indent].to_i - colwidth = options[:colwidth] - options[:truncate] = terminal_width if options[:truncate] == true - - formats << "%-#{colwidth + 2}s".dup if colwidth - start = colwidth ? 1 : 0 - - colcount = array.max { |a, b| a.size <=> b.size }.size - - maximas = [] - - start.upto(colcount - 1) do |index| - maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max - maximas << maxima - formats << if index == colcount - 1 - # Don't output 2 trailing spaces when printing the last column - "%-s".dup - else - "%-#{maxima + 2}s".dup - end - end - - formats[0] = formats[0].insert(0, " " * indent) - formats << "%s" - - array.each do |row| - sentence = "".dup - - row.each_with_index do |column, index| - maxima = maximas[index] - - f = if column.is_a?(Numeric) - if index == row.size - 1 - # Don't output 2 trailing spaces when printing the last column - "%#{maxima}s" - else - "%#{maxima}s " - end - else - formats[index] - end - sentence << f % column.to_s - end - - sentence = truncate(sentence, options[:truncate]) if options[:truncate] - stdout.puts sentence - end + printer = TablePrinter.new(stdout, options) + printer.print(array) end # Prints a long string, word-wrapping the text to the current width of the @@ -245,33 +191,8 @@ def print_table(array, options = {}) # rubocop:disable Metrics/MethodLength # indent:: Indent each line of the printed paragraph by indent value. # def print_wrapped(message, options = {}) - indent = options[:indent] || 0 - width = terminal_width - indent - paras = message.split("\n\n") - - paras.map! do |unwrapped| - words = unwrapped.split(" ") - counter = words.first.length - words.inject do |memo, word| - word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n") - counter = 0 if word.include? "\n" - if (counter + word.length + 1) < width - memo = "#{memo} #{word}" - counter += (word.length + 1) - else - memo = "#{memo}\n#{word}" - counter = word.length - end - memo - end - end.compact! - - paras.each do |para| - para.split("\n").each do |line| - stdout.puts line.insert(0, " " * indent) - end - stdout.puts unless para == paras.last - end + printer = WrappedPrinter.new(stdout, options) + printer.print(message) end # Deals with file collision and returns true if the file should be @@ -321,19 +242,6 @@ def file_collision(destination) end end - # This code was copied from Rake, available under MIT-LICENSE - # Copyright (c) 2003, 2004 Jim Weirich - def terminal_width - result = if ENV["THOR_COLUMNS"] - ENV["THOR_COLUMNS"].to_i - else - unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH - end - result < 10 ? DEFAULT_TERMINAL_WIDTH : result - rescue - DEFAULT_TERMINAL_WIDTH - end - # Called if something goes wrong during the execution. This is used by Thor # internally and should not be used inside your scripts. If something went # wrong, you can always raise an exception. If you raise a Thor::Error, it @@ -416,46 +324,8 @@ def quiet? #:nodoc: mute? || (base && base.options[:quiet]) end - # Calculate the dynamic width of the terminal - def dynamic_width - @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) - end - - def dynamic_width_stty - `stty size 2>/dev/null`.split[1].to_i - end - - def dynamic_width_tput - `tput cols 2>/dev/null`.to_i - end - def unix? - RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris)/i - end - - def truncate(string, width) - as_unicode do - chars = string.chars.to_a - if chars.length <= width - chars.join - else - chars[0, width - 3].join + "..." - end - end - end - - if "".respond_to?(:encode) - def as_unicode - yield - end - else - def as_unicode - old = $KCODE - $KCODE = "U" - yield - ensure - $KCODE = old - end + Terminal.unix? end def ask_simply(statement, color, options) diff --git a/lib/thor/shell/column_printer.rb b/lib/thor/shell/column_printer.rb new file mode 100644 index 00000000..983b4fde --- /dev/null +++ b/lib/thor/shell/column_printer.rb @@ -0,0 +1,29 @@ +require_relative "terminal" + +class Thor + module Shell + class ColumnPrinter + attr_reader :stdout, :options + + def initialize(stdout, options = {}) + @stdout = stdout + @options = options + @indent = options[:indent].to_i + end + + def print(array) + return if array.empty? + colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2 + array.each_with_index do |value, index| + # Don't output trailing spaces when printing the last column + if ((((index + 1) % (Terminal.terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length + stdout.puts value + else + stdout.printf("%-#{colwidth}s", value) + end + end + end + end + end +end + diff --git a/lib/thor/shell/table_printer.rb b/lib/thor/shell/table_printer.rb new file mode 100644 index 00000000..e1ab6fd1 --- /dev/null +++ b/lib/thor/shell/table_printer.rb @@ -0,0 +1,95 @@ +require_relative "column_printer" +require_relative "terminal" + +class Thor + module Shell + class TablePrinter < ColumnPrinter + def initialize(stdout, options = {}) + super + @formats = [] + @maximas = [] + @colwidth = options[:colwidth] + @truncate = options[:truncate] == true ? Terminal.terminal_width : options[:truncate] + end + + def print(array) + return if array.empty? + + prepare(array) + + array.each do |row| + sentence = "".dup + + row.each_with_index do |column, index| + maxima = @maximas[index] + + f = if column.is_a?(Numeric) + if index == row.size - 1 + # Don't output 2 trailing spaces when printing the last column + "%#{maxima}s" + else + "%#{maxima}s " + end + else + @formats[index] + end + sentence << f % column.to_s + end + + sentence = truncate(sentence) + stdout.puts sentence + end + end + + private + + def prepare(array) + @formats << "%-#{@colwidth + 2}s".dup if @colwidth + start = @colwidth ? 1 : 0 + + colcount = array.max { |a, b| a.size <=> b.size }.size + + start.upto(colcount - 1) do |index| + maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max + @maximas << maxima + @formats << if index == colcount - 1 + # Don't output 2 trailing spaces when printing the last column + "%-s".dup + else + "%-#{maxima + 2}s".dup + end + end + + @formats[0] = @formats[0].insert(0, " " * @indent) + @formats << "%s" + end + + def truncate(string) + return string unless @truncate + as_unicode do + chars = string.chars.to_a + if chars.length <= @truncate + chars.join + else + chars[0, @truncate - 3].join + "..." + end + end + end + + if "".respond_to?(:encode) + def as_unicode + yield + end + else + def as_unicode + old = $KCODE # rubocop:disable Style/GlobalVars + $KCODE = "U" # rubocop:disable Style/GlobalVars + yield + ensure + $KCODE = old # rubocop:disable Style/GlobalVars + end + end + end + end +end + diff --git a/lib/thor/shell/terminal.rb b/lib/thor/shell/terminal.rb new file mode 100644 index 00000000..5716f003 --- /dev/null +++ b/lib/thor/shell/terminal.rb @@ -0,0 +1,42 @@ +class Thor + module Shell + module Terminal + DEFAULT_TERMINAL_WIDTH = 80 + + class << self + # This code was copied from Rake, available under MIT-LICENSE + # Copyright (c) 2003, 2004 Jim Weirich + def terminal_width + result = if ENV["THOR_COLUMNS"] + ENV["THOR_COLUMNS"].to_i + else + unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH + end + result < 10 ? DEFAULT_TERMINAL_WIDTH : result + rescue + DEFAULT_TERMINAL_WIDTH + end + + def unix? + RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris)/i + end + + private + + # Calculate the dynamic width of the terminal + def dynamic_width + @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) + end + + def dynamic_width_stty + `stty size 2>/dev/null`.split[1].to_i + end + + def dynamic_width_tput + `tput cols 2>/dev/null`.to_i + end + + end + end + end +end diff --git a/lib/thor/shell/wrapped_printer.rb b/lib/thor/shell/wrapped_printer.rb new file mode 100644 index 00000000..a079c1d2 --- /dev/null +++ b/lib/thor/shell/wrapped_printer.rb @@ -0,0 +1,38 @@ +require_relative "column_printer" +require_relative "terminal" + +class Thor + module Shell + class WrappedPrinter < ColumnPrinter + def print(message) + width = Terminal.terminal_width - @indent + paras = message.split("\n\n") + + paras.map! do |unwrapped| + words = unwrapped.split(" ") + counter = words.first.length + words.inject do |memo, word| + word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n") + counter = 0 if word.include? "\n" + if (counter + word.length + 1) < width + memo = "#{memo} #{word}" + counter += (word.length + 1) + else + memo = "#{memo}\n#{word}" + counter = word.length + end + memo + end + end.compact! + + paras.each do |para| + para.split("\n").each do |line| + stdout.puts line.insert(0, " " * @indent) + end + stdout.puts unless para == paras.last + end + end + end + end +end + diff --git a/spec/shell/basic_spec.rb b/spec/shell/basic_spec.rb index 99ad9e1f..f9f34c85 100644 --- a/spec/shell/basic_spec.rb +++ b/spec/shell/basic_spec.rb @@ -368,8 +368,7 @@ def shell it "uses maximum terminal width" do @table << ["def", "#456", "Lançam foo bar"] @table << ["ghi", "#789", "بالله عليكم"] - expect(shell).to receive(:terminal_width).and_return(20) - content = capture(:stdout) { shell.print_table(@table, indent: 2, truncate: true) } + content = capture(:stdout) { shell.print_table(@table, indent: 2, truncate: 20) } expect(content).to eq(<<-TABLE) abc #123 firs... #0 empty diff --git a/spec/thor_spec.rb b/spec/thor_spec.rb index 16c88dce..6d03eb41 100644 --- a/spec/thor_spec.rb +++ b/spec/thor_spec.rb @@ -546,7 +546,7 @@ def shell end it "uses the maximum terminal size to show commands" do - expect(@shell).to receive(:terminal_width).and_return(80) + expect(Thor::Shell::Terminal).to receive(:terminal_width).and_return(80) content = capture(:stdout) { MyScript.help(shell) } expect(content).to match(/aaa\.\.\.$/) end