Skip to content

Support custom pagers and passphrase prompts on Windows - #5740

Merged
stefanhaller merged 11 commits into
masterfrom
windows-pty
Jul 3, 2026
Merged

stefanhaller merged 11 commits into
masterfrom
windows-pty

Conversation

@stefanhaller

Copy link
Copy Markdown
Collaborator

Custom pagers were not available on Windows because the feature depends on a working PTY implementation, and the PTY library we are using doesn't have support for Windows (see creack/pty#155). This PR adds this support on our side; the same PTY library is still used on Mac and Linux, but on Windows we now have our own implementation using ConPTY.

This also enables prompting for SSH passphrases for users who don't have an ssh-agent running, which previously also didn't work on Windows. There's one limitation here: pressing f in the Files panel means fetch --all by default (unless you turn that off using git.fetchAll: false), in which case passphrase prompting still doesn't work; the reason is that git spawns a bunch of child processes for each remote to be fetched, and on Windows these don't inherit the PTY like they do on Mac and Linux.

Fixes #1453
Fixes #5525

stefanhaller and others added 11 commits July 3, 2026 18:47
The env var was previously set only on Windows, where the no-op pty
stub was just running the command without a pty and needed to expose
the width to pager scripts another way. With ConPTY coming to Windows
the rationale disappears there, but the env var is documented in
docs/Custom_Pagers.md for pager scripts that can't query the terminal
width directly. Set it on every platform so those scripts remain
portable, regardless of whether a pty is in play.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Windows ConPTY can't attach a child process to a pseudoconsole via
os/exec — Go's stdlib doesn't expose PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
(golang/go#62708). The ConPTY path has to call CreateProcess directly,
so it can't hand an *exec.Cmd back to the task runner.

Widen NewCmdTask to accept a small Cmd interface satisfied by both
*exec.Cmd (via the ExecCmd adapter) and the Windows ConPTY command type
we're about to add. Change TerminateProcessGracefully to take
*os.Process, which both cmd shapes can provide.

Behavior is unchanged on every platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the pty master behind a small interface (Read/Write/Close/Resize),
and push the actual startup into a platform-specific StartPty function
in pkg/commands/oscommands. The Unix implementation still uses
creack/pty; the Windows implementation is a stub that returns
ErrPtyUnsupported, at which point newPtyTask falls back to a plain cmd
task — matching the existing Windows behavior.

The primitive lives in oscommands rather than pkg/gui because the
cmd_obj_runner pty handler (also in oscommands) is going to consume it
too, and tasks → oscommands is the existing dependency direction.

Same observable behavior on every platform; this just carves out a seam
for a real ConPTY implementation on Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the StartPty stub with a real ConPTY implementation:
CreatePipe + CreatePseudoConsole + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
+ CreateProcess. Pagers and external diff tools now get real terminal
behavior instead of being handed pipes.

One Windows-specific quirk worth flagging: ConPTY does not EOF the
output pipe when the child exits; conhost keeps it alive until
ClosePseudoConsole is called explicitly. A background waiter goroutine
calls ClosePseudoConsole as soon as proc.Wait returns, so callers see
EOF on outRead — restoring the Unix master-fd-EOFs-when-slave-closes
semantics they depend on.

The ErrPtyUnsupported sentinel and the no-pty fallback in newPtyTask
are gone now that both platforms have a real implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-platform getCmdHandlerPty split existed because the Unix side
had creack/pty and the Windows side had nothing — so it fell back to a
non-pty handler. Now that oscommands.StartPty provides a pty on both
platforms, the two files collapse into one cross-platform
implementation and the stub is gone.

cmdHandler grows a 'wait' field because the pty path on Windows spawns
via CreateProcess and never runs exec.Cmd.Start — so cmd.Wait wouldn't
work there. Non-pty handlers set wait = cmd.Wait; pty handlers set it
to the wait closure StartPty returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConPTY presents its child's stdout as a screen buffer and uses CUP
(`\x1b[<row>;<col>H`) to skip over blank rows rather than emitting LFs
for them. Our escape interpreter swallows CUP via the catch-all
"valid CSI final byte we don't implement" branch, so the blank rows
the child put between non-blank ones disappear and the surrounding
lines collapse together — which is what makes the delta-rendered diff
in the screenshot look like its blank lines and section breaks were
removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConPTY presents its child's output as a screen buffer and uses CUP /
CUD / CNL / VPA to skip over blank rows rather than emitting LFs. The
previous behaviour swallowed all of those and the visible content
collapsed together. Now the escape parser tracks the screen-relative
cursor row, and any CSI that moves the cursor past the current row
emits a cursorDown instruction that the view turns into the matching
number of empty lines.

Column tracking is deliberately omitted: doing it correctly would mean
duplicating the view's grapheme-cluster width math in the parser, and
ConPTY in practice positions to column 1 after a CR-equivalent, which
the existing wx-reset path already handles. ConPTY-internal scrolling
needs no special handling either: it only emits cursor-positioning
escapes within the first, un-scrolled screenful — once its screen
scrolls it switches to plain linefeeds, which the view advances on
directly regardless of the tracked cursor.

Backward cursor moves are silently dropped — the view's buffer is
append-style and can't undo earlier writes. The exception is cursor-home
(CUP to row 1): ConPTY emits it at the start of every screen, so rather
than drop it we re-anchor the row tracking to the current write position.
Without that, a view not rewound in lockstep with ConPTY's screen (the
command log, which streams pty output without a rewind) accumulates
drift, and every later absolute CUP becomes a dropped backward move that
collapses the rows ConPTY positioned with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) rather than emitting them literally. Both currently
fall through the parser's swallow path, so the gap they describe
collapses entirely and content that the child wrote with leading
indentation ends up slid left against the previous cell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) instead of emitting them literally. ECH is still a
no-op for us — our buffer is built sequentially and has nothing to
erase — but CUF has to materialize as N visible space cells so the
gap actually appears, otherwise content the child wrote with leading
indentation slides left against the preceding cell.

The view's cursorForward branch reuses the same machinery as tab
expansion: substitute the trigger byte for a space and let the
repeatCount path emit the cells under the parser-tracked SGR. The
existing notifyCellsWritten plumbing then advances screenCol over
the gap, keeping subsequent CUP targets aligned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@stefanhaller
stefanhaller enabled auto-merge July 3, 2026 16:47
@stefanhaller
stefanhaller merged commit 16d773f into master Jul 3, 2026
13 checks passed
@stefanhaller
stefanhaller deleted the windows-pty branch July 3, 2026 16:50
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Jul 9, 2026
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [jesseduffield/lazygit](https://github.com/jesseduffield/lazygit) | minor | `v0.62.2` → `v0.63.0` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>jesseduffield/lazygit (jesseduffield/lazygit)</summary>

### [`v0.63.0`](https://github.com/jesseduffield/lazygit/releases/tag/v0.63.0)

[Compare Source](jesseduffield/lazygit@v0.62.2...v0.63.0)

<!-- Release notes generated using configuration in .github/release.yml at v0.63.0 -->

#### What's Changed

##### Enhancements 🔥

- Add direnv support by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5660](jesseduffield/lazygit#5660)
- Improve cycling through multiple pagers by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5678](jesseduffield/lazygit#5678)
- Detect external repo changes via background polling by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5662](jesseduffield/lazygit#5662)
- Make the side panels configurable by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5702](jesseduffield/lazygit#5702)
- Add a global keybinding for editing the config file by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5728](jesseduffield/lazygit#5728)
- Improve resolving non-textual and submodule merge conflicts by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5735](jesseduffield/lazygit#5735)
- Auto-dismiss the continue-rebase prompt when it becomes stale by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5758](jesseduffield/lazygit#5758)
- Support custom pagers and passphrase prompts on Windows by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5740](jesseduffield/lazygit#5740)
- Make creating worktrees simpler and less error-prone by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5741](jesseduffield/lazygit#5741)
- Improve deleting worktrees and their branches by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5748](jesseduffield/lazygit#5748)
- Allow overriding the platform used for default keybindings by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5671](jesseduffield/lazygit#5671)
- Add `gui.shrinkSidePanelsToContent` option by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5754](jesseduffield/lazygit#5754)
- Show renamed files in the custom patch builder by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5759](jesseduffield/lazygit#5759)

##### Fixes 🔧

- Fix unstaging a submodule with dirty content by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5666](jesseduffield/lazygit#5666)
- Fix coloring of wrapped delta lines by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5711](jesseduffield/lazygit#5711)
- Fix Files Panel artefacts during rebase commands by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5661](jesseduffield/lazygit#5661)
- Keep selected commits stable across refreshes by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5717](jesseduffield/lazygit#5717)
- Fix quoting of shell commands on Windows by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5704](jesseduffield/lazygit#5704)
- Silently consume unrecognized or malformed escape sequences by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5738](jesseduffield/lazygit#5738)
- Don't include common ancestor when picking "both" for a conflict in diff3 style by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5747](jesseduffield/lazygit#5747)

##### Maintenance ⚙️

- Some fixes to our infrastructure by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5705](jesseduffield/lazygit#5705)
- Restructure the `just` recipes for running integration tests by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5720](jesseduffield/lazygit#5720)
- Fix flaky TestNewCmdTaskInstantStop test by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5743](jesseduffield/lazygit#5743)
- Bump golang.org/x/sync from 0.20.0 to 0.21.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5684](jesseduffield/lazygit#5684)
- Bump golang.org/x/sys from 0.45.0 to 0.46.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5685](jesseduffield/lazygit#5685)
- Bump github.com/sahilm/fuzzy from 0.1.2 to 0.1.3 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5706](jesseduffield/lazygit#5706)
- Bump actions/cache from 5 to 6 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5722](jesseduffield/lazygit#5722)
- Bump actions/checkout from 6 to 7 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5723](jesseduffield/lazygit#5723)
- Bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5745](jesseduffield/lazygit#5745)
- Bump golangci/golangci-lint-action from 9.2.0 to 9.3.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5746](jesseduffield/lazygit#5746)
- Bump golang.org/x/net from 0.47.0 to 0.55.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;5752](jesseduffield/lazygit#5752)
- Pin gofumpt version to 0.9.2 by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5753](jesseduffield/lazygit#5753)
- Fix a few flaky tests by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5756](jesseduffield/lazygit#5756)

##### I18n 🌎

- Update translations from Crowdin by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5760](jesseduffield/lazygit#5760)

##### Performance Improvements 📊

- Prevent staging from becoming slower over time by [@&#8203;stefanhaller](https://github.com/stefanhaller) in [#&#8203;5712](jesseduffield/lazygit#5712)

**Full Changelog**: <jesseduffield/lazygit@v0.62.2...v0.63.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTYuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI1Ni4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWlub3IiXX0=-->
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
lazygit 0.63.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre><!-- Release notes generated using configuration in .github/release.yml at v0.63.0 -->

## What's Changed
### Enhancements 🔥
* Add direnv support by @stefanhaller in jesseduffield/lazygit#5660
* Improve cycling through multiple pagers by @stefanhaller in jesseduffield/lazygit#5678
* Detect external repo changes via background polling by @stefanhaller in jesseduffield/lazygit#5662
* Make the side panels configurable by @stefanhaller in jesseduffield/lazygit#5702
* Add a global keybinding for editing the config file by @stefanhaller in jesseduffield/lazygit#5728
* Improve resolving non-textual and submodule merge conflicts by @stefanhaller in jesseduffield/lazygit#5735
* Auto-dismiss the continue-rebase prompt when it becomes stale by @stefanhaller in jesseduffield/lazygit#5758
* Support custom pagers and passphrase prompts on Windows by @stefanhaller in jesseduffield/lazygit#5740
* Make creating worktrees simpler and less error-prone by @stefanhaller in jesseduffield/lazygit#5741
* Improve deleting worktrees and their branches by @stefanhaller in jesseduffield/lazygit#5748
* Allow overriding the platform used for default keybindings by @stefanhaller in jesseduffield/lazygit#5671
* Add `gui.shrinkSidePanelsToContent` option by @stefanhaller in jesseduffield/lazygit#5754
* Show renamed files in the custom patch builder by @stefanhaller in jesseduffield/lazygit#5759
### Fixes 🔧
* Fix unstaging a submodule with dirty content by @stefanhaller in jesseduffield/lazygit#5666
* Fix coloring of wrapped delta lines by @stefanhaller in jesseduffield/lazygit#5711
* Fix Files Panel artefacts during rebase commands by @stefanhaller in jesseduffield/lazygit#5661
* Keep selected commits stable across refreshes by @stefanhaller in jesseduffield/lazygit#5717
* Fix quoting of shell commands on Windows by @stefanhaller in jesseduffield/lazygit#5704
* Silently consume unrecognized or malformed escape sequences by @stefanhaller in jesseduffield/lazygit#5738
* Don't include common ancestor when picking "both" for a conflict in diff3 style by @stefanhaller in jesseduffield/lazygit#5747
### Maintenance ⚙️
* Some fixes to our infrastructure by @stefanhaller in jesseduffield/lazygit#5705
* Restructure the `just` recipes for running integration tests by @stefanhaller in jesseduffield/lazygit#5720
* Fix flaky TestNewCmdTaskInstantStop test by @stefanhaller in jesseduffield/lazygit#5743
* Bump golang.org/x/sync from 0.20.0 to 0.21.0 by @dependabot[bot] in jesseduffield/lazygit#5684
* Bump golang.org/x/sys from 0.45.0 to 0.46.0 by @dependabot[bot] in jesseduffield/lazygit#5685
* Bump github.com/sahilm/fuzzy from 0.1.2 to 0.1.3 by @dependabot[bot] in jesseduffield/lazygit#5706
* Bump actions/cache from 5 to 6 by @dependabot[bot] in jesseduffield/lazygit#5722
* Bump actions/checkout from 6 to 7 by @dependabot[bot] in jesseduffield/lazygit#5723
* Bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3 by @dependabot[bot] in jesseduffield/lazygit#5745
* Bump golangci/golangci-lint-action from 9.2.0 to 9.3.0 by @dependabot[bot] in jesseduffield/lazygit#5746
* Bump golang.org/x/net from 0.47.0 to 0.55.0 by @dependabot[bot] in jesseduffield/lazygit#5752
* Pin gofumpt version to 0.9.2 by @stefanhaller in jesseduffield/lazygit#5753
* Fix a few flaky tests by @stefanhaller in jesseduffield/lazygit#5756
### I18n 🌎
* Update translations from Crowdin by @stefanhaller in jesseduffield/lazygit#5760
### Performance Improvements 📊
* Prevent staging from becoming slower over time by @stefanhaller in jesseduffield/lazygit#5712


**Full Changelog**: jesseduffield/lazygit@v0.62.2...v0.63.0

</pre>
  <p>View the full release notes at <a href="https://github.com/jesseduffield/lazygit/releases/tag/v0.63.0">https://github.com/jesseduffield/lazygit/releases/tag/v0.63.0</a>.</p>
</details>
<hr>

See merge request: Harmonybrew/homebrew-core!13513
stefanhaller added a commit that referenced this pull request Sep 25, 2026
In #5740 we implemented PTY support for Windows; back then we thought
this is a prerequisite for supporting custom diff renderers (which were
still called "custom pagers" back then), because git will only use the
GIT_PAGER env var when it is running in a PTY. The problem is that the
Windows PTY, being based on ConPTY, does not behave like a Unix PTY,
which basically just passes through all data from the client. ConPTY
renders what it receives from the client into its own screen buffer, and
then re-encodes it from there for the terminal side. This has already
caused problems that are awkward to work around (e.g. ConPTY will
convert a series of multiple blank lines to a cursor positioning escape
sequence, so we need to parse that and convert it back, see
180fe0c); but now, with the upcoming OSC 1717 work, it turns out
that it's impossible to attach OSC 1717 metadata records to the cells
they belong to, because ConPTY sends those immediately to the terminal,
but the rest of the cell data some time later, and it's impossible to
reconstruct the original stream.

So use an ordinary pipe on Windows, where we start git and the diff
renderer on our side instead of telling git to drive the renderer. It's
a shame that we didn't realize it's possible; we could have done this
years ago without having to wait for a working Windows PTY.

One downside is that the diff renderer can no longer ask the terminal
how wide it is, so if it needs to know that (e.g. for a side-by-side
diff, or for horizontal lines that should be as wide as the view), then
it needs another way to find out. We set the `COLUMNS` environment
variable, which delta, difftastic and diff-so-fancy all support in their
latest versions, and for those renderers that don't, we provide a
`{{width}}` template variable that can be used in a diff renderer
command to pass it as a command-line argument.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows SSH Passphrase Support (Through ConPTY) Custom Pager support for windows

1 participant