Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/specify_cli/_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""
from __future__ import annotations

import sys
from collections.abc import Callable

import readchar
Expand Down Expand Up @@ -192,7 +193,8 @@ def create_selection_panel():

def run_selection_loop():
nonlocal selected_key, selected_index
with Live(create_selection_panel(), console=console, transient=True, auto_refresh=False) as live:
_transient = sys.platform != "win32"
with Live(create_selection_panel(), console=console, transient=_transient, auto_refresh=False) as live:
while True:
try:
key = get_key()
Expand Down
6 changes: 5 additions & 1 deletion src/specify_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,11 @@ def init(
]:
tracker.add(key, label)

with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
# Disable transient mode on Windows: PowerShell 5.1's legacy console
# hangs when Rich tries to restore cursor state via VT escape sequences.
_transient = sys.platform != "win32"

with Live(tracker.render(), console=console, refresh_per_second=8, transient=_transient) as live:
tracker.attach_refresh(lambda: live.update(tracker.render()))
try:
from ..integrations.manifest import IntegrationManifest
Expand Down
81 changes: 81 additions & 0 deletions tests/test_live_transient_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Tests for Rich Live transient=False on Windows (GitHub issue #2927).

PowerShell 5.1's legacy console host does not support VT escape sequences
reliably. Rich's ``Live(transient=True)`` attempts cursor restoration on
exit, which hangs indefinitely on that console. The fix disables transient
mode when ``sys.platform == "win32"``.

These tests patch ``sys.platform`` and intercept the ``Live`` constructor
to verify the correct ``transient`` value reaches Rich.
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed


# ---------------------------------------------------------------------------
# _console.py — Live in the select_with_arrows helper
# ---------------------------------------------------------------------------


def _invoke_select_with_arrows(platform: str) -> bool:
"""Patch sys.platform and Live, invoke select_with_arrows, return transient kwarg."""
captured = {}

mock_live_instance = MagicMock()
mock_live_instance.__enter__ = MagicMock(return_value=mock_live_instance)
mock_live_instance.__exit__ = MagicMock(return_value=False)

def fake_live(*args, **kwargs):
captured.update(kwargs)
return mock_live_instance

# Patch readchar so the loop immediately returns "enter"
import readchar

with (
patch("sys.platform", platform),
patch("specify_cli._console.Live", side_effect=fake_live),
patch("specify_cli._console.readchar.readkey", return_value=readchar.key.ENTER),
):
from specify_cli._console import select_with_arrows

select_with_arrows({"a": "Option A", "b": "Option B"}, "Pick one", "a")

return captured.get("transient")


class TestSelectWithArrowsLiveTransient:
"""Verify that select_with_arrows passes transient=False on Windows."""

def test_transient_false_on_windows(self):
assert _invoke_select_with_arrows("win32") is False

def test_transient_true_on_linux(self):
assert _invoke_select_with_arrows("linux") is True

def test_transient_true_on_macos(self):
assert _invoke_select_with_arrows("darwin") is True
Comment on lines +50 to +60


# ---------------------------------------------------------------------------
# init.py — verify source contains the platform guard (regression check)
# ---------------------------------------------------------------------------


class TestSourceContainsPlatformGuard:
"""Ensure the platform guard is present in source (prevents regression)."""

def test_init_has_win32_guard(self):
"""init.py must contain the win32 platform check for transient."""
init_src = Path(__file__).resolve().parent.parent / "src" / "specify_cli" / "commands" / "init.py"
content = init_src.read_text(encoding="utf-8")
assert '_transient = sys.platform != "win32"' in content
Comment on lines +73 to +75

def test_console_has_win32_guard(self):
"""_console.py must contain the win32 platform check for transient."""
console_src = Path(__file__).resolve().parent.parent / "src" / "specify_cli" / "_console.py"
content = console_src.read_text(encoding="utf-8")
assert '_transient = sys.platform != "win32"' in content
Comment on lines +79 to +81
Loading