Skip to content

feat: Add Windows support via ConPTY#6

Open
deblasis wants to merge 2 commits into
ghostty-org:mainfrom
deblasis:feat/windows-port
Open

feat: Add Windows support via ConPTY#6
deblasis wants to merge 2 commits into
ghostty-org:mainfrom
deblasis:feat/windows-port

Conversation

@deblasis
Copy link
Copy Markdown

Adds Windows support using the ConPTY API for terminal emulation.

  • Shell spawn via ConPTY with threaded pipe reader, resize, and cleanup
  • Auto-detect shell (pwsh, powershell, cmd) with --shell override
  • GUI subsystem app so the child shell writes through the pipe
  • CMake workarounds for upstream ghostty DLL path and import library on Windows
  • CI job for Windows

All behind #ifdef _WIN32 in main.c, Unix codepath unchanged.

Tested on Win, Mac and Linux

Comment thread README.md
functional terminal built on the libghostty C API in a
[single C file](https://github.com/ghostty-org/ghostling/blob/main/main.c).

The example uses Raylib for windowing and rendering. It is single-threaded
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the threading detail because with windows technically this is an incorrect statement since we have two threads. Happy to revert as needed

@mitchellh
Copy link
Copy Markdown
Contributor

Lets wait for ghostty-org/ghostty#11756 and then I'll ask if you can rebase this and clean it up, since I think that'll maybe improve some of this.

mitchellh added a commit to ghostty-org/ghostty that referenced this pull request Mar 24, 2026
# What 

PR #11756 added IMPORTED_IMPLIB pointing to the .lib import library, but
the
import library is not listed in the OUTPUT directive of the
`add_custom_command`
that runs zig build. The file is produced as a side-effect of the build.

This works with the Visual Studio generator (which is lenient about
undeclared outputs) but fails with Ninja:

ninja: error: 'zig-out/lib/ghostty-vt.lib', needed by 'ghostling',
missing and no known rule to make it

The fix adds "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_IMPLIB}" to the OUTPUT
list. No
behavioral change. The file was already being built, Ninja just needs to
know
about it.

## but_why.gif

I am cleaning up ghostty-org/ghostling#6 and I
realise that in order to get rid of the CMake workarounds we had before
#11756, this change is necessary.

# POC

I set up a branch pointing at my fork with a POC and it builds, this is
the cleaned up CMakeList
https://github.com/deblasis/winghostling/blob/test/cmake-implib-no-workaround/CMakeLists.txt
@deblasis deblasis force-pushed the feat/windows-port branch 2 times, most recently from 39ccb3a to 224bcc4 Compare March 24, 2026 04:06
@deblasis
Copy link
Copy Markdown
Author

The #ifdef _WIN32 blocks in main() could be consolidated behind a unified Pty struct with platform-specific implementations (eg: pty_drain(), pty_reap(), pty_resize()), eliminating all conditionals from main().

I kept inline #ifdef blocks to match the existing style in the codebase, but happy to refactor if you'd prefer that approach.

Anything else, fire away.

Copy link
Copy Markdown
Contributor

@mitchellh mitchellh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not bad. I agree the ifdefs are pretty ugly. I'll have to think about it. I put a couple simple requests otherwise.

Comment thread main.c Outdated
@@ -1,6 +1,55 @@
// ---------------------------------------------------------------------------
// Platform-independent headers
// ---------------------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this at the top.

Comment thread main.c Outdated
int main(void)
int main(int argc, char *argv[])
{
// Parse --shell <path> flag for explicit shell selection.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's just change this to argv[1] (no parsing).

@deblasis
Copy link
Copy Markdown
Author

Not bad. I agree the ifdefs are pretty ugly. I'll have to think about it. I put a couple simple requests otherwise.

Awesome! Done and done and tested.

@deblasis deblasis requested a review from mitchellh March 24, 2026 04:40
@res2k
Copy link
Copy Markdown

res2k commented Apr 20, 2026

I gave these changes a go, and I have some observations:

  • "Send an initial carriage return" - not necessary for me, I can see the initial process output just fine.
    In fact, this is actually unwanted input to the shell and should probably be removed.
  • PROPERTIES WIN32_EXECUTABLE TRUE wasn't necessary, either; even without it, output was never mistakenly sent to ghostling's console, yet ghostling's own output can be inspected.
  • The process exit detection doesn't quite work; it appears the child process exiting doesn't automatically results in the PTY getting closed, and so a PTY read error can't be used to detect that event. Instead, the WaitForSingleObject() on the process handle will detect the child having exited.

deblasis and others added 2 commits April 21, 2026 03:12
Add a build-windows job that runs on windows-latest with Zig + Ninja.
Set WIN32_EXECUTABLE TRUE so the app launches as a GUI subsystem
process without spawning an extra console window.  MSVC/Clang need
an explicit /ENTRY:mainCRTStartup override; MinGW handles this
automatically.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
Add a complete Windows backend using the ConPTY (pseudo-console) API,
gated by #ifdef _WIN32 alongside the existing Unix pty code.

Platform abstraction:
- PtyHandle typedef (HANDLE on Windows, int on Unix) so pty_write,
  handle_input, handle_mouse, etc. share signatures across platforms
- WIN32_LEAN_AND_MEAN + NOGDI + NOUSER to avoid symbol clashes
  with raylib (Rectangle, CloseWindow, ShowCursor, PlaySound)

ConPTY implementation:
- pty_spawn_win32(): creates pipes, pseudo-console, attribute list,
  and launches the child shell (pwsh > powershell > cmd fallback)
- Threaded pipe reader to work around PeekNamedPipe unreliability
- pty_buf_drain() called each frame from the main loop
- pty_resize() delegates to ResizePseudoConsole
- pty_cleanup() with 1s graceful timeout before TerminateProcess

Integration:
- AttachConsole(ATTACH_PARENT_PROCESS) + CONOUT$ redirect so logs
  are visible when launched from a terminal (no-op from Explorer)
- WaitForSingleObject() for child exit detection (ConPTY pipes don't
  reliably close on child exit)
- Shell detection via SearchPathW, --shell flag support
- TERM=xterm-256color set via SetEnvironmentVariableA
- placement_iter freed on both platforms

Co-authored-by: Mitchell Hashimoto <m@mitchellh.com>
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
@deblasis deblasis force-pushed the feat/windows-port branch from 37a33db to 616c675 Compare April 21, 2026 01:16
@deblasis
Copy link
Copy Markdown
Author

deblasis commented Apr 21, 2026

Hi @res2k, thank you for taking the time to look into this.

I gave these changes a go, and I have some observations:

  • "Send an initial carriage return" - not necessary for me, I can see the initial process output just fine.
    In fact, this is actually unwanted input to the shell and should probably be removed.

You are right, removed.

  • PROPERTIES WIN32_EXECUTABLE TRUE wasn't necessary, either; even without it, output was never mistakenly sent to ghostling's console, yet ghostling's own output can be inspected.

Without WIN32_EXECUTABLE TRUE, launching from Explorer (clicking the icon) opens an extra console window alongside the raylib window. The child shell inherits the parent's console subsystem and opens its own. The STARTF_USESTDHANDLES with NULL handles prevents I/O from going to the wrong place, but it doesn't prevent the console window itself from appearing.

image

Launching from the terminal instead just swallows the output. I made a change that perhaps hits two birds with one stone (without harming animals). Now launching from terminal gets the output from the caller, same behaviour as linux/mac where there's no subsystem and logs go to the parent terminal.

  • The process exit detection doesn't quite work; it appears the child process exiting doesn't automatically results in the PTY getting closed, and so a PTY read error can't be used to detect that event. Instead, the WaitForSingleObject() on the process handle will detect the child having exited.

Good catch, addressed. The ConPTY pipe doesn't close on child exit, so we now poll the process handle with WaitForSingleObject() each frame to detect exit independently.

@res2k
Copy link
Copy Markdown

res2k commented Apr 21, 2026

Now launching from terminal gets the output from the caller, same behaviour as linux/mac where there's no subsystem.

This matches macOS/Linux behavior where logs go to the parent terminal.

Alright, that's a sensible approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants