diff --git a/lib/reline.rb b/lib/reline.rb index ddb0224180..4ba74d2cb2 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -412,7 +412,7 @@ def ambiguous_width end private def may_req_ambiguous_char_width - @ambiguous_width = 2 if io_gate.dumb? || !STDIN.tty? || !STDOUT.tty? + @ambiguous_width = 1 if io_gate.dumb? || !STDIN.tty? || !STDOUT.tty? return if defined? @ambiguous_width io_gate.move_cursor_column(0) begin @@ -421,7 +421,7 @@ def ambiguous_width # LANG=C @ambiguous_width = 1 else - @ambiguous_width = io_gate.cursor_pos.x + @ambiguous_width = io_gate.cursor_pos.x == 2 ? 2 : 1 end io_gate.move_cursor_column(0) io_gate.erase_after_cursor diff --git a/lib/reline/io/ansi.rb b/lib/reline/io/ansi.rb index a730a953f7..82d2ee2371 100644 --- a/lib/reline/io/ansi.rb +++ b/lib/reline/io/ansi.rb @@ -245,39 +245,30 @@ def set_screen_size(rows, columns) self end - def cursor_pos - if both_tty? - res = +'' - m = nil - @input.raw do |stdin| - @output << "\e[6n" - @output.flush - loop do - c = stdin.getc - next if c.nil? - res << c - m = res.match(/\e\[(?\d+);(?\d+)R/) - break if m - end - (m.pre_match + m.post_match).chars.reverse_each do |ch| - stdin.ungetc ch + private def cursor_pos_internal(timeout:) + match = nil + @input.raw do |stdin| + @output << "\e[6n" + @output.flush + timeout_at = Time.now + timeout + buf = +'' + while (wait = timeout_at - Time.now) > 0 && stdin.wait_readable(wait) + buf << stdin.readpartial(1024) + if (match = buf.match(/\e\[(?\d+);(?\d+)R/)) + buf = match.pre_match + match.post_match + break end end - column = m[:column].to_i - 1 - row = m[:row].to_i - 1 - else - begin - buf = @output.pread(@output.pos, 0) - row = buf.count("\n") - column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0 - rescue Errno::ESPIPE, IOError - # Just returns column 1 for ambiguous width because this I/O is not - # tty and can't seek. - row = 0 - column = 1 + buf.chars.reverse_each do |ch| + stdin.ungetc ch end end - Reline::CursorPos.new(column, row) + [match[:column].to_i - 1, match[:row].to_i - 1] if match + end + + def cursor_pos + col, row = cursor_pos_internal(timeout: 0.5) if both_tty? + Reline::CursorPos.new(col || 0, row || 0) end def both_tty? diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index 515805467d..d4928c5374 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -1,4 +1,5 @@ require_relative 'helper' +require 'pty' require 'reline' require 'stringio' @@ -432,6 +433,34 @@ def win? /mswin|mingw/.match?(RUBY_PLATFORM) end + def test_tty_amibuous_width + ruby_file = Tempfile.create('rubyfile') + ruby_file.write(<<~RUBY) + require 'reline' + Thread.new { sleep 2; puts 'timeout'; exit } + p [Reline.ambiguous_width, gets.chomp] + RUBY + ruby_file.close + lib = File.expand_path('../../lib', __dir__) + cmd = [{ 'TERM' => 'xterm' }, 'ruby', '-I', lib, ruby_file.to_path] + + # Calculate ambiguous width from cursor position + [1, 2].each do |ambiguous_width| + PTY.spawn(*cmd) do |r, w| + loop { break if r.readpartial(1024).include?("\e[6n") } + w.puts "hello\e[10;#{ambiguous_width + 1}Rworld" + assert_include(r.gets, [ambiguous_width, 'helloworld'].inspect) + end + end + + # Ambiguous width = 1 when cursor pos timed out + PTY.spawn(*cmd) do |r, w| + loop { break if r.readpartial(1024).include?("\e[6n") } + w.puts "hello\e[10;2Sworld" + assert_include(r.gets, [1, "hello\e[10;2Sworld"].inspect) + end + end + def get_reline_encoding if encoding = Reline.core.encoding encoding diff --git a/test/reline/test_unicode.rb b/test/reline/test_unicode.rb index deba4d4681..818d9cfa98 100644 --- a/test/reline/test_unicode.rb +++ b/test/reline/test_unicode.rb @@ -15,7 +15,7 @@ def test_get_mbchar_width end def test_ambiguous_width - assert_equal 2, Reline::Unicode.calculate_width('√', true) + assert_equal 1, Reline::Unicode.calculate_width('√', true) end def test_csi_regexp