Merge Pry::Terminal with Pry::Output

Fixes #1991 (Pry in a non-stdin/stdout PTY uses incorrect window size on
non-JRuby platforms)

`Pry::Terminal` was built without custom outputs in mind. We would always assume
that `$stdout` is what the user wants. This contradicted the `output` config
option.

Thanks to `Pry::Output`, which we use internally, we can decorate the output
that the user passes us with "size" methods. If we do that, we get improved
output support for free, so that PTY's `slave` can be passed to Pry and would be
able to determine its size correctly (example from #1991).

I do suspect that there are still some gotchas. Some commands or portions of
code may still be assuming that `$stdout` is the only possible option. This has
to be addressed separately, in the scope of
https://github.com/pry/pry/issues/1988. The more tests we add, the easier it
will be to uncover those spots.
This commit is contained in:
Kyrylo Silin 2019-05-26 10:42:52 +03:00
parent 28cf3e9fa8
commit 67b0b53f0b
14 changed files with 276 additions and 112 deletions

View File

@ -86,6 +86,8 @@
([#1898](https://github.com/pry/pry/pull/1898))
* Fixed Ruby 2.6 warning about `Binding#source_location`
([#1904](https://github.com/pry/pry/pull/1904))
* Fixed wrong `winsize` when custom `output` is passed to Pry
([#2045](https://github.com/pry/pry/pull/2045))
### [v0.12.2][v0.12.2] (November 12, 2018)

View File

@ -58,7 +58,6 @@ require 'pry/pry_class'
require 'pry/pry_instance'
require 'pry/inspector'
require 'pry/pager'
require 'pry/terminal'
require 'pry/indent'
require 'pry/object_path'
require 'pry/output'

View File

@ -11,7 +11,7 @@ class Pry
def self.default(_output, value, pry_instance)
pry_instance.pager.open do |pager|
pager.print pry_instance.config.output_prefix
pp(value, pager, Pry::Terminal.width - 1)
pp(value, pager, pry_instance.output.width - 1)
end
end

View File

@ -31,7 +31,7 @@ class Pry
return '' if body.compact.empty?
fancy_heading = Pry::Helpers::Text.bold(color(:heading, heading))
Pry::Helpers.tablify_or_one_line(fancy_heading, body, @pry_instance.config)
Pry::Helpers.tablify_or_one_line(fancy_heading, body, @pry_instance)
end
def format_value(value)

View File

@ -2,29 +2,30 @@
class Pry
module Helpers
def self.tablify_or_one_line(heading, things, config = Pry.config)
def self.tablify_or_one_line(heading, things, pry_instance = Pry.new)
plain_heading = Pry::Helpers::Text.strip_color(heading)
attempt = Table.new(things, column_count: things.size)
if attempt.fits_on_line?(Terminal.width - plain_heading.size - 2)
if attempt.fits_on_line?(pry_instance.output.width - plain_heading.size - 2)
"#{heading}: #{attempt}\n"
else
"#{heading}: \n#{tablify_to_screen_width(things, { indent: ' ' }, config)}\n"
content = tablify_to_screen_width(things, { indent: ' ' }, pry_instance)
"#{heading}: \n#{content}\n"
end
end
def self.tablify_to_screen_width(things, options, config = Pry.config)
def self.tablify_to_screen_width(things, options, pry_instance = Pry.new)
options ||= {}
things = things.compact
if (indent = options[:indent])
usable_width = Terminal.width - indent.size
tablify(things, usable_width, config).to_s.gsub(/^/, indent)
usable_width = pry_instance.output.width - indent.size
tablify(things, usable_width, pry_instance).to_s.gsub(/^/, indent)
else
tablify(things, Terminal.width, config).to_s
tablify(things, pry_instance.output.width, pry_instance).to_s
end
end
def self.tablify(things, line_length, config = Pry.config)
table = Table.new(things, { column_count: things.size }, config)
def self.tablify(things, line_length, pry_instance = Pry.new)
table = Table.new(things, { column_count: things.size }, pry_instance)
until (table.column_count == 1) || table.fits_on_line?(line_length)
table.column_count -= 1
end
@ -33,9 +34,9 @@ class Pry
class Table
attr_reader :items, :column_count
def initialize(items, args, config = Pry.config)
def initialize(items, args, pry_instance = Pry.new)
@column_count = args[:column_count]
@config = config
@config = pry_instance.config
self.items = items
end

View File

@ -101,7 +101,8 @@ class Pry
indent.module_nesting
end
def initialize
def initialize(pry_instance = Pry.new)
@pry_instance = pry_instance
reset
end
@ -394,7 +395,7 @@ class Pry
line_to_measure = Pry::Helpers::Text.strip_color(prompt) << code
whitespace = ' ' * overhang
cols = Terminal.width
cols = @pry_instance.output.width
lines = cols == 0 ? 1 : (line_to_measure.length / cols + 1).to_i
if Helpers::Platform.windows_ansi?

View File

@ -2,6 +2,9 @@
class Pry
class Output
# @return [Array<Integer>] default terminal screen size [rows, cols]
DEFAULT_SIZE = [27, 80].freeze
attr_reader :pry_instance
def initialize(pry_instance)
@ -52,5 +55,82 @@ class Pry
Pry::Helpers::Text.strip_color(str)
end
# @return [Array<Integer>] a pair of [rows, columns] which gives the size of
# the window. If the window size cannot be determined, the default value.
def size
rows, cols = actual_screen_size
return [rows.to_i, cols.to_i] if rows.to_i != 0 && cols.to_i != 0
DEFAULT_SIZE
end
# Return a screen width or the default if that fails.
def width
size.last
end
# Return a screen height or the default if that fails.
def height
size.first
end
private
def actual_screen_size
# The best way, if possible (requires non-jruby >=1.9 or io-console gem).
io_console_size ||
# Fall back to the old standby, though it might be stale.
env_size ||
# Fall further back, though this one is also out of date without
# something calling Readline.set_screen_size.
readline_size ||
# Windows users can otherwise run ansicon and get a decent answer.
ansicon_env_size
end
def io_console_size
return if Pry::Helpers::Platform.jruby?
begin
require 'io/console'
begin
@output.winsize if tty? && @output.respond_to?(:winsize)
rescue Errno::EOPNOTSUPP # rubocop:disable Lint/HandleExceptions
# Output is probably a socket, which doesn't support #winsize.
end
rescue LoadError # rubocop:disable Lint/HandleExceptions
# They probably don't have the io/console stdlib or the io-console gem.
# We'll keep trying.
end
end
def env_size
size = [ENV['LINES'] || ENV['ROWS'], ENV['COLUMNS']]
size if nonzero_column?(size)
end
def readline_size
return unless defined?(Readline) && Readline.respond_to?(:get_screen_size)
size = Readline.get_screen_size
size if nonzero_column?(size)
rescue Java::JavaLang::NullPointerException
# This rescue won't happen on jrubies later than:
# https://github.com/jruby/jruby/pull/436
nil
end
def ansicon_env_size
return unless ENV['ANSICON'] =~ /\((.*)x(.*)\)/
size = [Regexp.last_match(2), Regexp.last_match(1)]
size if nonzero_column?(size)
end
def nonzero_column?(size)
size[1].to_i > 0
end
end
end

View File

@ -88,11 +88,11 @@ class Pry
private
def height
@height ||= Pry::Terminal.height
@height ||= @out.height
end
def width
@width ||= Pry::Terminal.width
@width ||= @out.width
end
end

View File

@ -300,7 +300,7 @@ Readline version #{Readline::VERSION} detected - will not auto_resize! correctly
trap :WINCH do
begin
Readline.set_screen_size(*Terminal.size)
Readline.set_screen_size(*output.size)
rescue StandardError => e
warn "\nPry.auto_resize!'s Readline.set_screen_size failed: #{e}"
end

View File

@ -80,7 +80,7 @@ class Pry
# The initial context for this session.
def initialize(options = {})
@binding_stack = []
@indent = Pry::Indent.new
@indent = Pry::Indent.new(self)
@eval_string = ''.dup
@backtrace = options.delete(:backtrace) || caller
target = options.delete(:target)

View File

@ -21,7 +21,7 @@ class Pry
# @option options [Object] :target The initial target of the session.
def initialize(pry, options = {})
@pry = pry
@indent = Pry::Indent.new
@indent = Pry::Indent.new(pry)
@readline_output = nil

View File

@ -1,90 +0,0 @@
# frozen_string_literal: true
class Pry
class Terminal
class << self
# Return a pair of [rows, columns] which gives the size of the window.
#
# If the window size cannot be determined, return nil.
def screen_size
rows, cols = actual_screen_size
[rows.to_i, cols.to_i] if rows.to_i != 0 && cols.to_i != 0
end
# Return a screen size or a default if that fails.
def size(default = [27, 80])
screen_size || default
end
# Return a screen width or the default if that fails.
def width
size[1]
end
# Return a screen height or the default if that fails.
def height
size[0]
end
def actual_screen_size
# The best way, if possible (requires non-jruby >=1.9 or io-console gem)
screen_size_according_to_io_console ||
# Fall back to the old standby, though it might be stale:
screen_size_according_to_env ||
# Fall further back, though this one is also out of date without something
# calling Readline.set_screen_size
screen_size_according_to_readline ||
# Windows users can otherwise run ansicon and get a decent answer:
screen_size_according_to_ansicon_env
end
def screen_size_according_to_io_console
return if Pry::Helpers::Platform.jruby?
begin
require 'io/console'
begin
if $stdout.respond_to?(:tty?) && $stdout.tty? && $stdout.respond_to?(:winsize)
$stdout.winsize
end
rescue Errno::EOPNOTSUPP # rubocop:disable Lint/HandleExceptions
# $stdout is probably a socket, which doesn't support #winsize.
end
rescue LoadError # rubocop:disable Lint/HandleExceptions
# They probably don't have the io/console stdlib or the io-console gem.
# We'll keep trying.
end
end
def screen_size_according_to_env
size = [ENV['LINES'] || ENV['ROWS'], ENV['COLUMNS']]
size if nonzero_column?(size)
end
def screen_size_according_to_readline
if defined?(Readline) && Readline.respond_to?(:get_screen_size)
size = Readline.get_screen_size
size if nonzero_column?(size)
end
rescue Java::JavaLang::NullPointerException
# This rescue won't happen on jrubies later than:
# https://github.com/jruby/jruby/pull/436
nil
end
def screen_size_according_to_ansicon_env
return unless ENV['ANSICON'] =~ /\((.*)x(.*)\)/
size = [Regexp.last_match(2), Regexp.last_match(1)]
size if nonzero_column?(size)
end
private
def nonzero_column?(size)
size[1].to_i > 0
end
end
end
end

View File

@ -5,7 +5,7 @@
# lines.
describe Pry::Indent do
before do
@indent = Pry::Indent.new
@indent = Pry::Indent.new(Pry.new)
end
it 'should indent an array' do

View File

@ -194,4 +194,175 @@ RSpec.describe Pry::Output do
end
end
end
describe "#size" do
context "when the output is a tty and responds to winsize" do
before do
skip("io/console doesn't support JRuby") if Pry::Helpers::Platform.jruby?
expect(output).to receive(:tty?).and_return(true)
expect(output).to receive(:winsize).and_return([1, 1])
end
it "returns the io/console winsize" do
expect(subject.size).to eq([1, 1])
end
end
context "when the output is not a tty" do
before do
skip("io/console doesn't support JRuby") if Pry::Helpers::Platform.jruby?
expect(output).to receive(:tty?).and_return(false)
allow(ENV).to receive(:[])
end
context "and ENV has size info in ROWS and COLUMNS" do
before do
expect(ENV).to receive(:[]).with('ROWS').and_return(2)
expect(ENV).to receive(:[]).with('COLUMNS').and_return(2)
end
it "returns the ENV variable winsize" do
expect(subject.size).to eq([2, 2])
end
end
context "and ENV has size info in LINES and COLUMNS" do
before do
expect(ENV).to receive(:[]).with('LINES').and_return(3)
expect(ENV).to receive(:[]).with('COLUMNS').and_return(2)
end
it "returns ENV variable winsize" do
expect(subject.size).to eq([3, 2])
end
end
end
context "when the output is not a tty and no info in ENV" do
let(:readline) { Object.new }
before do
unless Pry::Helpers::Platform.jruby?
expect(output).to receive(:tty?).and_return(false)
end
allow(ENV).to receive(:[])
stub_const('Readline', readline)
end
context "when Readline's size has no zeroes" do
before do
expect(readline).to receive(:get_screen_size).and_return([1, 1])
end
it "returns the Readline winsize" do
expect(subject.size).to eq([1, 1])
end
end
context "when Readline's size has zero column" do
before do
expect(readline).to receive(:get_screen_size).and_return([1, 0])
end
it "returns the default size" do
expect(subject.size).to eq([27, 80])
end
end
end
context "when the output is not a tty, and no info in ENV and no Readline info" do
let(:readline) { Object.new }
before do
unless Pry::Helpers::Platform.jruby?
expect(output).to receive(:tty?).and_return(false)
end
allow(ENV).to receive(:[])
stub_const('Readline', readline)
expect(readline).to receive(:respond_to?)
.with(:get_screen_size).and_return(false)
end
context "and when there's ANSICON ENV variable" do
context "and when it can be matched" do
context "and when the size consists of positive integers" do
before do
expect(ENV).to receive(:[]).with('ANSICON').and_return('(5x5)')
end
it "returns the ansicon winsize" do
expect(subject.size).to eq([5, 5])
end
end
context "and when the size has a zero column" do
before do
expect(ENV).to receive(:[]).with('ANSICON').and_return('(0x0)')
end
it "returns the default winsize" do
expect(subject.size).to eq([27, 80])
end
end
end
context "and when it cannot be matched" do
before do
expect(ENV).to receive(:[]).with('ANSICON').and_return('5x5')
end
it "returns the default winsize" do
expect(subject.size).to eq([27, 80])
end
end
end
context "and when there's no ANSICON ENV variable" do
it "returns the default winsize" do
expect(subject.size).to eq([27, 80])
end
end
end
end
describe "#width" do
let(:readline) { Object.new }
before do
unless Pry::Helpers::Platform.jruby?
expect(output).to receive(:tty?).and_return(false)
end
allow(ENV).to receive(:[])
stub_const('Readline', readline)
expect(readline).to receive(:respond_to?)
.with(:get_screen_size).and_return(false)
end
it "returns the number of columns" do
expect(subject.width).to eq(80)
end
end
describe "#height" do
let(:readline) { Object.new }
before do
unless Pry::Helpers::Platform.jruby?
expect(output).to receive(:tty?).and_return(false)
end
allow(ENV).to receive(:[])
stub_const('Readline', readline)
expect(readline).to receive(:respond_to?)
.with(:get_screen_size).and_return(false)
end
it "returns the number of rows" do
expect(subject.height).to eq(27)
end
end
end