diff --git a/CHANGELOG.md b/CHANGELOG.md index 4549ab1b8..88e045948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fix Rich clobbering cursor style on Windows https://github.com/Textualize/rich/pull/2339 - Fix text wrapping edge case https://github.com/Textualize/rich/pull/2296 - Allow exceptions that are raised while a Live is rendered to be displayed and/or processed https://github.com/Textualize/rich/pull/2305 - Fix crashes that can happen with `inspect` when docstrings contain some special control codes https://github.com/Textualize/rich/pull/2294 diff --git a/rich/_win32_console.py b/rich/_win32_console.py index f642279c5..e969d8164 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -263,6 +263,30 @@ def SetConsoleCursorPosition( return bool(_SetConsoleCursorPosition(std_handle, coords)) +_GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo +_GetConsoleCursorInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_CURSOR_INFO), +] +_GetConsoleCursorInfo.restype = wintypes.BOOL + + +def GetConsoleCursorInfo( + std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO +) -> bool: + """Get the cursor info - used to get cursor visibility and width + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct that receives information + about the console's cursor. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + return bool(_GetConsoleCursorInfo(std_handle, byref(cursor_info))) + + _SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo _SetConsoleCursorInfo.argtypes = [ wintypes.HANDLE, @@ -523,12 +547,14 @@ def move_cursor_backward(self) -> None: def hide_cursor(self) -> None: """Hide the cursor""" - invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=0) + current_cursor_size = self._get_cursor_size() + invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=0) SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor) def show_cursor(self) -> None: """Show the cursor""" - visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=1) + current_cursor_size = self._get_cursor_size() + visible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=1) SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor) def set_title(self, title: str) -> None: @@ -540,6 +566,12 @@ def set_title(self, title: str) -> None: assert len(title) < 255, "Console title must be less than 255 characters" SetConsoleTitle(title) + def _get_cursor_size(self) -> int: + """Get the percentage of the character cell that is filled by the cursor""" + cursor_info = CONSOLE_CURSOR_INFO() + GetConsoleCursorInfo(self._handle, cursor_info=cursor_info) + return int(cursor_info.dwSize) + if __name__ == "__main__": handle = GetStdHandle() diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index 33d4fe48b..a550cb32c 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -17,6 +17,7 @@ SCREEN_WIDTH = 20 SCREEN_HEIGHT = 30 DEFAULT_STYLE_ATTRIBUTE = 16 + CURSOR_SIZE = 25 @dataclasses.dataclass class StubScreenBufferInfo: @@ -37,26 +38,35 @@ def win32_handle(): with mock.patch.object(_win32_console, "GetStdHandle", return_value=handle): yield handle - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_cursor_position(_): + @pytest.fixture + def win32_console_getters(): + def stub_console_cursor_info(std_handle, cursor_info): + cursor_info.dwSize = CURSOR_SIZE + cursor_info.bVisible = True + + with mock.patch.object( + _win32_console, + "GetConsoleScreenBufferInfo", + return_value=StubScreenBufferInfo, + ) as GetConsoleScreenBufferInfo, mock.patch.object( + _win32_console, "GetConsoleCursorInfo", side_effect=stub_console_cursor_info + ) as GetConsoleCursorInfo: + yield { + "GetConsoleScreenBufferInfo": GetConsoleScreenBufferInfo, + "GetConsoleCursorInfo": GetConsoleCursorInfo, + } + + def test_cursor_position(win32_console_getters): term = LegacyWindowsTerm(sys.stdout) assert term.cursor_position == WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_screen_size(_): + def test_screen_size(win32_console_getters): term = LegacyWindowsTerm(sys.stdout) assert term.screen_size == WindowsCoordinates( row=SCREEN_HEIGHT, col=SCREEN_WIDTH ) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_write_text(_, win32_handle, capsys): + def test_write_text(win32_console_getters, win32_handle, capsys): text = "Hello, world!" term = LegacyWindowsTerm(sys.stdout) @@ -66,10 +76,12 @@ def test_write_text(_, win32_handle, capsys): assert captured.out == text @patch.object(_win32_console, "SetConsoleTextAttribute") - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_write_styled(_, SetConsoleTextAttribute, win32_handle, capsys): + def test_write_styled( + SetConsoleTextAttribute, + win32_console_getters, + win32_handle, + capsys, + ): style = Style.parse("black on red") text = "Hello, world!" term = LegacyWindowsTerm(sys.stdout) @@ -91,10 +103,9 @@ def test_write_styled(_, SetConsoleTextAttribute, win32_handle, capsys): assert second_kwargs["attributes"] == DEFAULT_STYLE_ATTRIBUTE @patch.object(_win32_console, "SetConsoleTextAttribute") - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_write_styled_bold(_, SetConsoleTextAttribute, win32_handle): + def test_write_styled_bold( + SetConsoleTextAttribute, win32_console_getters, win32_handle + ): style = Style.parse("bold black on red") text = "Hello, world!" term = LegacyWindowsTerm(sys.stdout) @@ -109,10 +120,9 @@ def test_write_styled_bold(_, SetConsoleTextAttribute, win32_handle): assert first_kwargs["attributes"].value == expected_attr @patch.object(_win32_console, "SetConsoleTextAttribute") - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_write_styled_reverse(_, SetConsoleTextAttribute, win32_handle): + def test_write_styled_reverse( + SetConsoleTextAttribute, win32_console_getters, win32_handle + ): style = Style.parse("reverse red on blue") text = "Hello, world!" term = LegacyWindowsTerm(sys.stdout) @@ -127,10 +137,9 @@ def test_write_styled_reverse(_, SetConsoleTextAttribute, win32_handle): assert first_kwargs["attributes"].value == expected_attr @patch.object(_win32_console, "SetConsoleTextAttribute") - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_write_styled_reverse(_, SetConsoleTextAttribute, win32_handle): + def test_write_styled_reverse( + SetConsoleTextAttribute, win32_console_getters, win32_handle + ): style = Style.parse("dim bright_red on blue") text = "Hello, world!" term = LegacyWindowsTerm(sys.stdout) @@ -145,10 +154,9 @@ def test_write_styled_reverse(_, SetConsoleTextAttribute, win32_handle): assert first_kwargs["attributes"].value == expected_attr @patch.object(_win32_console, "SetConsoleTextAttribute") - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_write_styled_no_foreground_color(_, SetConsoleTextAttribute, win32_handle): + def test_write_styled_no_foreground_color( + SetConsoleTextAttribute, win32_console_getters, win32_handle + ): style = Style.parse("on blue") text = "Hello, world!" term = LegacyWindowsTerm(sys.stdout) @@ -163,10 +171,9 @@ def test_write_styled_no_foreground_color(_, SetConsoleTextAttribute, win32_hand assert first_kwargs["attributes"].value == expected_attr @patch.object(_win32_console, "SetConsoleTextAttribute") - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_write_styled_no_background_color(_, SetConsoleTextAttribute, win32_handle): + def test_write_styled_no_background_color( + SetConsoleTextAttribute, win32_console_getters, win32_handle + ): style = Style.parse("blue") text = "Hello, world!" term = LegacyWindowsTerm(sys.stdout) @@ -184,11 +191,11 @@ def test_write_styled_no_background_color(_, SetConsoleTextAttribute, win32_hand @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) def test_erase_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + FillConsoleOutputAttribute, + FillConsoleOutputCharacter, + win32_console_getters, + win32_handle, ): term = LegacyWindowsTerm(sys.stdout) term.erase_line() @@ -202,11 +209,11 @@ def test_erase_line( @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) def test_erase_end_of_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + FillConsoleOutputAttribute, + FillConsoleOutputCharacter, + win32_console_getters, + win32_handle, ): term = LegacyWindowsTerm(sys.stdout) term.erase_end_of_line() @@ -223,11 +230,11 @@ def test_erase_end_of_line( @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) def test_erase_start_of_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + FillConsoleOutputAttribute, + FillConsoleOutputCharacter, + win32_console_getters, + win32_handle, ): term = LegacyWindowsTerm(sys.stdout) term.erase_start_of_line() @@ -242,10 +249,9 @@ def test_erase_start_of_line( ) @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_to( + SetConsoleCursorPosition, win32_console_getters, win32_handle + ): coords = WindowsCoordinates(row=4, col=5) term = LegacyWindowsTerm(sys.stdout) @@ -254,11 +260,8 @@ def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle): SetConsoleCursorPosition.assert_called_once_with(win32_handle, coords=coords) @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) def test_move_cursor_to_out_of_bounds_row( - _, SetConsoleCursorPosition, win32_handle + SetConsoleCursorPosition, win32_console_getters, win32_handle ): coords = WindowsCoordinates(row=-1, col=4) term = LegacyWindowsTerm(sys.stdout) @@ -268,11 +271,8 @@ def test_move_cursor_to_out_of_bounds_row( assert not SetConsoleCursorPosition.called @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) def test_move_cursor_to_out_of_bounds_col( - _, SetConsoleCursorPosition, win32_handle + SetConsoleCursorPosition, win32_console_getters, win32_handle ): coords = WindowsCoordinates(row=10, col=-4) term = LegacyWindowsTerm(sys.stdout) @@ -282,10 +282,9 @@ def test_move_cursor_to_out_of_bounds_col( assert not SetConsoleCursorPosition.called @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_up( + SetConsoleCursorPosition, win32_console_getters, win32_handle + ): term = LegacyWindowsTerm(sys.stdout) term.move_cursor_up() @@ -295,10 +294,9 @@ def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle): ) @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_down( + SetConsoleCursorPosition, win32_console_getters, win32_handle + ): term = LegacyWindowsTerm(sys.stdout) term.move_cursor_down() @@ -308,10 +306,9 @@ def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle): ) @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_forward( + SetConsoleCursorPosition, win32_console_getters, win32_handle + ): term = LegacyWindowsTerm(sys.stdout) term.move_cursor_forward() @@ -321,27 +318,26 @@ def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle): ) @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle): + def test_move_cursor_forward_newline_wrap( + SetConsoleCursorPosition, win32_console_getters, win32_handle + ): cursor_at_end_of_line = StubScreenBufferInfo( dwCursorPosition=COORD(SCREEN_WIDTH - 1, CURSOR_Y) ) - with patch.object( - _win32_console, - "GetConsoleScreenBufferInfo", - return_value=cursor_at_end_of_line, - ): - term = LegacyWindowsTerm(sys.stdout) - term.move_cursor_forward() + win32_console_getters[ + "GetConsoleScreenBufferInfo" + ].return_value = cursor_at_end_of_line + term = LegacyWindowsTerm(sys.stdout) + term.move_cursor_forward() SetConsoleCursorPosition.assert_called_once_with( win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=0) ) @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_to_column( + SetConsoleCursorPosition, win32_console_getters, win32_handle + ): term = LegacyWindowsTerm(sys.stdout) term.move_cursor_to_column(5) SetConsoleCursorPosition.assert_called_once_with( @@ -349,10 +345,9 @@ def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle): ) @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_backward( + SetConsoleCursorPosition, win32_console_getters, win32_handle + ): term = LegacyWindowsTerm(sys.stdout) term.move_cursor_backward() SetConsoleCursorPosition.assert_called_once_with( @@ -361,28 +356,23 @@ def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle): @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) def test_move_cursor_backward_prev_line_wrap( - SetConsoleCursorPosition, win32_handle + SetConsoleCursorPosition, win32_console_getters, win32_handle ): cursor_at_start_of_line = StubScreenBufferInfo( dwCursorPosition=COORD(0, CURSOR_Y) ) - with patch.object( - _win32_console, - "GetConsoleScreenBufferInfo", - return_value=cursor_at_start_of_line, - ): - term = LegacyWindowsTerm(sys.stdout) - term.move_cursor_backward() + win32_console_getters[ + "GetConsoleScreenBufferInfo" + ].return_value = cursor_at_start_of_line + term = LegacyWindowsTerm(sys.stdout) + term.move_cursor_backward() SetConsoleCursorPosition.assert_called_once_with( win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1), ) @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): + def test_hide_cursor(SetConsoleCursorInfo, win32_console_getters, win32_handle): term = LegacyWindowsTerm(sys.stdout) term.hide_cursor() @@ -392,13 +382,10 @@ def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): args, kwargs = call_args[0] assert kwargs["cursor_info"].bVisible == 0 - assert kwargs["cursor_info"].dwSize == 100 + assert kwargs["cursor_info"].dwSize == CURSOR_SIZE @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) - @patch.object( - _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo - ) - def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): + def test_show_cursor(SetConsoleCursorInfo, win32_console_getters, win32_handle): term = LegacyWindowsTerm(sys.stdout) term.show_cursor() @@ -408,17 +395,17 @@ def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): args, kwargs = call_args[0] assert kwargs["cursor_info"].bVisible == 1 - assert kwargs["cursor_info"].dwSize == 100 + assert kwargs["cursor_info"].dwSize == CURSOR_SIZE @patch.object(_win32_console, "SetConsoleTitle", return_value=None) - def test_set_title(SetConsoleTitle): + def test_set_title(SetConsoleTitle, win32_console_getters): term = LegacyWindowsTerm(sys.stdout) term.set_title("title") SetConsoleTitle.assert_called_once_with("title") @patch.object(_win32_console, "SetConsoleTitle", return_value=None) - def test_set_title_too_long(_): + def test_set_title_too_long(_, win32_console_getters): term = LegacyWindowsTerm(sys.stdout) with pytest.raises(AssertionError):