summaryrefslogtreecommitdiff
path: root/lib/reline.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/reline.rb')
-rw-r--r--lib/reline.rb299
1 files changed, 163 insertions, 136 deletions
diff --git a/lib/reline.rb b/lib/reline.rb
index 9746b35ac5..f3fd28b627 100644
--- a/lib/reline.rb
+++ b/lib/reline.rb
@@ -1,5 +1,4 @@
require 'io/console'
-require 'timeout'
require 'forwardable'
require 'reline/version'
require 'reline/config'
@@ -8,28 +7,26 @@ require 'reline/key_stroke'
require 'reline/line_editor'
require 'reline/history'
require 'reline/terminfo'
+require 'reline/face'
require 'rbconfig'
module Reline
+ # NOTE: For making compatible with the rb-readline gem
FILENAME_COMPLETION_PROC = nil
USERNAME_COMPLETION_PROC = nil
class ConfigEncodingConversionError < StandardError; end
- Key = Struct.new('Key', :char, :combined_char, :with_meta) do
- def match?(key)
- if key.instance_of?(Reline::Key)
- (key.char.nil? or char.nil? or char == key.char) and
- (key.combined_char.nil? or combined_char.nil? or combined_char == key.combined_char) and
- (key.with_meta.nil? or with_meta.nil? or with_meta == key.with_meta)
- elsif key.is_a?(Integer) or key.is_a?(Symbol)
- if not combined_char.nil? and combined_char == key
- true
- elsif combined_char.nil? and not char.nil? and char == key
- true
- else
- false
- end
+ Key = Struct.new(:char, :combined_char, :with_meta) do
+ def match?(other)
+ case other
+ when Reline::Key
+ (other.char.nil? or char.nil? or char == other.char) and
+ (other.combined_char.nil? or combined_char.nil? or combined_char == other.combined_char) and
+ (other.with_meta.nil? or with_meta.nil? or with_meta == other.with_meta)
+ when Integer, Symbol
+ (combined_char and combined_char == other) or
+ (combined_char.nil? and char and char == other)
else
false
end
@@ -37,7 +34,16 @@ module Reline
alias_method :==, :match?
end
CursorPos = Struct.new(:x, :y)
- DialogRenderInfo = Struct.new(:pos, :contents, :bg_color, :width, :height, :scrollbar, keyword_init: true)
+ DialogRenderInfo = Struct.new(
+ :pos,
+ :contents,
+ :face,
+ :bg_color, # For the time being, this line should stay here for the compatibility with IRB.
+ :width,
+ :height,
+ :scrollbar,
+ keyword_init: true
+ )
class Core
ATTR_READER_NAMES = %i(
@@ -62,52 +68,60 @@ module Reline
attr_accessor :last_incremental_search
attr_reader :output
+ extend Forwardable
+ def_delegators :config,
+ :autocompletion,
+ :autocompletion=
+
def initialize
self.output = STDOUT
- @dialog_proc_list = []
+ @dialog_proc_list = {}
yield self
@completion_quote_character = nil
- @bracketed_paste_finished = false
+ end
+
+ def io_gate
+ Reline::IOGate
end
def encoding
- Reline::IOGate.encoding
+ io_gate.encoding
end
def completion_append_character=(val)
if val.nil?
@completion_append_character = nil
elsif val.size == 1
- @completion_append_character = val.encode(Reline::IOGate.encoding)
+ @completion_append_character = val.encode(encoding)
elsif val.size > 1
- @completion_append_character = val[0].encode(Reline::IOGate.encoding)
+ @completion_append_character = val[0].encode(encoding)
else
@completion_append_character = nil
end
end
def basic_word_break_characters=(v)
- @basic_word_break_characters = v.encode(Reline::IOGate.encoding)
+ @basic_word_break_characters = v.encode(encoding)
end
def completer_word_break_characters=(v)
- @completer_word_break_characters = v.encode(Reline::IOGate.encoding)
+ @completer_word_break_characters = v.encode(encoding)
end
def basic_quote_characters=(v)
- @basic_quote_characters = v.encode(Reline::IOGate.encoding)
+ @basic_quote_characters = v.encode(encoding)
end
def completer_quote_characters=(v)
- @completer_quote_characters = v.encode(Reline::IOGate.encoding)
+ @completer_quote_characters = v.encode(encoding)
end
def filename_quote_characters=(v)
- @filename_quote_characters = v.encode(Reline::IOGate.encoding)
+ @filename_quote_characters = v.encode(encoding)
end
def special_prefixes=(v)
- @special_prefixes = v.encode(Reline::IOGate.encoding)
+ @special_prefixes = v.encode(encoding)
end
def completion_case_fold=(v)
@@ -127,14 +141,6 @@ module Reline
@completion_proc = p
end
- def autocompletion
- @config.autocompletion
- end
-
- def autocompletion=(val)
- @config.autocompletion = val
- end
-
def output_modifier_proc=(p)
raise ArgumentError unless p.respond_to?(:call) or p.nil?
@output_modifier_proc = p
@@ -159,28 +165,33 @@ module Reline
@dig_perfect_match_proc = p
end
+ DialogProc = Struct.new(:dialog_proc, :context)
def add_dialog_proc(name_sym, p, context = nil)
- raise ArgumentError unless p.respond_to?(:call) or p.nil?
raise ArgumentError unless name_sym.instance_of?(Symbol)
- @dialog_proc_list << [name_sym, p, context]
+ if p.nil?
+ @dialog_proc_list.delete(name_sym)
+ else
+ raise ArgumentError unless p.respond_to?(:call)
+ @dialog_proc_list[name_sym] = DialogProc.new(p, context)
+ end
+ end
+
+ def dialog_proc(name_sym)
+ @dialog_proc_list[name_sym]
end
def input=(val)
raise TypeError unless val.respond_to?(:getc) or val.nil?
- if val.respond_to?(:getc)
- if defined?(Reline::ANSI) and Reline::IOGate == Reline::ANSI
- Reline::ANSI.input = val
- elsif Reline::IOGate == Reline::GeneralIO
- Reline::GeneralIO.input = val
- end
+ if val.respond_to?(:getc) && io_gate.respond_to?(:input=)
+ io_gate.input = val
end
end
def output=(val)
raise TypeError unless val.respond_to?(:write) or val.nil?
@output = val
- if defined?(Reline::ANSI) and Reline::IOGate == Reline::ANSI
- Reline::ANSI.output = val
+ if io_gate.respond_to?(:output=)
+ io_gate.output = val
end
end
@@ -203,7 +214,7 @@ module Reline
end
def get_screen_size
- Reline::IOGate.get_screen_size
+ io_gate.get_screen_size
end
Reline::DEFAULT_DIALOG_PROC_AUTOCOMPLETE = ->() {
@@ -242,27 +253,42 @@ module Reline
context.push(cursor_pos_to_render, result, pointer, dialog)
end
dialog.pointer = pointer
- DialogRenderInfo.new(pos: cursor_pos_to_render, contents: result, scrollbar: true, height: 15)
+ DialogRenderInfo.new(
+ pos: cursor_pos_to_render,
+ contents: result,
+ scrollbar: true,
+ height: [15, preferred_dialog_height].min,
+ face: :completion_dialog
+ )
}
Reline::DEFAULT_DIALOG_CONTEXT = Array.new
def readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination)
- unless confirm_multiline_termination
- raise ArgumentError.new('#readmultiline needs block to confirm multiline termination')
- end
- inner_readline(prompt, add_hist, true, &confirm_multiline_termination)
+ Reline.update_iogate
+ io_gate.with_raw_input do
+ unless confirm_multiline_termination
+ raise ArgumentError.new('#readmultiline needs block to confirm multiline termination')
+ end
+ inner_readline(prompt, add_hist, true, &confirm_multiline_termination)
- whole_buffer = line_editor.whole_buffer.dup
- whole_buffer.taint if RUBY_VERSION < '2.7'
- if add_hist and whole_buffer and whole_buffer.chomp("\n").size > 0
- Reline::HISTORY << whole_buffer
- end
+ whole_buffer = line_editor.whole_buffer.dup
+ whole_buffer.taint if RUBY_VERSION < '2.7'
+ if add_hist and whole_buffer and whole_buffer.chomp("\n").size > 0
+ Reline::HISTORY << whole_buffer
+ end
- line_editor.reset_line if line_editor.whole_buffer.nil?
- whole_buffer
+ if line_editor.eof?
+ line_editor.reset_line
+ # Return nil if the input is aborted by C-d.
+ nil
+ else
+ whole_buffer
+ end
+ end
end
def readline(prompt = '', add_hist = false)
+ Reline.update_iogate
inner_readline(prompt, add_hist, false)
line = line_editor.line.dup
@@ -277,7 +303,7 @@ module Reline
private def inner_readline(prompt, add_hist, multiline, &confirm_multiline_termination)
if ENV['RELINE_STDERR_TTY']
- if Reline::IOGate.win?
+ if io_gate.win?
$stderr = File.open(ENV['RELINE_STDERR_TTY'], 'a')
else
$stderr.reopen(ENV['RELINE_STDERR_TTY'], 'w')
@@ -285,10 +311,10 @@ module Reline
$stderr.sync = true
$stderr.puts "Reline is used by #{Process.pid}"
end
- otio = Reline::IOGate.prep
+ otio = io_gate.prep
may_req_ambiguous_char_width
- line_editor.reset(prompt, encoding: Reline::IOGate.encoding)
+ line_editor.reset(prompt, encoding: encoding)
if multiline
line_editor.multiline_on
if block_given?
@@ -304,53 +330,54 @@ module Reline
line_editor.prompt_proc = prompt_proc
line_editor.auto_indent_proc = auto_indent_proc
line_editor.dig_perfect_match_proc = dig_perfect_match_proc
- line_editor.pre_input_hook = pre_input_hook
- @dialog_proc_list.each do |d|
- name_sym, dialog_proc, context = d
- line_editor.add_dialog_proc(name_sym, dialog_proc, context)
+ pre_input_hook&.call
+ unless Reline::IOGate == Reline::GeneralIO
+ @dialog_proc_list.each_pair do |name_sym, d|
+ line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context)
+ end
end
unless config.test_mode
config.read
config.reset_default_key_bindings
- Reline::IOGate.set_default_key_bindings(config)
+ io_gate.set_default_key_bindings(config)
end
+ line_editor.print_nomultiline_prompt(prompt)
+ line_editor.update_dialogs
line_editor.rerender
begin
- prev_pasting_state = false
+ line_editor.set_signal_handlers
loop do
- prev_pasting_state = Reline::IOGate.in_pasting?
read_io(config.keyseq_timeout) { |inputs|
- line_editor.set_pasting_state(Reline::IOGate.in_pasting?)
- inputs.each { |c|
- line_editor.input_key(c)
- line_editor.rerender
- }
- if @bracketed_paste_finished
- line_editor.rerender_all
- @bracketed_paste_finished = false
- end
+ line_editor.set_pasting_state(io_gate.in_pasting?)
+ inputs.each { |key| line_editor.update(key) }
}
- if prev_pasting_state == true and not Reline::IOGate.in_pasting? and not line_editor.finished?
- line_editor.set_pasting_state(false)
- prev_pasting_state = false
- line_editor.rerender_all
+ if line_editor.finished?
+ line_editor.render_finished
+ break
+ else
+ line_editor.set_pasting_state(io_gate.in_pasting?)
+ line_editor.rerender
end
- break if line_editor.finished?
end
- Reline::IOGate.move_cursor_column(0)
+ io_gate.move_cursor_column(0)
rescue Errno::EIO
# Maybe the I/O has been closed.
rescue StandardError => e
line_editor.finalize
- Reline::IOGate.deprep(otio)
+ io_gate.deprep(otio)
raise e
+ rescue Exception
+ # Including Interrupt
+ line_editor.finalize
+ io_gate.deprep(otio)
+ raise
end
line_editor.finalize
- Reline::IOGate.deprep(otio)
+ io_gate.deprep(otio)
end
# GNU Readline waits for "keyseq-timeout" milliseconds to see if the ESC
@@ -365,10 +392,9 @@ module Reline
private def read_io(keyseq_timeout, &block)
buffer = []
loop do
- c = Reline::IOGate.getc
+ c = io_gate.getc(Float::INFINITY)
if c == -1
result = :unmatched
- @bracketed_paste_finished = true
else
buffer << c
result = key_stroke.match_status(buffer)
@@ -402,15 +428,8 @@ module Reline
end
private def read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block)
- begin
- succ_c = nil
- Timeout.timeout(keyseq_timeout / 1000.0) {
- succ_c = Reline::IOGate.getc
- }
- rescue Timeout::Error # cancel matching only when first byte
- block.([Reline::Key.new(c, c, false)])
- return :break
- else
+ succ_c = io_gate.getc(keyseq_timeout.fdiv(1000))
+ if succ_c
case key_stroke.match_status(buffer.dup.push(succ_c))
when :unmatched
if c == "\e".ord
@@ -420,7 +439,7 @@ module Reline
end
return :break
when :matching
- Reline::IOGate.ungetc(succ_c)
+ io_gate.ungetc(succ_c)
return :next
when :matched
buffer << succ_c
@@ -430,27 +449,23 @@ module Reline
block.(expanded)
return :break
end
+ else
+ block.([Reline::Key.new(c, c, false)])
+ return :break
end
end
private def read_escaped_key(keyseq_timeout, c, block)
- begin
- escaped_c = nil
- Timeout.timeout(keyseq_timeout / 1000.0) {
- escaped_c = Reline::IOGate.getc
- }
- rescue Timeout::Error # independent ESC
+ escaped_c = io_gate.getc(keyseq_timeout.fdiv(1000))
+
+ if escaped_c.nil?
block.([Reline::Key.new(c, c, false)])
+ elsif escaped_c >= 128 # maybe, first byte of multi byte
+ block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)])
+ elsif escaped_c == "\e".ord # escape twice
+ block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)])
else
- if escaped_c.nil?
- block.([Reline::Key.new(c, c, false)])
- elsif escaped_c >= 128 # maybe, first byte of multi byte
- block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)])
- elsif escaped_c == "\e".ord # escape twice
- block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)])
- else
- block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)])
- end
+ block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)])
end
end
@@ -460,19 +475,19 @@ module Reline
end
private def may_req_ambiguous_char_width
- @ambiguous_width = 2 if Reline::IOGate == Reline::GeneralIO or STDOUT.is_a?(File)
- return if @ambiguous_width
- Reline::IOGate.move_cursor_column(0)
+ @ambiguous_width = 2 if io_gate == Reline::GeneralIO or !STDOUT.tty?
+ return if defined? @ambiguous_width
+ io_gate.move_cursor_column(0)
begin
output.write "\u{25bd}"
rescue Encoding::UndefinedConversionError
# LANG=C
@ambiguous_width = 1
else
- @ambiguous_width = Reline::IOGate.cursor_pos.x
+ @ambiguous_width = io_gate.cursor_pos.x
end
- Reline::IOGate.move_cursor_column(0)
- Reline::IOGate.erase_after_cursor
+ io_gate.move_cursor_column(0)
+ io_gate.erase_after_cursor
end
end
@@ -520,6 +535,7 @@ module Reline
def_single_delegators :core, :last_incremental_search
def_single_delegators :core, :last_incremental_search=
def_single_delegators :core, :add_dialog_proc
+ def_single_delegators :core, :dialog_proc
def_single_delegators :core, :autocompletion, :autocompletion=
def_single_delegators :core, :readmultiline
@@ -534,7 +550,7 @@ module Reline
@core ||= Core.new { |core|
core.config = Reline::Config.new
core.key_stroke = Reline::KeyStroke.new(core.config)
- core.line_editor = Reline::LineEditor.new(core.config, Reline::IOGate.encoding)
+ core.line_editor = Reline::LineEditor.new(core.config, core.encoding)
core.basic_word_break_characters = " \t\n`><=;|&{("
core.completer_word_break_characters = " \t\n`><=;|&{("
@@ -547,33 +563,44 @@ module Reline
end
def self.ungetc(c)
- Reline::IOGate.ungetc(c)
+ core.io_gate.ungetc(c)
end
def self.line_editor
core.line_editor
end
-end
-require 'reline/general_io'
-if RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
- require 'reline/windows'
- if Reline::Windows.msys_tty?
- Reline::IOGate = if ENV['TERM'] == 'dumb'
- Reline::GeneralIO
- else
+ def self.update_iogate
+ return if core.config.test_mode
+
+ # Need to change IOGate when `$stdout.tty?` change from false to true by `$stdout.reopen`
+ # Example: rails/spring boot the application in non-tty, then run console in tty.
+ if ENV['TERM'] != 'dumb' && core.io_gate == Reline::GeneralIO && $stdout.tty?
require 'reline/ansi'
- Reline::ANSI
+ remove_const(:IOGate)
+ const_set(:IOGate, Reline::ANSI)
end
- else
- Reline::IOGate = Reline::Windows
end
-else
- Reline::IOGate = if $stdout.isatty
- require 'reline/ansi'
- Reline::ANSI
+end
+
+require 'reline/general_io'
+io = Reline::GeneralIO
+unless ENV['TERM'] == 'dumb'
+ case RbConfig::CONFIG['host_os']
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
+ require 'reline/windows'
+ tty = (io = Reline::Windows).msys_tty?
else
- Reline::GeneralIO
+ tty = $stdout.tty?
end
end
+Reline::IOGate = if tty
+ require 'reline/ansi'
+ Reline::ANSI
+else
+ io
+end
+
+Reline::Face.load_initial_configs
+
Reline::HISTORY = Reline::History.new(Reline.core.config)