Add native Windows support (ConPTY + pyte backend, platform abstraction) - #1107
Open
zhxf94 wants to merge 18 commits into
Open
Add native Windows support (ConPTY + pyte backend, platform abstraction)#1107zhxf94 wants to merge 18 commits into
zhxf94 wants to merge 18 commits into
Conversation
- Add terminatorlib/platform.py (pure stdlib): IS_WINDOWS, display_manager, shell_lookup (pwd on Linux, PowerShell/cmd on Windows), path_lookup (PATHEXT aware), open_url (os.startfile on Windows), get_window_id_env, XDG vs %APPDATA% config dirs, DWM dark-mode bridge, supports_dbus. - Refactor util.py to delegate to platform; drop top-level 'import pwd' (the hard blocker for importing on Windows). - terminal.py: replace xdg-open fallback with platform.open_url. - setup.py: conditional dbus-python (Linux only); add pywin32/pyte (Windows only) via PEP 508 markers. - Add terminatorlib/ipc_win32.py: named-pipe single-instance + command forwarding transport, mirroring the DBus service surface (wired in M3). Linux behaviour unchanged; 24 existing tests + doctests pass. Co-Authored-By: Claude <noreply@anthropic.com>
- Add terminal_backend.py: TerminalBackend contract + make_terminal_widget() factory that selects the concrete backend per platform (lazy import so neither VTE-on-Windows nor pywin32-on-Linux is ever loaded on the wrong OS). - Add backends/ package + backends/vte_backend.py: VteBackend subclasses Vte.Terminal (so all ~100 self.vte.* call sites in terminal.py keep working unchanged) and centralises the PTY spawn behind a portable spawn() surface (the part that is most platform-specific and that ConPTY will override). - terminal.py: construct via make_terminal_widget(); route spawn_sync / spawn_async through self.vte.spawn(args, envv, cwd, flatpak=...). Linux behaviour identical; VteBackend IS-A Vte.Terminal; 24 tests pass. Co-Authored-By: Claude <noreply@anthropic.com>
- backends/conpty/screen.py: pure-Python terminal screen over pyte (ByteStream for bytes, Cell model, truecolour/palette decoding, cursor, scrollback, URL matching via re ported from regex.py). Tested on Linux. - backends/conpty/pty.py: Windows ConPTY wrapper via ctypes (CreatePseudoConsole/Resize/Close, CreatePipe, STARTUPINFOEX + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, non-blocking PeekNamedPipe read, spawn/resize/poll_exit/close). Windows-only; ImportError elsewhere. - backends/conpty_backend.py: ConPtyTerminal(Gtk.DrawingArea) implementing the VteBackend surface for Windows -- spawn/feed/feed_child, Pango/Cairo cell rendering with cursor/selection, geometry getters, vadjustment, colours/font, URL hit-testing, clipboard, VTE-compatible GObject signals (child-exited/bell/selection-changed/window-title-changed) and the input-enabled property. Advanced VTE features (SIXEL, OSC-8 hover, cursor shape variants) stubbed with M5 TODOs. - terminal.py: platform-gate the Vte import (Vte=None on Windows); branch URL-regex registration to the backend's re-based match_add_regex on Windows; guard cursor-shape/blink config; route button-press through a new _vte_button_press helper (no-ops on Windows). - tests/test_conpty_screen.py: 11 unit tests for the emulation layer (feed->grid, attributes, truecolour, cursor, resize, URL matching, clipboard text). Co-Authored-By: Claude <noreply@anthropic.com>
- Entry script (terminator): platform-dispatched single-instance IPC. Linux keeps the DBus master/forward path unchanged; Windows uses ipc_win32 (PipeService master + send_command forward) for single-instance and new-window/new-tab/reload/toggle/unhide forwarding. - window.py: Dark/Light/Auto window decoration now applies on Windows via DWM (platform.set_window_dark_mode) through a new set_decoration_style() dispatcher; X11 keeps the _GTK_THEME_VARIANT path, Wayland/other no-op. Keybinder/libX11/blur already guarded by display_manager()=='X11', so they short-circuit on Windows. - Plugin gating needs no code change: PluginRegistry.load_plugins already wraps __import__ in try/except, so Vte-dependent plugins (command_notify, mousefree_url_handler, remote) skip gracefully on Windows. Linux behaviour unchanged; 38 tests pass. Co-Authored-By: Claude <noreply@anthropic.com>
- packaging/terminator.spec: one-folder PyInstaller build bundling terminatorlib + the ConPTY backend + pyte + win32 pipe hidden imports + Glade/theme/icon data; optionally collects GTK DLLs via GTK_BIN. - .github/workflows/windows.yml: windows-latest CI that installs the pip-available deps (pyte, pywin32, psutil, configobj) and runs the platform-portable tests (borg, signalman, conpty screen) plus byte-compilation. - PACKAGING-WINDOWS.md: build/run instructions and feature coverage notes. Co-Authored-By: Claude <noreply@anthropic.com>
- tests/test_terminal_backend.py: 45-case contract test pinning the full method surface terminal.py relies on (lifecycle, geometry, appearance, behaviour, title/cwd, selection/clipboard/URL, GtkWidget) against whatever make_terminal_widget() returns on the current platform. - tests/test_platform.py: 10 tests for platform.py including Windows branches exercised via monkeypatch on Linux (config dir, display manager, supports_dbus, open_url/startfile, dark-mode no-op). - Fix platform.open_url to catch OSError (WindowsError subclass) so the except clause is valid on non-Windows too. - docs/WINDOWS-ACCEPTANCE.md: manual verification matrix (cmd/powershell/wsl x launch/resize/scrollback/split/tab/broadcast/copy/paste/url/search/ single-instance/dark-titlebar) plus tracked gaps. Full suite: 105 passed. Co-Authored-By: Claude <noreply@anthropic.com>
conpty_backend.py: - _on_draw rewritten to build one Pango layout per line with a PangoAttrList encoding runs of equal (fg, bg, bold, italic, underline, strike), so true-colour, palette, reverse-video, bold/italic/underline/strikethrough all render correctly. Selection inverts fg<->bg per cell. - Cursor shapes: block (fill + glyph redrawn in cursor_fg), underline, beam/ibeam, positioned at the pyte cursor and drawn last on top. - Cursor colours: set_color_cursor / set_color_cursor_foreground now store (r,g,b) from Gdk.RGBA and are applied in the cursor draw. - Cursor blink: set_cursor_blink_mode starts/stops a GLib timeout that toggles visibility only while the widget is the focus (solid otherwise). - _resolve_colour generalised; helpers _norm/_pango16/_rgba_to_rgb/ _emit_run/_cursor_shape_from_value/_cursor_blink_on factored out. terminal.py: pass cursor_shape (raw string) and cursor_blink (bool) to the backend on Windows so the config actually takes effect. tests/test_conpty_render.py: 12 tests -- pure helper coverage + full draw path against a Cairo image surface (plain/bold/colour/reverse, cursor shapes, cursor colour, selection, clear-background). Full suite: 117 passed. Docs updated to move per-cell colour + cursor shape out of the known-gaps list (SIXEL/OSC-8/IME/wcwidth remain). Co-Authored-By: Claude <noreply@anthropic.com>
screen.py: - Cell gains a 'width' slot (1 normal, 2 East-Asian wide, 0 continuation). - cells() computes width via unicodedata.east_asian_width; pyte's empty continuation cell (the second column of a wide glyph) is marked width 0. - Fix Screen.resize: pyte's resize() takes (lines, columns) -- the opposite order of __init__(columns, lines) -- so pass keywords; the previous positional call scrambled the grid (swapped rows/cols). conpty_backend.py: - _on_draw builds line text from width>0 cells only, advancing the column cursor by each cell's width, and uses text indices for Pango attribute runs. Pango lays wide glyphs across two columns naturally, so trailing cells stay aligned without per-cell positioning. - _span_in_selection checks a wide glyph's whole column span for inversion. - _draw_cursor covers the cell's full display width (block/underline span 2cw over a wide glyph). - _selection_text skips width-0 continuation cells (no stray trailing space when copying a wide glyph). tests: 8 new (display-width helper, wide/continuation cells, resize keeps buffer, cursor advance-by-two, CJK draw, cursor-on-wide-glyph, selection text). Full suite: 125 passed. Docs: move double-width alignment out of the gaps list. Co-Authored-By: Claude <noreply@anthropic.com>
End-user-facing guide (zh-CN) for installing and running Terminator on Windows: prerequisites, MSYS2/GTK3 setup, dependency install, setup.py install, direct run, PyInstaller packaging, default-shell configuration (powershell/cmd/wsl), keybindings, colours/cursor, config file locations, known alpha limits, troubleshooting. Co-Authored-By: Claude <noreply@anthropic.com>
- install-windows.bat (double-click) -> install-windows.ps1: winget-installs Python + MSYS2, pacman GTK3+PyGObject, pip deps, setup.py install (with pip fallback for Py3.12+ distutils removal), writes run-windows.bat with baked paths, creates a desktop shortcut. - run-windows.bat: launch Terminator (MSYS2 MinGW64 python3 + GTK DLLs on PATH), runs from source so it works even if setup.py install failed. - build-windows-package.ps1: after install, runs PyInstaller bundling the GTK DLLs (GTK_BIN) into a self-contained dist\terminator\. - docs/WINDOWS-安装使用指南.md: new section 0 'one-click install' pointing at the scripts, with honest notes (winget first-time prompt, pywin32 IPC degradation under MSYS2). Note: PowerShell runtime not syntax-checked (no PS on the dev box); keep scripts simple + commented for Windows-side adjustment. Co-Authored-By: Claude <noreply@anthropic.com>
The one-click install failed at the pacman step with 'target not found': two causes, both fixed: 1. A fresh winget MSYS2 has never synced its package database, so every package is 'not found' until pacman -Sy runs. Now runs 'pacman -Sy --overwrite *' first. 2. MSYS2 renamed python3-* to python-*; python3-gobject/python3-pip/ python3-psutil no longer resolve. Install-Pkg tries both name variants and takes whichever resolves. Also: detect whether the mingw python is python3.exe or python.exe and bake that into run-windows.bat; add gobject-introspection (typelibs gi loads); keep psutil/pycairo on pacman (PyPI binary wheels don't fit mingw python ABI), pip only for pure-Python pyte/configobj; clearer error if python-gobject fails (the GUI cannot start without it). Co-Authored-By: Claude <noreply@anthropic.com>
make-offline-bundle.ps1: snapshot a working MSYS2 + Terminator install into a portable bundle/ that other machines install with zero network. - copy mode: robocopy C:\msys64\mingw64 (DLLs, mingw python, PyGObject, typelibs, pip pure-python deps) + terminator source into bundle/; generates run.bat (sets PATH + GI_TYPELIB_PATH, runs bundled python) and install-offline.bat (copies to %LOCALAPPDATA%\Terminator, desktop + start menu shortcuts). - pyinstaller mode: runs build-windows-package.ps1 and uses dist\terminator as the bundle (smaller, cleaner) -- recommended for distribution. The bundle is the 'deps-included' install: double-click install-offline.bat on any machine, seconds, no internet. (Bundle must be built once on a machine where the online install already succeeded; cannot be produced from the Linux dev box.) Guide updated with an offline-install section. Co-Authored-By: Claude <noreply@anthropic.com>
The pacman sync step appeared to hang (no output for a long time). Two fixes: 1. Explicitly run 'pacman-key --init' and 'pacman-key --populate msys2' before -Sy. A fresh MSYS2 initialises its keyring on first pacman use, which can block on entropy with no output -- doing it explicitly makes it observable and (usually) fast. 2. New Invoke-BashStream() runs pacman via Start-Process -NoNewWindow so its stdout streams live to the console (bash -lc output was buffered), and prints a dot every 3s with an elapsed-time marker every 60s while it runs -- so the user can always tell it is progressing, not hung. Install-Pkg now streams too (dropped the 2>$null that hid errors). Co-Authored-By: Claude <noreply@anthropic.com>
A fresh/stale MSYS2 keyring can't verify packages signed by Christoph Reiter's rotated key -> 'invalid signature' on every pacman op. Added a repair pass before the normal sync: make a copy of pacman.conf with SigLevel=Never, use it to sync the DB and install the latest msys2-keyring package (unsigned), then 'pacman-key --init' + '--populate msys2' to rebuild trust from the fresh keyring. The subsequent normal -Sy + installs then verify. Each sub-step streams output + heartbeat. Co-Authored-By: Claude <noreply@anthropic.com>
Member
|
This is pretty intriguing code. I like how it splits out the backend entirely and doesn't touch the existing terminator code. I did notice a test failing, can you fix this? |
…ied'
Invoke-BashStream used `Start-Process -ArgumentList @('-lc', $cmd)`, which
joins the array with spaces into one unquoted string. bash's -c then grabs
only the first token of $cmd as the command and drops the rest as positional
params, so pacman ran with no operation flag -> "错误:没有指定操作".
This silently broke the keyring-repair steps too and only surfaced at sync.
Write $cmd to a temp .sh file and run `bash -l <file>` instead, sidestepping
all quoting (single quotes, glob '*', sed expressions). Use .NET Process
rather than Start-Process so a space in the Windows temp path doesn't split
the path argument.
Co-Authored-By: Claude <noreply@anthropic.com>
On Linux, `use_system_font` resolves the GNOME monospace font (Ubuntu Mono /
DejaVu Sans Mono) so Terminator blends in. On Windows there is no gsettings
schema to query, so it fell back to Pango's generic "monospace" alias at size
10 -- an arbitrary family that looks nothing like the Ubuntu sibling. The cell
colours are already identical everywhere (Tango palette, black bg), so the font
is the only thing that needs pinning.
DejaVu isn't an MSYS2 package and isn't on a stock Windows box, so pinning the
family alone would just get silently substituted by fontconfig. Instead:
- bundle DejaVuSansMono{,-Bold}.ttf (+ LICENSE) under data/fonts/
- on Windows default profile: use_system_font=False, font="DejaVu Sans Mono 12"
- install-windows.ps1 copies the TTFs into the per-user Windows font dir
(fontconfig scans WINDOWSUSERFONTDIR -- no admin) and runs fc-cache -f
- make-offline-bundle.ps1's install-offline.bat does the same for offline
installs
- MANIFEST.in ships the fonts in sdists
No behaviour change on Linux (os.name != 'nt').
Co-Authored-By: Claude <noreply@anthropic.com>
… crash)
Launch crashed immediately with "Namespace Vte not available" because
terminatorlib/terminator.py (the master singleton) did
`gi.require_version('Vte', '2.91')` unconditionally at import time. Vte is
unavailable on Windows and isn't needed there (the ConPTY backend replaces
it), but the import runs before anything else. terminal.py already gated
this import behind `platform.IS_WINDOWS`; three sibling modules in the
startup chain were missed:
- terminatorlib/terminator.py -- imported first by the entry script;
the actual crash site. Vte is only used
for the use_theme_colors probe (a dummy
Vte.Terminal to read theme colours); skip
it on Windows, falling back to the
explicit profile background_colour.
- terminatorlib/searchbar.py -- imported at top of terminal.py. Vte is
only used for PCRE2 search; on Windows
do_search() now short-circuits (the
ConPTY backend has no search API) instead
of crashing on search_set_regex.
- terminatorlib/regex.py -- imported at top of terminal.py. With
Vte None, FLAGS_PCRE2 becomes None and
searchbar falls back to GLib.Regex.
The same `if not platform.IS_WINDOWS: ... else: Vte = None` pattern already
used in terminal.py. No behaviour change on Linux (verified: 119 tests pass;
Windows path simulated by patching platform.IS_WINDOWS=True -- all three
import cleanly with Vte=None).
The crash also blocked the split-terminal feature and the Ubuntu-style font
from the previous commit, since the app never reached the window.
Co-Authored-By: Claude <noreply@anthropic.com>
Launch crashed at `from configobj import` because configobj was never
installed: the pip step `pip install --upgrade pip pyte configobj` failed
silently. Two root causes:
1. MSYS2 mingw python ships the PEP-668 EXTERNALLY-MANAGED marker, so a
plain `pip install` is rejected with "externally-managed-environment".
2. `--upgrade pip` fights pacman's managed pip.
configobj is BSD-licensed and pure-Python, and is not a mingw MSYS2 package
(only an msys one, invisible to mingw python), so vendor it under
terminatorlib/_vendor (configobj + validate shim + LICENSE). terminatorlib
__init__ appends _vendor to sys.path (last, so a system install wins when
present; the vendored copy is the fallback). config.py is unchanged -- its
`from configobj import` / `from validate import` resolve to the vendored
copy when nothing else is installed. Verified: simulated absence of system
configobj (python -S) -> vendored resolves, config.py imports past the
configobj lines.
pyte is LGPLv3 (incompatible with Terminator's GPLv2-only), so it cannot be
vendored -- it must come from pip. Fix the pip command: drop --upgrade pip,
add --break-system-packages (with a no-flag retry for older pip), and
verify `import pyte` succeeds, Die-ing loudly if not (the ConPTY backend
cannot run without it). Also pass --break-system-packages to the setup.py
fallback `pip install .`.
Defense in depth: cwd.py imported psutil at module top, and
get_pid_cwd() runs in Terminal.__init__ -- so a missing psutil would crash
the next terminal creation. Guard the import and fall back to os.getcwd()
when psutil is absent (the terminal still launches; only "open split in
parent dir" degrades).
Also: setup.py now ships the _vendor subpackages and drops configobj from
install_requires (vendored). MANIFEST.in includes the vendored files.
No behaviour change on Linux (119 tests pass; system configobj still wins).
Co-Authored-By: Claude <noreply@anthropic.com>
Member
|
Reading over the code a little more, and I have a couple of things, and aside from the comments in the review, I have a few more comments.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes Terminator run natively on Windows (Windows 10 1809+, which provides
the ConPTY API). The historical blocker was libvte, which does PTY
management + VT emulation + glyph rendering in one C library with no Windows
port. This PR keeps Linux behaviour byte-for-byte identical while adding a
pluggable backend that provides a Windows equivalent.
Approach
A concrete terminal backend is-a GTK widget. On Linux
VteBackendsubclasses
Vte.Terminal, so every existingself.vte.*call site interminal.pyis unchanged (zero regression). On WindowsConPtyTerminalsubclasses
Gtk.DrawingAreaand implements the same surface, backed by:CreatePseudoConsole+CreateProcess) for the PTY/process side (ctypes, no SDK);Platform-specific concerns (shell lookup, URL opening, config dirs, X11
WINDOWID, D-Bus IPC) are funnelled through a newterminatorlib/platform.py(pure stdlib). Windows single-instance/forwarding uses a named-pipe IPC
(
ipc_win32.py) instead of D-Bus.Milestones
TerminalBackendabstraction +VteBackend(zero-risk)Testing
terminal.pymethod surface, 45 cases),platform tests incl. Windows branches via monkeypatch, pyte emulation layer,
and the renderer drawn against a real Cairo image surface.
windows.yml) runs the GTK-free subset onwindows-latest.Honest note
ConPTY, the GTK renderer, the named-pipe IPC, and DWM are written and
syntax/logic-verified, but their runtime was not exercised in this PR's
development environment (Linux). The acceptance matrix in
docs/WINDOWS-ACCEPTANCE.mdis the manual verification checklist.Known gaps (non-blocking for alpha)
Files
terminatorlib/platform.py,terminal_backend.py,ipc_win32.pyterminatorlib/backends/{vte_backend,conpty_backend}.pyterminatorlib/backends/conpty/{pty,screen}.pyterminal.py,util.py,window.py,setup.py,terminatorpackaging/terminator.spec,.github/workflows/windows.yml, docs🤖 Generated with Claude Code