Windows: diff syntax highlighting silently fails — and the obvious fixes hit a console-window flash, then a ~5s launch stall
Summary
On a stock Windows 11 + Git for Windows setup, diff syntax highlighting in git-gui-pyggy
silently does nothing. Chasing it down surfaced three distinct Windows-only problems,
each of which bites the "obvious" fix for the previous one:
syntax_python picks an interpreter that Tcl can't actually launch, so highlighting is silently disabled.
- Using a console interpreter makes git-gui pop up (and focus) an empty console window.
- Using a windowless interpreter (
pythonw/pyw) avoids the window but stalls git-gui launch by ~5 seconds.
A single design — spawn windowless, and have the helper re-exec itself as a detached
windowless grandchild — solves all three. Details and a proposed patch below.
Environment
- Windows 11 Pro (26200)
- Git for Windows (git-gui under
wish.exe, Tcl/Tk 8.6 bundled in mingw64/bin)
- Python: multiple interpreters on PATH, including Microsoft Store "App execution alias" stubs
- git-gui-pyggy
gitgui-0.21.pyggy
gui.diffsyntax defaults to true, the Python helper works fine when run by hand, and
Pygments is importable — yet no highlighting appeared in the diff viewer.
Problem 1 — syntax_python returns an unlaunchable Store stub
On Windows, python3 (and often python) resolve first to Microsoft Store App
execution alias stubs under …\AppData\Local\Microsoft\WindowsApps\. These are zero-byte
reparse points. They work from cmd/PowerShell, but Tcl's [open "|…"] cannot launch
them:
couldn't execute "…\WindowsApps\python3.EXE": no such file or directory
syntax_python tries python3 / python before py, returns the stub, syntax_start's
open fails, syntax_fd is left empty, and highlighting is silently disabled — with no
fallback to the working py launcher.
Reproduced with Git's own bundled tclsh:
auto_execok python3 = …\WindowsApps\python3.EXE
PYTHON3: OPEN FAILED: couldn't execute "…\WindowsApps\python3.EXE": no such file or directory
PY3: GOT: RES 1 6 # py -3 -> real interpreter, works
Fix: skip any candidate whose path contains WindowsApps.
Problem 2 — console interpreter pops up an empty console window
git-gui runs under wish.exe, a GUI-subsystem process with no console. When Tcl spawns
a console interpreter (py.exe / python.exe), Windows allocates a fresh, visible
console window for the child — it pops up and steals focus on every git-gui launch.
Hiding it after the fact from inside the helper (ShowWindow(GetConsoleWindow(), SW_HIDE))
is not good enough: the window still flashes open and plays a minimize animation. Looks
broken.
Problem 3 — windowless interpreter stalls launch ~5 seconds
The natural answer to Problem 2 is the windowless interpreter (pythonw.exe / pyw.exe),
which never shows a window. But that makes [open "|…"] block for ~5.5 s on every
launch.
Measured with the bundled tclsh (open duration only; the helper then responds in ~25 ms):
| interpreter spawned |
subsystem |
open time |
py -3 (launcher) |
console |
4 ms |
…\Python312\python.exe |
console |
6 ms |
…\anaconda3\python.exe |
console |
5 ms |
pyw -3 (launcher) |
windowless |
5526 ms |
…\Python312\pythonw.exe |
windowless |
5532 ms |
…\anaconda3\pythonw.exe |
windowless |
5571 ms |
This is Tcl's tclWinPipe.c doing WaitForSingleObject(child, 5000) on GUI-subsystem
children — it waits for the child to exit, and a persistent helper never does, so it always
burns the full 5 s timeout.
It is not WaitForInputIdle, which is the tempting explanation. I disproved that: a
windowless process that creates a real message-only window and sits idle in GetMessage()
still stalled 5.5 s, while a windowless process that simply exits fast returned in
~30 ms. So the gate is process exit, not input-idle state — no in-process message-pump trick
can help a long-lived helper.
The fix that resolves all three
Spawn the windowless interpreter (no window — Problem 2 solved), and have the helper
re-exec itself once as a detached windowless grandchild that inherits the stdio pipe, then
exit immediately. Tcl only ever waits on the short-lived bootstrap (exits in ~50 ms → fast
open — Problem 3 solved), while the grandchild keeps serving on the same pipe with no window.
Combined with skipping the Store stubs (Problem 1 solved).
Validated end-to-end against the installed files:
interp -> …\Launcher\pyw.EXE -3
open took 48 ms
first RES after 74 ms: RES 1 2
No console window, ~48 ms launch, highlighting works.
lib/syntax.tcl — syntax_python
Prefer windowless, skip Store stubs:
proc syntax_python {} {
# Skip Windows "App execution alias" stubs (under WindowsApps): zero-byte
# reparse points that [open |...] cannot launch. Prefer the WINDOWLESS
# pythonw / pyw so no console window appears; the helper re-exec's itself to
# avoid the ~5s WaitForSingleObject stall (see git-gui-highlight.py). Console
# python is the fallback. On Linux/macOS pyw/pythonw usually don't exist, so
# this falls through to python3/python.
set cands {}
set exe [auto_execok pyw]
if {$exe ne {}} {lappend cands [concat $exe -3]}
set exe [auto_execok pythonw]
if {$exe ne {}} {lappend cands $exe}
foreach cand {python3 python} {
set exe [auto_execok $cand]
if {$exe ne {}} {lappend cands $exe}
}
set exe [auto_execok py]
if {$exe ne {}} {lappend cands [concat $exe -3]}
foreach exe $cands {
if {[string match -nocase *WindowsApps* [lindex $exe 0]]} continue
return $exe
}
return [lindex $cands 0]
}
lib/git-gui-highlight.py — re-exec to a detached windowless grandchild
Near the top, before the Pygments import (requires import os):
# On Windows, re-exec ourselves once as a detached, windowless grandchild and
# exit. This is the only way to get BOTH no console window AND a fast git-gui
# launch:
# * a console interpreter spawns instantly but pops up an empty console window;
# * a windowless interpreter shows no window, but Tcl's [open |...] does
# WaitForSingleObject(child, 5000) on GUI-subsystem children, so a persistent
# windowless helper stalls git-gui startup ~5s.
# git-gui spawns us windowless (pythonw -> no window); we relaunch the real,
# persistent helper as a windowless grandchild that inherits this stdio pipe,
# then exit at once. Tcl waits only on us (we exit in ~50ms), and the grandchild
# keeps serving with no window. The pipe survives our exit because the grandchild
# holds inherited copies of the handles. Gated on actually running under
# pythonw.exe so a console fallback doesn't spawn a second window.
if (sys.platform == 'win32'
and os.environ.get('_GG_HL_CHILD') != '1'
and os.path.basename(sys.executable).lower() == 'pythonw.exe'):
import subprocess
_env = dict(os.environ)
_env['_GG_HL_CHILD'] = '1'
subprocess.Popen([sys.executable, os.path.abspath(__file__)],
stdin=sys.stdin, stdout=sys.stdout,
stderr=subprocess.DEVNULL, env=_env, close_fds=False)
sys.exit(0)
The grandchild's stdin EOF (git-gui closing the pipe on exit) terminates it cleanly, so there
is no leaked process.
Notes
- All three issues are Windows-only; the
sys.platform == 'win32' gate and the absence of
pyw/pythonw on Linux/macOS keep those paths unchanged.
- Even with the Store stubs removed from PATH, preferring
pyw/pythonw is still the right
call, since it's what enables the no-window path.
- The Windows installer's
pip install pygments targets whichever interpreter Get-Command python resolves to (often the Store stub's backing interpreter), which may differ from the
one git-gui ends up spawning. Worth ensuring the chosen interpreter is the one Pygments is
installed into (or detecting a missing Pygments and degrading visibly rather than silently).
Windows: diff syntax highlighting silently fails — and the obvious fixes hit a console-window flash, then a ~5s launch stall
Summary
On a stock Windows 11 + Git for Windows setup, diff syntax highlighting in git-gui-pyggy
silently does nothing. Chasing it down surfaced three distinct Windows-only problems,
each of which bites the "obvious" fix for the previous one:
syntax_pythonpicks an interpreter that Tcl can't actually launch, so highlighting is silently disabled.pythonw/pyw) avoids the window but stalls git-gui launch by ~5 seconds.A single design — spawn windowless, and have the helper re-exec itself as a detached
windowless grandchild — solves all three. Details and a proposed patch below.
Environment
wish.exe, Tcl/Tk 8.6 bundled inmingw64/bin)gitgui-0.21.pyggygui.diffsyntaxdefaults totrue, the Python helper works fine when run by hand, andPygments is importable — yet no highlighting appeared in the diff viewer.
Problem 1 —
syntax_pythonreturns an unlaunchable Store stubOn Windows,
python3(and oftenpython) resolve first to Microsoft Store Appexecution alias stubs under
…\AppData\Local\Microsoft\WindowsApps\. These are zero-bytereparse points. They work from cmd/PowerShell, but Tcl's
[open "|…"]cannot launchthem:
syntax_pythontriespython3/pythonbeforepy, returns the stub,syntax_start'sopenfails,syntax_fdis left empty, and highlighting is silently disabled — with nofallback to the working
pylauncher.Reproduced with Git's own bundled
tclsh:Fix: skip any candidate whose path contains
WindowsApps.Problem 2 — console interpreter pops up an empty console window
git-gui runs under
wish.exe, a GUI-subsystem process with no console. When Tcl spawnsa console interpreter (
py.exe/python.exe), Windows allocates a fresh, visibleconsole window for the child — it pops up and steals focus on every git-gui launch.
Hiding it after the fact from inside the helper (
ShowWindow(GetConsoleWindow(), SW_HIDE))is not good enough: the window still flashes open and plays a minimize animation. Looks
broken.
Problem 3 — windowless interpreter stalls launch ~5 seconds
The natural answer to Problem 2 is the windowless interpreter (
pythonw.exe/pyw.exe),which never shows a window. But that makes
[open "|…"]block for ~5.5 s on everylaunch.
Measured with the bundled
tclsh(openduration only; the helper then responds in ~25 ms):opentimepy -3(launcher)…\Python312\python.exe…\anaconda3\python.exepyw -3(launcher)…\Python312\pythonw.exe…\anaconda3\pythonw.exeThis is Tcl's
tclWinPipe.cdoingWaitForSingleObject(child, 5000)on GUI-subsystemchildren — it waits for the child to exit, and a persistent helper never does, so it always
burns the full 5 s timeout.
It is not
WaitForInputIdle, which is the tempting explanation. I disproved that: awindowless process that creates a real message-only window and sits idle in
GetMessage()still stalled 5.5 s, while a windowless process that simply exits fast returned in
~30 ms. So the gate is process exit, not input-idle state — no in-process message-pump trick
can help a long-lived helper.
The fix that resolves all three
Spawn the windowless interpreter (no window — Problem 2 solved), and have the helper
re-exec itself once as a detached windowless grandchild that inherits the stdio pipe, then
exit immediately. Tcl only ever waits on the short-lived bootstrap (exits in ~50 ms → fast
open — Problem 3 solved), while the grandchild keeps serving on the same pipe with no window.
Combined with skipping the Store stubs (Problem 1 solved).
Validated end-to-end against the installed files:
No console window, ~48 ms launch, highlighting works.
lib/syntax.tcl—syntax_pythonPrefer windowless, skip Store stubs:
lib/git-gui-highlight.py— re-exec to a detached windowless grandchildNear the top, before the Pygments import (requires
import os):The grandchild's stdin EOF (git-gui closing the pipe on exit) terminates it cleanly, so there
is no leaked process.
Notes
sys.platform == 'win32'gate and the absence ofpyw/pythonwon Linux/macOS keep those paths unchanged.pyw/pythonwis still the rightcall, since it's what enables the no-window path.
pip install pygmentstargets whichever interpreterGet-Command pythonresolves to (often the Store stub's backing interpreter), which may differ from theone git-gui ends up spawning. Worth ensuring the chosen interpreter is the one Pygments is
installed into (or detecting a missing Pygments and degrading visibly rather than silently).