Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed ^A/^B and added support for Windows ConPty. #2209

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,15 @@

* Fixed bug where reading from the `_out_` sticky local variable could return
wrong results ([#2201](https://github.com/pry/pry/pull/2201))
* Fixed issue with printing control characters `\001`/`^A` and `\002`/`^B`. ([#2209](https://github.com/pry/pry/pull/2209))
\
Added `Pry.config.escape_prompt` to manage adding those control characters to the prompt (the default is `true`).
It can then be disabled for terminals that don't have readline, or where readline's version doesn't support those characters.
\
Added support for [ConPty](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, ConPTY is not the name we use for the version of the console that supports VT/control sequences.

ENABLE_VIRTUAL_TERMINAL_PROCESSING enables VT sequences on a fairly old version of Windows 10 -- something like "10.0.10240". That's the only thing you'll need to check. 😄

ConPTY is a newer API that lets somebody else display the UI for the console, instead of leaving us to do it. That's how somebody like alacritty or hyper can run pry! It is much newer (Windows 10 version 17763 or higher.)

Fortunately, you only need the first thing.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Happy to answer any questions about it! I'm the engineering lead for the console and Windows Terminal. 👋)

in Windows, which supports Ansi escapes natively.
\
Fixed all unit tests to run in Windows.

### [v0.14.1][v0.14.1] (April 12, 2021)

Expand Down
9 changes: 2 additions & 7 deletions lib/pry/commands/easter_eggs.rb
Expand Up @@ -83,15 +83,10 @@ class Pry
\____/ \________________________|
OUTPUT

move_up =
if Helpers::Platform.windows_ansi?
proc { |n| "\e[#{n}F" }
else
proc { |n| "\e[#{n}A\e[0G" }
end
move_up = "\e[F"

output.puts "\n" * 6
output.puts picture.lines.map(&:chomp).reverse.join(move_up[1])
output.puts picture.lines.map(&:chomp).reverse.join(move_up)
output.puts "\n" * 6
output.puts "** ENV['TERM'] is #{ENV['TERM']} **\n\n"

Expand Down
4 changes: 4 additions & 0 deletions lib/pry/config.rb
Expand Up @@ -67,6 +67,9 @@ class Config
# @return [Boolean]
attribute :pager

# @return [Boolean]
attribute :escape_prompt

# @return [Boolean] whether the global ~/.pryrc should be loaded
attribute :should_load_rc

Expand Down Expand Up @@ -177,6 +180,7 @@ def initialize

hooks: Pry::Hooks.default,
pager: true,
escape_prompt: true,
system: Pry::SystemCommandHandler.method(:default),
color: Pry::Helpers::BaseHelpers.use_ansi_codes?,
default_window_size: 5,
Expand Down
8 changes: 6 additions & 2 deletions lib/pry/helpers/base_helpers.rb
Expand Up @@ -36,8 +36,12 @@ def not_a_real_file?(file)
end

def use_ansi_codes?
Pry::Helpers::Platform.windows_ansi? ||
((term = Pry::Env['TERM']) && term != "dumb")
Pry::Helpers::Platform.windows_ansi? || smart_term?
end

def smart_term?
term = Pry::Env['TERM']
term != nil && term != "dumb"
end

def colorize_code(code)
Expand Down
41 changes: 38 additions & 3 deletions lib/pry/helpers/platform.rb
Expand Up @@ -24,11 +24,46 @@ def self.windows?
!!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/)
end

# Checks older version of Windows console that required alternative
# libraries to work with Ansi escapes codes.
# @return [Boolean]
def self.windows_ansi?
return false unless windows?
# ensures that ConPty isn't available before checking anything else
windows? && !!(windows_conpty? || defined?(Win32::Console) || Pry::Env['ANSICON'] || mri_2?)
end

# New version of Windows console that understands Ansi escapes codes.
# @return [Boolean]
def self.windows_conpty?
@conpty ||= windows? && begin
require 'fiddle/import'
require 'fiddle/types'

kernel32 = Module.new do
extend Fiddle::Importer
dlload 'kernel32'
include Fiddle::Win32Types
extern 'HANDLE GetStdHandle(DWORD)'
extern 'BOOL GetConsoleMode(HANDLE, DWORD*)'
end

mode = kernel32.create_value('DWORD')

std_output_handle = -11
enable_virtual_terminal_processing = 0x4

stdout_handle = kernel32.GetStdHandle(std_output_handle)

stdout_handle > 0 &&
kernel32.GetConsoleMode(stdout_handle, mode) != 0 &&
mode.value & enable_virtual_terminal_processing != 0

!!(defined?(Win32::Console) || Pry::Env['ANSICON'] || mri_2?)
rescue LoadError, Fiddle::DLError
false
ensure
Fiddle.free mode.to_ptr if mode
kernel32.handler.handlers.each(&:close) if kernel32
end
end

# @return [Boolean]
Expand All @@ -53,7 +88,7 @@ def self.mri_19?

# @return [Boolean]
def self.mri_2?
mri? && RUBY_VERSION.start_with?('2')
mri? && RUBY_VERSION.start_with?('2.')
end
end
end
Expand Down
62 changes: 40 additions & 22 deletions lib/pry/helpers/text.rb
Expand Up @@ -7,52 +7,70 @@ module Helpers
module Text
extend self

DECORATIONS = {
"bold" => '1',
"faint" => '2',
"italic" => '3',
"underline" => '4',
"blink" => '5',
"reverse" => '7'
}.freeze

COLORS = {
"black" => 0,
"red" => 1,
"green" => 2,
"yellow" => 3,
"blue" => 4,
"purple" => 5,
"magenta" => 5,
"cyan" => 6,
"white" => 7
"black" => '0',
"red" => '1',
"green" => '2',
"yellow" => '3',
"blue" => '4',
"purple" => '5',
"magenta" => '5',
"cyan" => '6',
"white" => '7'
}.freeze

DECORATIONS.each_pair do |decoration, color_code|
define_method decoration do |text|
escape_text(color_code, text)
end
end

COLORS.each_pair do |color, value|
color_code = "0;3#{value}"
define_method color do |text|
"\001\033[0;#{30 + value}m\002#{text}\001\033[0m\002"
escape_text(color_code, text)
end

bright_color_code = "1;3#{value}"
define_method "bright_#{color}" do |text|
"\001\033[1;#{30 + value}m\002#{text}\001\033[0m\002"
escape_text(bright_color_code, text)
end

COLORS.each_pair do |bg_color, bg_value|
bg_color_code = "#{color_code};4#{bg_value}"
define_method "#{color}_on_#{bg_color}" do |text|
"\001\033[0;#{30 + value};#{40 + bg_value}m\002#{text}\001\033[0m\002"
escape_text(bg_color_code, text)
end

bright_bg_color_code = "#{bright_color_code};4#{bg_value}"
define_method "bright_#{color}_on_#{bg_color}" do |text|
"\001\033[1;#{30 + value};#{40 + bg_value}m\002#{text}\001\033[0m\002"
escape_text(bright_bg_color_code, text)
end
end
end

# @param color_code [String]
# @param text [String]
# @return [String]
def escape_text(color_code, text)
"\e[#{color_code}m#{text}\e[0m"
end

# Remove any color codes from _text_.
#
# @param [String, #to_s] text
# @return [String] _text_ stripped of any color codes.
def strip_color(text)
text.to_s.gsub(/(\001)?(\e\[(\d[;\d]?)*m)(\002)?/, '')
end

# Returns _text_ as bold text for use on a terminal.
#
# @param [String, #to_s] text
# @return [String] _text_
def bold(text)
"\001\e[1m\002#{text}\001\e[0m\002"
text.to_s.gsub(/\001|\002|\e\[[\d;]*m/, '')
end

# Returns `text` in the default foreground colour.
Expand Down
29 changes: 0 additions & 29 deletions lib/pry/indent.rb
Expand Up @@ -9,7 +9,6 @@ class Pry
# will be indented or un-indented by correctly.
#
class Indent
include Helpers::BaseHelpers

# Raised if {#module_nesting} would not work.
class UnparseableNestingError < StandardError; end
Expand Down Expand Up @@ -380,33 +379,5 @@ def module_nesting
"#{kind} #{token}"
end
end

# Return a string which, when printed, will rewrite the previous line with
# the correct indentation. Mostly useful for fixing 'end'.
#
# @param [String] prompt The user's prompt
# @param [String] code The code the user just typed in
# @param [Integer] overhang The number of characters to erase afterwards (the
# the difference in length between the old line and the new one)
#
# @return [String] correctly indented line
def correct_indentation(prompt, code, overhang = 0)
prompt = prompt.delete("\001\002")
line_to_measure = Pry::Helpers::Text.strip_color(prompt) << code
whitespace = ' ' * overhang

cols = @pry_instance.output.width
lines = cols == 0 ? 1 : (line_to_measure.length / cols + 1).to_i

if Helpers::Platform.windows_ansi?
move_up = "\e[#{lines}F"
move_down = "\e[#{lines}E"
else
move_up = "\e[#{lines}A\e[0G"
move_down = "\e[#{lines}B\e[0G"
end

"#{move_up}#{prompt}#{colorize_code(code)}#{whitespace}#{move_down}"
end
end
end
12 changes: 10 additions & 2 deletions lib/pry/output.rb
Expand Up @@ -5,8 +5,6 @@ class Output
# @return [Array<Integer>] default terminal screen size [rows, cols]
DEFAULT_SIZE = [27, 80].freeze

attr_reader :pry_instance

def initialize(pry_instance)
@output = pry_instance.config.output
@color = pry_instance.config.color
Expand Down Expand Up @@ -75,6 +73,16 @@ def height
size.first
end

# Returns the number of lines it takes to fill a specified length.
#
# @param [Integer] length The length to check the number of lines
#
# @return [Integer] number of lines that the specified length occupies
def calculate_num_lines(length)
*, cols = size
cols == nil || cols == 0 ? 1 : (length / cols + 1).to_i
end

private

def actual_screen_size
Expand Down
2 changes: 1 addition & 1 deletion lib/pry/pager.rb
Expand Up @@ -144,7 +144,7 @@ def self.available?
@system_pager =
begin
pager_executable = default_pager.split(' ').first
if Helpers::Platform.windows? || Helpers::Platform.windows_ansi?
if Helpers::Platform.windows?
`where /Q #{pager_executable}`
else
`which #{pager_executable}`
Expand Down
5 changes: 4 additions & 1 deletion lib/pry/pry_class.rb
Expand Up @@ -143,7 +143,10 @@ def self.final_session_setup
load_requires if Pry.config.should_load_requires
load_history if Pry.config.history_load
load_traps if Pry.config.should_trap_interrupts
load_win32console if Helpers::Platform.windows? && !Helpers::Platform.windows_ansi?

windows_no_ansi = Helpers::Platform.windows? && !Helpers::Platform.windows_ansi?

load_win32console if windows_no_ansi
end

# Start a Pry REPL.
Expand Down