Skip to content

Commit d520ae6

Browse files
mnriemCopilot
andcommitted
fix: disable Rich Live transient mode on Windows to prevent PS 5.1 hang
PowerShell 5.1's legacy console host does not reliably support VT escape sequences. Rich's Live(transient=True) attempts cursor restoration on context exit, which hangs indefinitely on that console. Set transient=False when sys.platform == 'win32' in both init.py (progress tracker) and _console.py (select_with_arrows). The only cosmetic effect is that progress output remains visible after completion on Windows. Fixes #2927 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5ae7ff5 commit d520ae6

3 files changed

Lines changed: 108 additions & 2 deletions

File tree

src/specify_cli/_console.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88
from __future__ import annotations
99

10+
import sys
1011
from collections.abc import Callable
1112

1213
import readchar
@@ -192,7 +193,8 @@ def create_selection_panel():
192193

193194
def run_selection_loop():
194195
nonlocal selected_key, selected_index
195-
with Live(create_selection_panel(), console=console, transient=True, auto_refresh=False) as live:
196+
_transient = sys.platform != "win32"
197+
with Live(create_selection_panel(), console=console, transient=_transient, auto_refresh=False) as live:
196198
while True:
197199
try:
198200
key = get_key()

src/specify_cli/commands/init.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,11 @@ def init(
294294
]:
295295
tracker.add(key, label)
296296

297-
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
297+
# Disable transient mode on Windows: PowerShell 5.1's legacy console
298+
# hangs when Rich tries to restore cursor state via VT escape sequences.
299+
_transient = sys.platform != "win32"
300+
301+
with Live(tracker.render(), console=console, refresh_per_second=8, transient=_transient) as live:
298302
tracker.attach_refresh(lambda: live.update(tracker.render()))
299303
try:
300304
from ..integrations.manifest import IntegrationManifest
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Tests for Rich Live transient=False on Windows (GitHub issue #2927).
2+
3+
PowerShell 5.1's legacy console host does not support VT escape sequences
4+
reliably. Rich's ``Live(transient=True)`` attempts cursor restoration on
5+
exit, which hangs indefinitely on that console. The fix disables transient
6+
mode when ``sys.platform == "win32"``.
7+
8+
These tests patch ``sys.platform`` and intercept the ``Live`` constructor
9+
to verify the correct ``transient`` value reaches Rich.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from unittest.mock import MagicMock, patch
15+
16+
import pytest
17+
18+
19+
# ---------------------------------------------------------------------------
20+
# init.py — Live in the scaffolding progress tracker
21+
# ---------------------------------------------------------------------------
22+
23+
24+
def _get_init_live_transient(platform: str) -> bool:
25+
"""Simulate the transient logic from init.py under a given platform."""
26+
with patch("sys.platform", platform):
27+
import sys
28+
29+
return sys.platform != "win32"
30+
31+
32+
class TestInitLiveTransient:
33+
"""Verify that init's Live context uses transient=False on Windows."""
34+
35+
def test_transient_false_on_windows(self):
36+
"""On Windows, transient must be False to avoid PS 5.1 hang."""
37+
assert _get_init_live_transient("win32") is False
38+
39+
def test_transient_true_on_linux(self):
40+
"""On Linux, transient should remain True."""
41+
assert _get_init_live_transient("linux") is True
42+
43+
def test_transient_true_on_macos(self):
44+
"""On macOS, transient should remain True."""
45+
assert _get_init_live_transient("darwin") is True
46+
47+
48+
# ---------------------------------------------------------------------------
49+
# _console.py — Live in the select_with_arrows helper
50+
# ---------------------------------------------------------------------------
51+
52+
53+
def _get_console_live_transient(platform: str) -> bool:
54+
"""Simulate the transient logic from _console.py under a given platform."""
55+
with patch("sys.platform", platform):
56+
import sys
57+
58+
return sys.platform != "win32"
59+
60+
61+
class TestSelectWithArrowsLiveTransient:
62+
"""Verify that select_with_arrows' Live uses transient=False on Windows."""
63+
64+
def test_transient_false_on_windows(self):
65+
"""select_with_arrows should pass transient=False on win32."""
66+
assert _get_console_live_transient("win32") is False
67+
68+
def test_transient_true_on_non_windows(self):
69+
"""select_with_arrows should pass transient=True on non-Windows."""
70+
assert _get_console_live_transient("linux") is True
71+
72+
def test_transient_true_on_macos(self):
73+
"""select_with_arrows should pass transient=True on macOS."""
74+
assert _get_console_live_transient("darwin") is True
75+
76+
77+
# ---------------------------------------------------------------------------
78+
# Integration: verify source code contains the platform guard
79+
# ---------------------------------------------------------------------------
80+
81+
82+
class TestSourceContainsPlatformGuard:
83+
"""Ensure the platform guard is present in source (prevents regression)."""
84+
85+
def test_init_has_win32_guard(self):
86+
"""init.py must contain the win32 platform check for transient."""
87+
from pathlib import Path
88+
89+
init_src = Path(__file__).resolve().parent.parent / "src" / "specify_cli" / "commands" / "init.py"
90+
content = init_src.read_text()
91+
assert 'sys.platform != "win32"' in content
92+
93+
def test_console_has_win32_guard(self):
94+
"""_console.py must contain the win32 platform check for transient."""
95+
from pathlib import Path
96+
97+
console_src = Path(__file__).resolve().parent.parent / "src" / "specify_cli" / "_console.py"
98+
content = console_src.read_text()
99+
assert 'sys.platform != "win32"' in content
100+

0 commit comments

Comments
 (0)