Skip to content

Commit 3f4b68f

Browse files
committed
Add a Windows terminal inventory script for toolbar qualification
Temporary diagnostic for qualifying the reserved-row bottom toolbar on Windows. It records what the qualification needs and a checkout cannot otherwise supply: the input and output backend classes actually selected, viewport geometry against the backing console buffer, which object serves each delegated method, the rows-below-cursor value the renderer uses for available height, and the console mode before and after a scroll-region probe. It refuses to run unless stdout and stdin are both terminals. Redirecting or piping selects PlainTextOutput, which would record the wrong backend and silently invalidate the whole inventory. The probe always restores full-screen margins and attributes, so it leaves the terminal as it found it. Intended to be removed once Windows qualification is complete.
1 parent 56aa0af commit 3f4b68f

2 files changed

Lines changed: 216 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,6 @@ gha-creds-*.json
6666

6767
# Superpowers extension
6868
docs/superpowers
69+
70+
# Artifacts from scripts/windows_toolbar_inventory.py
71+
scripts/windows_toolbar_inventory_*.json
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env python3
2+
"""Windows terminal inventory for the reserved-row bottom-toolbar work.
3+
4+
uv run python scripts/windows_toolbar_inventory.py
5+
6+
Run this INSIDE each terminal under test (Windows Terminal, mintty/git-bash,
7+
conhost). It records what the plan requires before manual qualification: backend
8+
classes actually selected, viewport vs backing-buffer geometry, console-mode
9+
restoration, and the checks that can be made without a human looking at the screen.
10+
11+
It changes no cmd2 code and makes no permanent terminal changes: every escape
12+
sequence it emits is reset before exit.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import contextlib
18+
import json
19+
import os
20+
import platform
21+
import sys
22+
from datetime import datetime, timezone
23+
from typing import TYPE_CHECKING, Any
24+
25+
import prompt_toolkit
26+
from prompt_toolkit.input.defaults import create_input
27+
from prompt_toolkit.output.defaults import create_output
28+
29+
if TYPE_CHECKING: # pragma: no cover
30+
from prompt_toolkit.output import Output
31+
32+
33+
def section(title: str) -> None:
34+
"""Print a section heading.
35+
36+
:param title: heading text
37+
"""
38+
print(f"\n=== {title} ===")
39+
40+
41+
def inventory() -> tuple[dict[str, Any], Output]:
42+
"""Collect backend, geometry and console-mode facts for the current terminal.
43+
44+
:return: the collected facts, and the output object they were collected from
45+
"""
46+
out = create_output(stdout=sys.stdout)
47+
try:
48+
inp = create_input()
49+
in_cls = f"{type(inp).__module__}.{type(inp).__name__}"
50+
except (OSError, ValueError, ImportError) as exc: # pragma: no cover - diagnostic
51+
in_cls = f"<unavailable: {exc!r}>"
52+
53+
size = out.get_size()
54+
data: dict[str, Any] = {
55+
"captured_at": datetime.now(timezone.utc).isoformat(),
56+
"platform": platform.platform(),
57+
"os_release": platform.release(),
58+
"python": sys.version,
59+
"python_executable": sys.executable,
60+
"prompt_toolkit": prompt_toolkit.__version__,
61+
"TERM": os.environ.get("TERM"),
62+
"WT_SESSION": os.environ.get("WT_SESSION"),
63+
"MSYSTEM": os.environ.get("MSYSTEM"),
64+
"TERM_PROGRAM": os.environ.get("TERM_PROGRAM"),
65+
"output_class": f"{type(out).__module__}.{type(out).__name__}",
66+
"input_class": in_cls,
67+
"stdout_isatty": sys.stdout.isatty(),
68+
"stdin_isatty": sys.stdin.isatty(),
69+
"viewport_rows": size.rows,
70+
"viewport_columns": size.columns,
71+
}
72+
73+
# Windows: the backing buffer is usually taller than the viewport, and the
74+
# delegation whitelist means geometry comes from the native side while
75+
# rendering goes through VT.
76+
try:
77+
info = out.get_win32_screen_buffer_info() # type: ignore[attr-defined]
78+
data["win32_buffer_size"] = {"X": info.dwSize.X, "Y": info.dwSize.Y}
79+
data["win32_window"] = {
80+
"Left": info.srWindow.Left,
81+
"Top": info.srWindow.Top,
82+
"Right": info.srWindow.Right,
83+
"Bottom": info.srWindow.Bottom,
84+
}
85+
data["win32_cursor"] = {"X": info.dwCursorPosition.X, "Y": info.dwCursorPosition.Y}
86+
data["viewport_differs_from_buffer"] = (info.srWindow.Bottom - info.srWindow.Top + 1) != info.dwSize.Y
87+
except (AttributeError, OSError, NotImplementedError) as exc:
88+
data["win32_screen_buffer_info"] = f"<not available: {exc!r}>"
89+
90+
try:
91+
data["rows_below_cursor_position"] = out.get_rows_below_cursor_position()
92+
except (AttributeError, OSError, NotImplementedError) as exc:
93+
data["rows_below_cursor_position"] = f"<not available: {exc!r}>"
94+
95+
# Which object actually serves erase_down / get_size on this backend?
96+
for name in ("erase_down", "erase_screen", "get_size", "get_rows_below_cursor_position", "flush"):
97+
try:
98+
bound = getattr(out, name)
99+
owner = getattr(bound, "__self__", None)
100+
data[f"delegate::{name}"] = (
101+
f"{type(owner).__module__}.{type(owner).__name__}" if owner is not None else "<unbound>"
102+
)
103+
except (AttributeError, OSError, NotImplementedError) as exc:
104+
data[f"delegate::{name}"] = f"<error: {exc!r}>"
105+
106+
data["console_mode_before"] = _console_mode()
107+
return data, out
108+
109+
110+
def _console_mode() -> str:
111+
"""Read the Win32 console output mode, if this is a Windows console.
112+
113+
:return: a human-readable description of the mode, or why it is unavailable
114+
"""
115+
try:
116+
from ctypes import byref, windll # type: ignore[attr-defined]
117+
from ctypes.wintypes import DWORD, HANDLE # type: ignore[attr-defined]
118+
119+
handle = HANDLE(windll.kernel32.GetStdHandle(-11))
120+
mode = DWORD()
121+
if not windll.kernel32.GetConsoleMode(handle, byref(mode)):
122+
return "<GetConsoleMode failed>"
123+
value = mode.value
124+
return (
125+
f"0x{value:04X} "
126+
f"(VIRTUAL_TERMINAL_PROCESSING={'on' if value & 0x0004 else 'off'}, "
127+
f"WRAP_AT_EOL={'on' if value & 0x0002 else 'off'})"
128+
)
129+
except (ImportError, AttributeError, OSError) as exc:
130+
return f"<not a Windows console: {exc!r}>"
131+
132+
133+
def probe_decstbm(out: Output, rows: int) -> dict[str, Any]:
134+
"""Set and reset a reserved-row region; report whether modes survive it.
135+
136+
Deliberately conservative: it prints a marker, establishes the region, writes
137+
enough lines to scroll, then resets. The human confirms what they saw.
138+
139+
:param out: the output to emit through
140+
:param rows: the terminal's physical height
141+
:return: what was emitted, and the console mode afterwards
142+
"""
143+
results: dict[str, Any] = {}
144+
mark = "TOOLBARMARKER"
145+
try:
146+
out.write_raw(f"\x1b[{rows};1H{mark}")
147+
out.write_raw(f"\x1b[1;{rows - 1}r")
148+
out.write_raw("\x1b[1;1H")
149+
for i in range(1, rows * 3):
150+
out.write_raw(f"probe line {i:04d}\r\n")
151+
out.flush()
152+
results["region_emitted"] = f"\\x1b[1;{rows - 1}r"
153+
results["lines_written"] = rows * 3 - 1
154+
finally:
155+
out.write_raw("\x1b[r") # always restore full-screen margins
156+
out.write_raw("\x1b[0m")
157+
out.flush()
158+
results["console_mode_after"] = _console_mode()
159+
return results
160+
161+
162+
def main() -> int:
163+
"""Run the inventory and the visual probe.
164+
165+
:return: process exit status
166+
"""
167+
# Piping or redirecting selects PlainTextOutput, which would record the wrong
168+
# backend and silently invalidate the whole inventory.
169+
if not sys.stdout.isatty() or not sys.stdin.isatty():
170+
print("REFUSING TO RUN: stdout/stdin is not a terminal.")
171+
print("Run this directly in the terminal under test -- do not pipe or redirect it,")
172+
print("or the recorded backend classes will be wrong.")
173+
return 2
174+
175+
data, out = inventory()
176+
section("Environment inventory")
177+
for k, v in data.items():
178+
print(f" {k:38s} {v}")
179+
180+
rows = data["viewport_rows"]
181+
if rows < 4:
182+
print("\n viewport too small for the probe; resize and rerun")
183+
return 2
184+
185+
section("DECSTBM probe (visual confirmation required)")
186+
print(" About to reserve the bottom row, scroll past it, then reset.")
187+
print(" WATCH THE BOTTOM ROW. Press Enter when ready.")
188+
with contextlib.suppress(EOFError):
189+
input()
190+
data["decstbm_probe"] = probe_decstbm(out, rows)
191+
192+
section("Report")
193+
print(" console mode before :", data["console_mode_before"])
194+
print(" console mode after :", data["decstbm_probe"]["console_mode_after"])
195+
print(" modes match :", data["console_mode_before"] == data["decstbm_probe"]["console_mode_after"])
196+
print("\n Answer in the result template:")
197+
print(" 1. Did TOOLBARMARKER stay on the bottom row for the whole scroll?")
198+
print(" 2. Is the scrollback complete (probe line 0001 upward) and in order?")
199+
print(" 3. Does TOOLBARMARKER appear anywhere in the scrollback? (it must not)")
200+
print(" 4. After this program exits, does the shell prompt behave normally?")
201+
202+
# Write beside this script rather than into whatever directory the tester
203+
# happened to be in, so the artifact is easy to find and collect.
204+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"windows_toolbar_inventory_{platform.node()}.json")
205+
with open(path, "w") as fh:
206+
json.dump(data, fh, indent=2, default=str)
207+
print(f"\n machine-readable record written to: {path}")
208+
print(" Attach that file to the result template.")
209+
return 0
210+
211+
212+
if __name__ == "__main__":
213+
sys.exit(main())

0 commit comments

Comments
 (0)