Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 64 additions & 57 deletions lib/reline/io/windows.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def call(*args)
STD_OUTPUT_HANDLE = -11
FILE_TYPE_PIPE = 0x0003
FILE_NAME_INFO = 2
ENABLE_WRAP_AT_EOL_OUTPUT = 2
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4

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

private def getconsolemode
mode = "\000\000\000\000"
mode = +"\0\0\0\0"
call_with_console_handle(@GetConsoleMode, mode)
mode.unpack1('L')
end
Expand Down Expand Up @@ -344,94 +345,78 @@ def get_console_screen_buffer_info
# [18,2] dwMaximumWindowSize.X
# [20,2] dwMaximumWindowSize.Y
csbi = 0.chr * 22
return if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) == 0
csbi
if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) != 0
# returns [width, height, x, y, attributes, left, top, right, bottom]
csbi.unpack("s9")
else
return nil
end
end

ALTERNATIVE_CSBI = [80, 24, 0, 0, 7, 0, 0, 79, 23].freeze

def get_screen_size
unless csbi = get_console_screen_buffer_info
return [1, 1]
end
csbi[0, 4].unpack('SS').reverse
width, _, _, _, _, _, top, _, bottom = get_console_screen_buffer_info || ALTERNATIVE_CSBI
[bottom - top + 1, width]
Copy link
Member

Choose a reason for hiding this comment

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

My understanding by reading the document is:

hidden_screen_buffer01
hidden_screen_buffer02
hidden_screen_buffer03
hidden_screen_buffer04
visible_screen_buffer1
visible_screen_buffer2
visible_sc█een_buffer3
visible_screen_buffer4

screen_buffer width = 22, screen_buffer height = 8
cursor x = 10 y = 6 (cursor coord in screen buffer, not coord in visible window)
window left=0 right=21 top=4 bottom=7

Is this right? If so, I think def cursor_pos should use CursorPos.new(x, y - top). Can you check this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right.
I tried to modify cursor_pos, but even the cursor moving functions needed to take csbi.srWindow.Top into consideration. Needs some more work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in df70541 .

Copy link
Member

Choose a reason for hiding this comment

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

Thank you 👍

end

def cursor_pos
unless csbi = get_console_screen_buffer_info
return Reline::CursorPos.new(0, 0)
end
x = csbi[4, 2].unpack1('s')
y = csbi[6, 2].unpack1('s')
Reline::CursorPos.new(x, y)
_, _, x, y, _, _, top, = get_console_screen_buffer_info || ALTERNATIVE_CSBI
Reline::CursorPos.new(x, y - top)
end

def move_cursor_column(val)
call_with_console_handle(@SetConsoleCursorPosition, cursor_pos.y * 65536 + val)
_, _, _, y, = get_console_screen_buffer_info
call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + val) if y
end

def move_cursor_up(val)
if val > 0
y = cursor_pos.y - val
_, _, x, y, _, _, top, = get_console_screen_buffer_info
return unless y
y = (y - top) - val
y = 0 if y < 0
call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + cursor_pos.x)
call_with_console_handle(@SetConsoleCursorPosition, (y + top) * 65536 + x)
elsif val < 0
move_cursor_down(-val)
end
end

def move_cursor_down(val)
if val > 0
return unless csbi = get_console_screen_buffer_info
screen_height = get_screen_size.first
y = cursor_pos.y + val
y = screen_height - 1 if y > (screen_height - 1)
call_with_console_handle(@SetConsoleCursorPosition, (cursor_pos.y + val) * 65536 + cursor_pos.x)
_, _, x, y, _, _, top, _, bottom = get_console_screen_buffer_info
return unless y
screen_height = bottom - top
y = (y - top) + val
y = screen_height if y > screen_height
call_with_console_handle(@SetConsoleCursorPosition, (y + top) * 65536 + x)
elsif val < 0
move_cursor_up(-val)
end
end

def erase_after_cursor
return unless csbi = get_console_screen_buffer_info
attributes = csbi[8, 2].unpack1('S')
cursor = csbi[4, 4].unpack1('L')
width, _, x, y, attributes, = get_console_screen_buffer_info
return unless x
written = 0.chr * 4
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
call_with_console_handle(@FillConsoleOutputAttribute, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
end

def scroll_down(val)
return if val < 0
return unless csbi = get_console_screen_buffer_info
buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
screen_height = window_bottom - window_top + 1
val = screen_height if val > screen_height

if @legacy_console || window_left != 0
# unless ENABLE_VIRTUAL_TERMINAL,
# if srWindow.Left != 0 then it's conhost.exe hosted console
# and puts "\n" causes horizontal scroll. its glitch.
# FYI irb write from culumn 1, so this gives no gain.
scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4')
destination_origin = 0 # y * 65536 + x
fill = [' '.ord, attributes].pack('SS')
call_with_console_handle(@ScrollConsoleScreenBuffer, scroll_rectangle, nil, destination_origin, fill)
else
origin_x = x + 1
origin_y = y - window_top + 1
@output.write [
(origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
"\n" * val,
(origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
].join
end
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, width - x, y * 65536 + x, written)
call_with_console_handle(@FillConsoleOutputAttribute, attributes, width - x, y * 65536 + x, written)
end

# This only works when the cursor is at the bottom of the scroll range
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
def scroll_down(x)
return if x.zero?
# We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
@output.write "\n" * x
end

def clear_screen
if @legacy_console
return unless csbi = get_console_screen_buffer_info
buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
fill_length = buffer_width * (window_bottom - window_top + 1)
screen_topleft = window_top * 65536
width, _, _, _, attributes, _, top, _, bottom = get_console_screen_buffer_info
return unless width
fill_length = width * (bottom - top + 1)
screen_topleft = top * 65536
written = 0.chr * 4
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, fill_length, screen_topleft, written)
call_with_console_handle(@FillConsoleOutputAttribute, attributes, fill_length, screen_topleft, written)
Expand Down Expand Up @@ -472,6 +457,28 @@ def deprep(otio)
# do nothing
end

def disable_auto_linewrap(setting = true, &block)
mode = getconsolemode
if 0 == (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
if block
begin
setconsolemode(mode & ~ENABLE_WRAP_AT_EOL_OUTPUT)
block.call
ensure
setconsolemode(mode | ENABLE_WRAP_AT_EOL_OUTPUT)
end
else
if setting
setconsolemode(mode & ~ENABLE_WRAP_AT_EOL_OUTPUT)
else
setconsolemode(mode | ENABLE_WRAP_AT_EOL_OUTPUT)
end
end
else
block.call if block
end
end

class KeyEventRecord

attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys
Expand Down
6 changes: 6 additions & 0 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,11 @@ def render_finished
end

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

def render
Expand Down Expand Up @@ -509,6 +512,7 @@ def render
# by calculating the difference from the previous render.

private def render_differential(new_lines, new_cursor_x, new_cursor_y)
Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
Copy link
Member

Choose a reason for hiding this comment

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

Why is this needed? I think line_editor does not print line longer than terminal width.
(Maybe the case line_width == terminal_width case?)

If this is needed, moving it to Reline::Windows#prep and Reline::Windows#deprep is better

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The restore in deprep does not work because render_finished outputs a single, long line.
Windows console has mode flags. (https://learn.microsoft.com/en-us/windows/console/high-level-console-modes)
If ENABLE_VIRTUAL_TERMINAL_PROCESSING is set, cursor moves as expected by reline.
In environments where the flag is off or not supported, ENABLE_WRAP_AT_EOL_OUTPUT is on normally, causing a forced line break at the end of a line of output.
For example, outputting 123456 on a 6-column wide terminal would result in the following.

123456
_ #=> cursor moves next line

If ENABLE_WRAP_AT_EOL_OUTPUT is turned off, no forced line breaks occur. Instead, the cursor stays at the end of the line and subsequent characters overwrite the end of the line.
For example, outputting 12345678 on a 6-column wide terminal would result in the following.

123458 #=> cursor stays last column of line

Therefore, it was necessary to insert control of ENABLE_WRAP_AT_EOL_OUTPUT in the appropriate place.
Another reason was that if there was console output in the background, long lines could be destroyed if the flag was not restored while waiting for keystrokes.

Copy link
Member

Choose a reason for hiding this comment

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

It makes sense. Thank you for the description 👍

rendered_lines = @rendered_screen.lines
cursor_y = @rendered_screen.cursor_y
if new_lines != rendered_lines
Expand Down Expand Up @@ -539,6 +543,8 @@ def render
Reline::IOGate.move_cursor_column new_cursor_x
Reline::IOGate.move_cursor_down new_cursor_y - cursor_y
@rendered_screen.cursor_y = new_cursor_y
ensure
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
end

private def clear_rendered_screen_cache
Expand Down
6 changes: 4 additions & 2 deletions test/reline/yamatanooroti/test_rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def iterate_over_face_configs(&block)
config_file = Tempfile.create(%w{face_config- .rb})
config_file.write face_config
block.call(config_name, config_file)
config_file.close
ensure
config_file.close
File.delete(config_file)
end
end
Expand Down Expand Up @@ -1065,7 +1065,7 @@ def test_dialog_scroll_pushup_condition

def test_simple_dialog_with_scroll_screen
iterate_over_face_configs do |config_name, config_file|
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.')
start_terminal(5, 50, %W{ruby -I#{@pwd}/lib -r#{config_file.path} #{@pwd}/test/reline/yamatanooroti/multiline_repl --dialog simple}, startup_message: /prompt>/)
write("if 1\n 2\n 3\n 4\n 5\n 6")
write("\C-p\C-n\C-p\C-p\C-p#")
close
Expand Down Expand Up @@ -1796,6 +1796,7 @@ def test_thread_safe
end

def test_stop_continue
omit if Reline.core.io_gate.win?
pidfile = Tempfile.create('pidfile')
rubyfile = Tempfile.create('rubyfile')
rubyfile.write <<~RUBY
Expand All @@ -1816,6 +1817,7 @@ def test_stop_continue
close
ensure
File.delete(rubyfile.path) if rubyfile
pidfile.close if pidfile
File.delete(pidfile.path) if pidfile
end

Expand Down
Loading