Files
herdr/scripts/windows_cross.py
Can Celik a124eed73c refactor: route all pane key encoding through libghostty (#5017)
* test: add input transparency conformance harness

* test: cover split host replies in input conformance harness

* fix: let libghostty encode every key sent to panes

* refactor: read live keyboard modes only from libghostty

* fix: wait for split input sequences instead of typing their fragments

* refactor: drop unused keyboard protocol argument from pane key encoding

* test: fix windows-only keyboard mode tests after libghostty mode refactor

* test: measure windows key record transparency against the keyboard layout

* fix: forward windows punctuation and altgr chords that produce no character

* test: expect libghostty kitty encoding in the windows conpty probe and cover release, alternate, text and modifyOtherKeys passthrough

* ci: lint windows-only test code in the cross-platform windows check

* refactor: delete herdr's own key encoder

* test: expect xterm modifyOtherKeys 2 encoding for shifted letters in the windows gauntlet

* refactor: read windows input through one native console reader

* refactor: parse sgr mouse reports once and map pixel reports without rewriting them

* refactor: decode framed input once instead of re-framing it in the client

* fix: keep macos alt, non-us kitty keys, text releases and reply-tail recovery correct after the libghostty switch

* fix: stop waiting for host replies the terminal never sends

* fix: deliver shifted punctuation releases after shift is let go

* fix: never type super, hyper or meta chords into panes that cannot read them

refs #4356

* test: replay reporter captures for fixed split-read and key-release issues

refs #4856
refs #4025
refs #4184

* fix: forward f1, f2 and f4 from kitty keyboard hosts

refs #4403

* fix: send pixel mouse reports to unfocused panes and only when the host supports them

refs #4750

* fix: treat 0x08 as backspace when the terminal's erase key is ctrl+h

refs #3244

* test: fix macos lint and keep the pixel mouse server test unix-only

* fix: deliver legacy alt+escape on macos instead of dropping it

* fix: address review findings on super+enter, held key releases and the pixel mouse probe

* test: expect super+enter to stay out of plain windows panes

* test: wait for the host pixel mouse mode instead of reading the channel once

* fix: send ctrl+shift+letter to plain shells as its control byte

* fix: encode each key under one keyboard mode snapshot

* fix: keep ime text intact and stop inventing key releases for committed text

* fix: release held key reports on focus loss and never drop held-key leases

* fix: send ctrl chords on non-latin layouts as their physical key

* fix: match non-latin ctrl chord releases after ctrl is let go

* fix: drop super chords before the windows conpty fallback in plain panes
2026-10-07 04:53:29 +03:00

120 lines
4.1 KiB
Python

"""One-time Windows SDK setup and Windows target linting from Unix hosts."""
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
SDK_ROOT = Path.home() / ".local/share/herdr/windows-cross"
LIBC_ENV = "LIBGHOSTTY_VT_WINDOWS_LIBC"
TARGET = "x86_64-pc-windows-msvc"
def libc_contents(root: Path) -> str:
paths = {
"include_dir": root / "sdk/include/ucrt",
"sys_include_dir": root / "crt/include",
"crt_dir": root / "sdk/lib/ucrt/x86_64",
"msvc_lib_dir": root / "crt/lib/x86_64",
"kernel32_lib_dir": root / "sdk/lib/um/x86_64",
}
required = {
"include_dir": "stdlib.h",
"sys_include_dir": "vcruntime.h",
"crt_dir": "ucrt.lib",
"msvc_lib_dir": "vcruntime.lib",
"kernel32_lib_dir": "kernel32.lib",
}
for key, filename in required.items():
if not (paths[key] / filename).is_file():
raise ValueError(f"Windows SDK is incomplete: missing {paths[key] / filename}")
return "".join(f"{key}={value}\n" for key, value in paths.items()) + "gcc_dir=\n"
def libc_path() -> Path:
override = os.environ.get(LIBC_ENV)
path = Path(override).expanduser() if override else SDK_ROOT / "libc.txt"
if not path.is_file():
raise ValueError(
f"Windows cross-check needs SDK configuration at {path}.\n"
"Run `just setup-windows-cross` once, or set "
f"{LIBC_ENV} to an existing Zig libc configuration."
)
return path.resolve()
def setup(accept_license: bool) -> None:
if not shutil.which("xwin"):
raise ValueError("Install xwin first: cargo install xwin --locked")
if not shutil.which(os.environ.get("ZIG", "zig")):
raise ValueError("Install Zig 0.16.0 first, or set ZIG to its executable.")
SDK_ROOT.mkdir(parents=True, exist_ok=True)
# SDK downloads can be large; /tmp is often RAM-backed on Linux.
temp_parent = "/var/tmp" if sys.platform.startswith("linux") else None
with tempfile.TemporaryDirectory(prefix="herdr-windows-sdk-", dir=temp_parent) as cache:
command = ["xwin", "--arch", "x86_64", "--cache-dir", cache]
if accept_license:
command.append("--accept-license")
subprocess.run(command + ["splat", "--copy", "--output", str(SDK_ROOT)], check=True)
config = SDK_ROOT / "libc.txt"
config.write_text(libc_contents(SDK_ROOT))
subprocess.run(
[os.environ.get("ZIG", "zig"), "libc", "-target", "x86_64-windows-msvc", str(config)],
check=True,
)
print(f"Windows SDK configured at {config}. Run `just windows-lint` or `just check`.")
def lint() -> None:
env = {**os.environ, LIBC_ENV: str(libc_path()), "LIBGHOSTTY_VT_SIMD": "false"}
subprocess.run(["rustup", "target", "add", TARGET], check=True)
subprocess.run(
# --tests also checks Windows-only test code, which Unix builds never compile.
[
"cargo",
"clippy",
"--bin",
"herdr",
"--tests",
"--locked",
"--target",
TARGET,
"--",
"-D",
"warnings",
],
env=env,
check=True,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
setup_parser = commands.add_parser("setup", help="Download Microsoft's SDK using xwin")
setup_parser.add_argument(
"--accept-license", action="store_true",
help="Explicitly accept Microsoft's SDK license instead of xwin's interactive prompt",
)
commands.add_parser("lint", help="Run Windows clippy with the configured SDK")
args = parser.parse_args()
try:
if args.command == "setup":
setup(args.accept_license)
else:
lint()
except (ValueError, OSError) as error:
print(error, file=sys.stderr)
return 1
except subprocess.CalledProcessError as error:
return error.returncode
return 0
if __name__ == "__main__":
sys.exit(main())