Skip to content

Commit b99c17a

Browse files
YO4matzbot
authored andcommitted
[ruby/reline] Windows fix (ruby/reline#775)
* test_yamatanooroti: close tempfile before unlink * test_yamatanooroti: omit because of windows does not support job control * test_yamatanooroti: change startup message detection for windows * windows.rb: can call win32api using nil as NULL for pointer argument Exception occurred when interrupted with Ctrl+C on legacy conhost * windows.rb: fix get_screen_size return [window height, buffer width] insted of [buffer height, buffer width] * windows.rb: import scroll_down() from ansi.rb * windows.rb: add auto linewrap control if VT output not supported (legacy console) * unfreeze WIN32API pointer arguments They internally duplicate arguments so api functions write to another place. This breaks the console mode detection with ruby-head. * remove useless code from Win32API#call argument repacking and return value tweaking is not needed for Reline::Windows requirements. * Correctly handle top of console viewport * Revert "remove useless code from Win32API#call" This reverts commit ruby/reline@060ba140ed43. * Revert "windows.rb: can call win32api using nil as NULL for pointer argument" This reverts commit ruby/reline@93a23bc5d0c9. ruby/reline@47c1ffbabe
1 parent 9a9a586 commit b99c17a

File tree

3 files changed

+74
-59
lines changed

3 files changed

+74
-59
lines changed

lib/reline/io/windows.rb

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def call(*args)
157157
STD_OUTPUT_HANDLE = -11
158158
FILE_TYPE_PIPE = 0x0003
159159
FILE_NAME_INFO = 2
160+
ENABLE_WRAP_AT_EOL_OUTPUT = 2
160161
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
161162

162163
# Calling Win32API with console handle is reported to fail after executing some external command.
@@ -170,7 +171,7 @@ def call(*args)
170171
end
171172

172173
private def getconsolemode
173-
mode = "\000\000\000\000"
174+
mode = +"\0\0\0\0"
174175
call_with_console_handle(@GetConsoleMode, mode)
175176
mode.unpack1('L')
176177
end
@@ -344,94 +345,78 @@ def get_console_screen_buffer_info
344345
# [18,2] dwMaximumWindowSize.X
345346
# [20,2] dwMaximumWindowSize.Y
346347
csbi = 0.chr * 22
347-
return if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) == 0
348-
csbi
348+
if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) != 0
349+
# returns [width, height, x, y, attributes, left, top, right, bottom]
350+
csbi.unpack("s9")
351+
else
352+
return nil
353+
end
349354
end
350355

356+
ALTERNATIVE_CSBI = [80, 24, 0, 0, 7, 0, 0, 79, 23].freeze
357+
351358
def get_screen_size
352-
unless csbi = get_console_screen_buffer_info
353-
return [1, 1]
354-
end
355-
csbi[0, 4].unpack('SS').reverse
359+
width, _, _, _, _, _, top, _, bottom = get_console_screen_buffer_info || ALTERNATIVE_CSBI
360+
[bottom - top + 1, width]
356361
end
357362

358363
def cursor_pos
359-
unless csbi = get_console_screen_buffer_info
360-
return Reline::CursorPos.new(0, 0)
361-
end
362-
x = csbi[4, 2].unpack1('s')
363-
y = csbi[6, 2].unpack1('s')
364-
Reline::CursorPos.new(x, y)
364+
_, _, x, y, _, _, top, = get_console_screen_buffer_info || ALTERNATIVE_CSBI
365+
Reline::CursorPos.new(x, y - top)
365366
end
366367

367368
def move_cursor_column(val)
368-
call_with_console_handle(@SetConsoleCursorPosition, cursor_pos.y * 65536 + val)
369+
_, _, _, y, = get_console_screen_buffer_info
370+
call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + val) if y
369371
end
370372

371373
def move_cursor_up(val)
372374
if val > 0
373-
y = cursor_pos.y - val
375+
_, _, x, y, _, _, top, = get_console_screen_buffer_info
376+
return unless y
377+
y = (y - top) - val
374378
y = 0 if y < 0
375-
call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + cursor_pos.x)
379+
call_with_console_handle(@SetConsoleCursorPosition, (y + top) * 65536 + x)
376380
elsif val < 0
377381
move_cursor_down(-val)
378382
end
379383
end
380384

381385
def move_cursor_down(val)
382386
if val > 0
383-
return unless csbi = get_console_screen_buffer_info
384-
screen_height = get_screen_size.first
385-
y = cursor_pos.y + val
386-
y = screen_height - 1 if y > (screen_height - 1)
387-
call_with_console_handle(@SetConsoleCursorPosition, (cursor_pos.y + val) * 65536 + cursor_pos.x)
387+
_, _, x, y, _, _, top, _, bottom = get_console_screen_buffer_info
388+
return unless y
389+
screen_height = bottom - top
390+
y = (y - top) + val
391+
y = screen_height if y > screen_height
392+
call_with_console_handle(@SetConsoleCursorPosition, (y + top) * 65536 + x)
388393
elsif val < 0
389394
move_cursor_up(-val)
390395
end
391396
end
392397

393398
def erase_after_cursor
394-
return unless csbi = get_console_screen_buffer_info
395-
attributes = csbi[8, 2].unpack1('S')
396-
cursor = csbi[4, 4].unpack1('L')
399+
width, _, x, y, attributes, = get_console_screen_buffer_info
400+
return unless x
397401
written = 0.chr * 4
398-
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
399-
call_with_console_handle(@FillConsoleOutputAttribute, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
400-
end
401-
402-
def scroll_down(val)
403-
return if val < 0
404-
return unless csbi = get_console_screen_buffer_info
405-
buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
406-
screen_height = window_bottom - window_top + 1
407-
val = screen_height if val > screen_height
408-
409-
if @legacy_console || window_left != 0
410-
# unless ENABLE_VIRTUAL_TERMINAL,
411-
# if srWindow.Left != 0 then it's conhost.exe hosted console
412-
# and puts "\n" causes horizontal scroll. its glitch.
413-
# FYI irb write from culumn 1, so this gives no gain.
414-
scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4')
415-
destination_origin = 0 # y * 65536 + x
416-
fill = [' '.ord, attributes].pack('SS')
417-
call_with_console_handle(@ScrollConsoleScreenBuffer, scroll_rectangle, nil, destination_origin, fill)
418-
else
419-
origin_x = x + 1
420-
origin_y = y - window_top + 1
421-
@output.write [
422-
(origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
423-
"\n" * val,
424-
(origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
425-
].join
426-
end
402+
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, width - x, y * 65536 + x, written)
403+
call_with_console_handle(@FillConsoleOutputAttribute, attributes, width - x, y * 65536 + x, written)
404+
end
405+
406+
# This only works when the cursor is at the bottom of the scroll range
407+
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
408+
def scroll_down(x)
409+
return if x.zero?
410+
# We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
411+
@output.write "\n" * x
427412
end
428413

429414
def clear_screen
430415
if @legacy_console
431-
return unless csbi = get_console_screen_buffer_info
432-
buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
433-
fill_length = buffer_width * (window_bottom - window_top + 1)
434-
screen_topleft = window_top * 65536
416+
width, _, _, _, attributes, _, top, _, bottom = get_console_screen_buffer_info
417+
return unless width
418+
fill_length = width * (bottom - top + 1)
419+
screen_topleft = top * 65536
435420
written = 0.chr * 4
436421
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, fill_length, screen_topleft, written)
437422
call_with_console_handle(@FillConsoleOutputAttribute, attributes, fill_length, screen_topleft, written)
@@ -472,6 +457,28 @@ def deprep(otio)
472457
# do nothing
473458
end
474459

460+
def disable_auto_linewrap(setting = true, &block)
461+
mode = getconsolemode
462+
if 0 == (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
463+
if block
464+
begin
465+
setconsolemode(mode & ~ENABLE_WRAP_AT_EOL_OUTPUT)
466+
block.call
467+
ensure
468+
setconsolemode(mode | ENABLE_WRAP_AT_EOL_OUTPUT)
469+
end
470+
else
471+
if setting
472+
setconsolemode(mode & ~ENABLE_WRAP_AT_EOL_OUTPUT)
473+
else
474+
setconsolemode(mode | ENABLE_WRAP_AT_EOL_OUTPUT)
475+
end
476+
end
477+
else
478+
block.call if block
479+
end
480+
end
481+
475482
class KeyEventRecord
476483

477484
attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys

lib/reline/line_editor.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,11 @@ def render_finished
472472
end
473473

474474
def print_nomultiline_prompt
475+
Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
475476
# Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence.
476477
@output.write Reline::Unicode.strip_non_printing_start_end(@prompt) if @prompt && !@is_multiline
478+
ensure
479+
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
477480
end
478481

479482
def render
@@ -509,6 +512,7 @@ def render
509512
# by calculating the difference from the previous render.
510513

511514
private def render_differential(new_lines, new_cursor_x, new_cursor_y)
515+
Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
512516
rendered_lines = @rendered_screen.lines
513517
cursor_y = @rendered_screen.cursor_y
514518
if new_lines != rendered_lines
@@ -539,6 +543,8 @@ def render
539543
Reline::IOGate.move_cursor_column new_cursor_x
540544
Reline::IOGate.move_cursor_down new_cursor_y - cursor_y
541545
@rendered_screen.cursor_y = new_cursor_y
546+
ensure
547+
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
542548
end
543549

544550
private def clear_rendered_screen_cache

test/reline/yamatanooroti/test_rendering.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ def iterate_over_face_configs(&block)
2525
config_file = Tempfile.create(%w{face_config- .rb})
2626
config_file.write face_config
2727
block.call(config_name, config_file)
28-
config_file.close
2928
ensure
29+
config_file.close
3030
File.delete(config_file)
3131
end
3232
end
@@ -1065,7 +1065,7 @@ def test_dialog_scroll_pushup_condition
10651065

10661066
def test_simple_dialog_with_scroll_screen
10671067
iterate_over_face_configs do |config_name, config_file|
1068-
start_terminal(5, 50, %W{ruby -I#{@pwd}/lib -r#{config_file.path} #{@pwd}/test/reline/yamatanooroti/multiline_repl --dialog simple}, startup_message: 'Multiline REPL.')
1068+
start_terminal(5, 50, %W{ruby -I#{@pwd}/lib -r#{config_file.path} #{@pwd}/test/reline/yamatanooroti/multiline_repl --dialog simple}, startup_message: /prompt>/)
10691069
write("if 1\n 2\n 3\n 4\n 5\n 6")
10701070
write("\C-p\C-n\C-p\C-p\C-p#")
10711071
close
@@ -1796,6 +1796,7 @@ def test_thread_safe
17961796
end
17971797

17981798
def test_stop_continue
1799+
omit if Reline.core.io_gate.win?
17991800
pidfile = Tempfile.create('pidfile')
18001801
rubyfile = Tempfile.create('rubyfile')
18011802
rubyfile.write <<~RUBY
@@ -1816,6 +1817,7 @@ def test_stop_continue
18161817
close
18171818
ensure
18181819
File.delete(rubyfile.path) if rubyfile
1820+
pidfile.close if pidfile
18191821
File.delete(pidfile.path) if pidfile
18201822
end
18211823

0 commit comments

Comments
 (0)