Repository: Textualize/rich

Issue:
[BUG] Console.show_cursor() (and displaying Rich progress bars) clobbers cursor style on Windows
**Describe the bug**

For some context, I originally discovered this with the Rich progress bars in Twine, that whenever one would display, the cursor style would be clobbered to a block cursor, and reported as pypa/twine#902 . However, I subsequently reproduced it in `pip` as and was directed to report it to Rich, and indeed, I can reproduce it with a minimal Rich snippit.

On Windows (tested both 8.1 and 10 on multiple machines) running `rich.console.Console.show_cursor()` clobbers the current cursor style (underbar in the stock Windows terminals, bar in ConEmu) and forces the cursor to display as a block, even if the cursor is already visible, with no way I've found to get it back other than completely closing and restarting the terminal. This also happens whenever I run pip and Twine and Rich progress bars are displayed, unless I specifically disable them through their respective CLIs, which I have to always remember to do, or I have to restart my terminal tab to get my normal cursor back, potentially losing tens of thousands of lines of scrollback and command history.

I've reproduced the same behavior in all environments on my machine I was able to test, as well as the other clean-installed Windows machines I tested (with no obvious common configuration):

* cmd.exe shell running in the default Command Prompt terminal
* Powershell running in the default Powershell terminal
* cmd.exe running in ConEmu
* Git Bash running in ConEmu

(In Git Bash running in its bundled MinTTY, I could not confirm if the behavior can be reproduced since Rich treats it as a not a terminal and the function terminates without doing anything, given `is_terminal` is `False` there and `show_cursor(False)` does nothing.)

Furthermore, I've reproduced the above running the latest version of Rich, 12.4.4, in fresh clean environments with only Rich, pip, Twine and their deps installed, both from PyPI with pip, and from conda-forge with conda.

I've tried numerous methods to restore it once this happens, including modifying my terminal emulator settings, various ANSI escape sequences, the `reset` command in my shell, starting a new `bash` session within an existing one and sshing into another machine, including a Linux box, but nothing works save closing and restarting the terminal.

_However,_ if I `ssh` into a Linux host (prior to triggering the bug) from either ConEmu + Git Bash or MinTTY + Git Bash and run `show_cursor()` there, the cursor both hides and shows correctly, retaining whatever shape I configured in my terminal emulator (and I tested with lines, blocks and underbars). (To note, this works even from MinTTY running Git Bash, despite it not being considered a terminal when running bash on Windows.) The cursor hiding/showing persists (correctly, I assume) even when I exist the remote session, but if I do trigger the bug on Windows, so does the block cursor within and without the ssh session (and `show_cursor()` does not restore the previous one, since the block cursor is set as the current cursor). Therefore, it would seem there is something specific to Windows that is triggering this issue, given I'm running the same shell and same terminal emulator and everything works fine.

Minimal reproducer:

```shell
python -c "import rich.console; rich.console.Console().show_cursor()"
# Cursor now a block
```

**Platform**


What platform (Win/Linux/Mac) are you running on? What terminal software are you using?

### Reproduced issue

All running Python 3.9.13 x64

* {1*) Windows 8.1 Pro x64 + ConEmu 210912 stable + Git Bash 4.4.23(1)-release (x86_64-pc-msys)
* Windows 8.1 Pro x64 + ConEmu 210912 stable + cmd.exe
* Windows 8.1 Pro x64 + Command Prompt + cmd.exe
* Windows 8.1 Pro x64 + Windows Powershell + powershell
* Windows 10 Enterprise x64 + Command Prompt + cmd.exe

<details>

<summary>Windows + ConEmu + Git Bash platform details</summary>

`python -m rich` and `python -m rich.diagnose` display colors and other characters correctly, as far as I can tell.

```console
$ python -m pip list | grep rich
rich               12.4.4

$ python -m rich.diagnose
┌───────────────────────── <class 'rich.console.Console'> ─────────────────────────┐
│ A high level console interface.                                                  │
│                                                                                  │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ <console width=117 ColorSystem.WINDOWS>                                      │ │
│ └──────────────────────────────────────────────────────────────────────────────┘ │
│                                                                                  │
│     color_system = 'windows'                                                     │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 57                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = True                                                          │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=117, height=57),             │
│                        legacy_windows=True,                                      │
│                        min_width=1,                                              │
│                        max_width=117,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=57,                                            │
│                        justify=None,                                             │
│                        overflow=None,                                            │
│                        no_wrap=False,                                            │
│                        highlight=None,                                           │
│                        markup=None,                                              │
│                        height=None                                               │
│                    )                                                             │
│            quiet = False                                                         │
│           record = False                                                         │
│         safe_box = True                                                          │
│             size = ConsoleDimensions(width=117, height=57)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 117                                                           │
└──────────────────────────────────────────────────────────────────────────────────┘
┌─── <class 'rich._windows.WindowsConsoleFeatures'> ────┐
│ Windows features available.                           │
│                                                       │
│ ┌───────────────────────────────────────────────────┐ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ └───────────────────────────────────────────────────┘ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
└───────────────────────────────────────────────────────┘
┌────── Environment Variables ───────┐
│ {                                  │
│     'TERM': 'cygwin',              │
│     'COLORTERM': None,             │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': None,          │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
└────────────────────────────────────┘
platform="Windows"
```

</details>

### Could not confirm either way (not considered a terminal)

* (2*) Windows 8.1 Pro x64 + MinTTY 3.1.0 (x86_64-pc-msys) + Git Bash 4.4.23(1)-release (x86_64-pc-msys) + Python 3.9.13

<details>

<summary>Windows + MinTTY + Git Bash platform details</summary>

`python -m rich` and `python -m rich.diagnose` do not display any colors, many of the characters are mojibake/missing, and only work at all without an error when running in UTF-8 mode

```console
$ python -m pip list | grep rich
rich               12.4.4

$ python -m rich.diagnose
+---------------------- <class 'rich.console.Console'> -----------------------+
| A high level console interface.                                             |
|                                                                             |
| +-------------------------------------------------------------------------+ |
| | <console width=79 None>                                                 | |
| +-------------------------------------------------------------------------+ |
|                                                                             |
|     color_system = None                                                     |
|         encoding = 'cp1252'                                                 |
|             file = <_io.TextIOWrapper name='<stdout>' mode='w'              |
|                    encoding='cp1252'>                                       |
|           height = 25                                                       |
|    is_alt_screen = False                                                    |
| is_dumb_terminal = False                                                    |
|   is_interactive = False                                                    |
|       is_jupyter = False                                                    |
|      is_terminal = False                                                    |
|   legacy_windows = True                                                     |
|         no_color = False                                                    |
|          options = ConsoleOptions(                                          |
|                        size=ConsoleDimensions(width=79, height=25),         |
|                        legacy_windows=True,                                 |
|                        min_width=1,                                         |
|                        max_width=79,                                        |
|                        is_terminal=False,                                   |
|                        encoding='cp1252',                                   |
|                        max_height=25,                                       |
|                        justify=None,                                        |
|                        overflow=None,                                       |
|                        no_wrap=False,                                       |
|                        highlight=None,                                      |
|                        markup=None,                                         |
|                        height=None                                          |
|                    )                                                        |
|            quiet = False                                                    |
|           record = False                                                    |
|         safe_box = True                                                     |
|             size = ConsoleDimensions(width=79, height=25)                   |
|        soft_wrap = False                                                    |
|           stderr = False                                                    |
|            style = None                                                     |
|         tab_size = 8                                                        |
|            width = 79                                                       |
+-----------------------------------------------------------------------------+
+--- <class 'rich._windows.WindowsConsoleFeatures'> ----+
| Windows features available.                           |
|                                                       |
| +---------------------------------------------------+ |
| | WindowsConsoleFeatures(vt=False, truecolor=False) | |
| +---------------------------------------------------+ |
|                                                       |
| truecolor = False                                     |
|        vt = False                                     |
+-------------------------------------------------------+
+------ Environment Variables -------+
| {                                  |
|     'TERM': 'xterm',               |
|     'COLORTERM': None,             |
|     'CLICOLOR': None,              |
|     'NO_COLOR': None,              |
|     'TERM_PROGRAM': None,          |
|     'COLUMNS': None,               |
|     'LINES': None,                 |
|     'JPY_PARENT_PID': None,        |
|     'VSCODE_VERBOSE_LOGGING': None |
| }                                  |
+------------------------------------+
platform="Windows"

$ python -X utf8 -m rich.diagnose
┌────────────────────── <class 'rich.console.Console'> ───────────────────────┐
│ A high level console interface.                                             │
│                                                                             │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ <console width=79 None>                                                 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                             │
│     color_system = None                                                     │
│         encoding = 'utf-8'                                                  │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w'              │
│                    encoding='utf-8'>                                        │
│           height = 25                                                       │
│    is_alt_screen = False                                                    │
│ is_dumb_terminal = False                                                    │
│   is_interactive = False                                                    │
│       is_jupyter = False                                                    │
│      is_terminal = False                                                    │
│   legacy_windows = True                                                     │
│         no_color = False                                                    │
│          options = ConsoleOptions(                                          │
│                        size=ConsoleDimensions(width=79, height=25),         │
│                        legacy_windows=True,                                 │
│                        min_width=1,                                         │
│                        max_width=79,                                        │
│                        is_terminal=False,                                   │
│                        encoding='utf-8',                                    │
│                        max_height=25,                                       │
│                        justify=None,                                        │
│                        overflow=None,                                       │
│                        no_wrap=False,                                       │
│                        highlight=None,                                      │
│                        markup=None,                                         │
│                        height=None                                          │
│                    )                                                        │
│            quiet = False                                                    │
│           record = False                                                    │
│         safe_box = True                                                     │
│             size = ConsoleDimensions(width=79, height=25)                   │
│        soft_wrap = False                                                    │
│           stderr = False                                                    │
│            style = None                                                     │
│         tab_size = 8                                                        │
│            width = 79                                                       │
└─────────────────────────────────────────────────────────────────────────────┘
┌─── <class 'rich._windows.WindowsConsoleFeatures'> ────┐
│ Windows features available.                           │
│                                                       │
│ ┌───────────────────────────────────────────────────┐ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ └───────────────────────────────────────────────────┘ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
└───────────────────────────────────────────────────────┘
┌────── Environment Variables ───────┐
│ {                                  │
│     'TERM': 'xterm',               │
│     'COLORTERM': None,             │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': None,          │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
└────────────────────────────────────┘
platform="Windows"
```

</details>

### Confirmed working correctly:

*  Ubuntu 20.04.4 LTS via SSH + ConEmu 210912 stable + GNU bash 5.0.17(1)-release (x86_64-pc-linux-gnu) + Python 3.8.10 + ssh from (1*) and (2*)

<details>

<summary>Ubuntu via (1*) platform details</summary>

`python -m rich` and `python -m rich.diagnose` display colors and other characters correctly, as far as I can tell.

```console
$ python -m pip list | grep rich
rich               12.4.4

$ python -m rich.diagnose
╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface.                                                  │
│                                                                                  │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=118 ColorSystem.STANDARD>                                     │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│     color_system = 'standard'                                                    │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 57                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = False                                                         │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=118, height=57),             │
│                        legacy_windows=False,                                     │
│                        min_width=1,                                              │
│                        max_width=118,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=57,                                            │
│                        justify=None,                                             │
│                        overflow=None,                                            │
│                        no_wrap=False,                                            │
│                        highlight=None,                                           │
│                        markup=None,                                              │
│                        height=None                                               │
│                    )                                                             │
│            quiet = False                                                         │
│           record = False                                                         │
│         safe_box = True                                                          │
│             size = ConsoleDimensions(width=118, height=57)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 118                                                           │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'cygwin',              │
│     'COLORTERM': None,             │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': None,          │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Linux"
```

</details>


<details>

<summary>Ubuntu via (2*) platform details</summary>

`python -m rich` and `python -m rich.diagnose` display colors and other characters correctly, as far as I can tell.

```console
$ python -m pip list | grep rich
rich               12.4.4

$ python -m rich.diagnose
╭─────────────────────── <class 'rich.console.Console'> ───────────────────────╮
│ A high level console interface.                                              │
│                                                                              │
│ ╭──────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=80 ColorSystem.STANDARD>                                  │ │
│ ╰──────────────────────────────────────────────────────────────────────────╯ │
│                                                                              │
│     color_system = 'standard'                                                │
│         encoding = 'utf-8'                                                   │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w'               │
│                    encoding='utf-8'>                                         │
│           height = 24                                                        │
│    is_alt_screen = False                                                     │
│ is_dumb_terminal = False                                                     │
│   is_interactive = True                                                      │
│       is_jupyter = False                                                     │
│      is_terminal = True                                                      │
│   legacy_windows = False                                                     │
│         no_color = False                                                     │
│          options = ConsoleOptions(                                           │
│                        size=ConsoleDimensions(width=80, height=24),          │
│                        legacy_windows=False,                                 │
│                        min_width=1,                                          │
│                        max_width=80,                                         │
│                        is_terminal=True,                                     │
│                        encoding='utf-8',                                     │
│                        max_height=24,                                        │
│                        justify=None,                                         │
│                        overflow=None,                                        │
│                        no_wrap=False,                                        │
│                        highlight=None,                                       │
│                        markup=None,                                          │
│                        height=None                                           │
│                    )                                                         │
│            quiet = False                                                     │
│           record = False                                                     │
│         safe_box = True                                                      │
│             size = ConsoleDimensions(width=80, height=24)                    │
│        soft_wrap = False                                                     │
│           stderr = False                                                     │
│            style = None                                                      │
│         tab_size = 8                                                         │
│            width = 80                                                        │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'xterm',               │
│     'COLORTERM': None,             │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': None,          │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Linux"
```

</details>



Discussion:
I see the issue.  We're calling `SetConsoleCursorInfo` to hide the cursor. But this function also sets the cursor size. 

```python
         visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=1)
        SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor)
```

I assume what we need to do is to get the cursor info first, so that we don't clobber the `dwSize` field.

Resolving patch:
diff --git a/rich/_win32_console.py b/rich/_win32_console.py
--- 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()


Test patch:
diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py
--- 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):