pull/12403/merge
mattn 2026-06-03 14:35:18 +08:00 committed by GitHub
commit e2b1e9e1e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 548 additions and 30 deletions

View File

@ -61,6 +61,12 @@
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
.lazy = true,
},
.win32 = .{
// marlersoft/zigwin32 -- Win32 API bindings for the win32 apprt.
.url = "git+https://github.com/marlersoft/zigwin32#ec98bb4d9eea532320a8551720a9e3ec6de64994",
.hash = "win32-25.0.28-preview-mX5pFWMt5QPTVIGh3r2-OpPunpcCCjApyRbA6Zn6WALH",
.lazy = true,
},
// C libs
.dcimgui = .{ .path = "./pkg/dcimgui", .lazy = true },

View File

@ -16,6 +16,7 @@ pub const action = @import("apprt/action.zig");
pub const ipc = @import("apprt/ipc.zig");
pub const gtk = @import("apprt/gtk.zig");
pub const none = @import("apprt/none.zig");
pub const win32 = @import("apprt/win32.zig");
pub const browser = @import("apprt/browser.zig");
pub const embedded = @import("apprt/embedded.zig");
pub const surface = @import("apprt/surface.zig");
@ -43,6 +44,7 @@ pub const runtime = switch (build_config.artifact) {
.exe => switch (build_config.app_runtime) {
.none => none,
.gtk => gtk,
.win32 => win32,
},
.lib => embedded,
.wasm_module => browser,

View File

@ -11,11 +11,17 @@ pub const Runtime = enum {
/// approach to building the application.
gtk,
/// Win32. Native Windows application using the Win32 API with OpenGL
/// rendering.
win32,
pub fn default(target: std.Target) Runtime {
return switch (target.os.tag) {
// The Linux and FreeBSD default is GTK because it is a full
// featured application.
.linux, .freebsd => .gtk,
// Windows uses the native Win32 API.
.windows => .win32,
// Otherwise, we do NONE so we don't create an exe and we create
// libghostty. On macOS, Xcode is used to build the app that links
// to libghostty.

View File

@ -54,7 +54,7 @@ pub const Clipboard = enum(Backing) {
.{ .name = "GhosttyApprtClipboard" },
),
.none => void,
.none, .win32 => void,
};
};

8
src/apprt/win32.zig Normal file
View File

@ -0,0 +1,8 @@
// The required comptime API for any apprt.
pub const App = @import("win32/App.zig");
pub const Surface = @import("win32/Surface.zig");
pub const resourcesDir = @import("../os/main.zig").resourcesDir;
test {
@import("std").testing.refAllDecls(@This());
}

294
src/apprt/win32/App.zig Normal file
View File

@ -0,0 +1,294 @@
/// Win32 application runtime for Ghostty. This is a minimal native Windows
/// application using the Win32 API with OpenGL rendering.
const App = @This();
const std = @import("std");
const builtin = @import("builtin");
const win32 = @import("win32").everything;
const Allocator = std.mem.Allocator;
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const CoreSurface = @import("../../Surface.zig");
const Surface = @import("Surface.zig");
const renderer = @import("../../renderer.zig");
const log = std.log.scoped(.win32);
/// User-defined wakeup message sent via PostMessage to break out of
/// GetMessage and run the core app's tick.
const WM_WAKEUP = win32.WM_USER + 1;
/// The core app instance.
core_app: *CoreApp,
/// The configuration.
config: *Config,
/// The allocator.
alloc: Allocator,
/// Whether the app is running.
running: bool = true,
/// The main window handle.
hwnd: ?win32.HWND = null,
/// The surface for the main window.
surface: Surface = undefined,
pub fn init(
self: *App,
core_app: *CoreApp,
opts: struct {},
) !void {
_ = opts;
const alloc = core_app.alloc;
// Load configuration
var config = try Config.load(alloc);
errdefer config.deinit();
const config_ptr = try alloc.create(Config);
config_ptr.* = config;
self.* = .{
.core_app = core_app,
.config = config_ptr,
.alloc = alloc,
};
// Create the main window
try self.createWindow();
// Initialize the surface with OpenGL
try self.surface.init(self.hwnd.?);
// Store self pointer in window for use in wndProc. SetWindowLongPtrW
// returns the previous value, which for a freshly created window is 0;
// we don't care about it here.
_ = win32.SetWindowLongPtrW(
self.hwnd.?,
win32.GWLP_USERDATA,
@bitCast(@intFromPtr(self)),
);
// Initialize the core surface (terminal emulation + rendering)
try self.initCoreSurface();
}
pub fn run(self: *App) !void {
log.info("starting Win32 event loop", .{});
while (self.running) {
var msg: win32.MSG = std.mem.zeroes(win32.MSG);
const ret = win32.GetMessageW(&msg, null, 0, 0);
if (ret == 0) {
// WM_QUIT
self.running = false;
break;
}
if (ret == -1) {
log.err("GetMessage failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
}
_ = win32.TranslateMessage(&msg);
_ = win32.DispatchMessageW(&msg);
}
}
pub fn terminate(self: *App) void {
self.surface.deinit();
if (self.hwnd) |hwnd| {
if (win32.DestroyWindow(hwnd) == 0) {
log.warn("DestroyWindow failed: err={d}", .{@intFromEnum(win32.GetLastError())});
}
self.hwnd = null;
}
self.config.deinit();
self.alloc.destroy(self.config);
}
pub fn wakeup(self: *App) void {
if (self.hwnd) |hwnd| {
if (win32.PostMessageW(hwnd, WM_WAKEUP, 0, 0) == 0) {
log.warn("PostMessage(WM_WAKEUP) failed: err={d}", .{@intFromEnum(win32.GetLastError())});
}
}
}
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
) !bool {
_ = self;
_ = target;
_ = value;
switch (action) {
.quit => {
win32.PostQuitMessage(0);
return true;
},
.new_window => {
// TODO: implement multiple windows
return false;
},
else => return false,
}
}
pub fn performIpc(
_: Allocator,
_: apprt.ipc.Target,
comptime action: apprt.ipc.Action.Key,
_: apprt.ipc.Action.Value(action),
) !bool {
return false;
}
pub fn redrawInspector(_: *App, surface: *Surface) void {
surface.redrawInspector();
}
fn initCoreSurface(self: *App) !void {
const alloc = self.alloc;
// Set the app pointer on the surface
self.surface.app = self;
// Create the core surface
const core_surface = try alloc.create(CoreSurface);
errdefer alloc.destroy(core_surface);
// Register with the core app
try self.core_app.addSurface(&self.surface);
errdefer self.core_app.deleteSurface(&self.surface);
// Create a surface config
var config = try apprt.surface.newConfig(
self.core_app,
self.config,
.window,
);
defer config.deinit();
// Initialize the core surface
core_surface.init(
alloc,
&config,
self.core_app,
self,
&self.surface,
) catch |err| {
log.err("failed to initialize core surface: {}", .{err});
return err;
};
self.surface.core_surface = core_surface;
log.info("core surface initialized successfully", .{});
}
fn createWindow(self: *App) !void {
const class_name = win32.L("GhosttyWindow");
const hinstance = win32.GetModuleHandleW(null);
const wc: win32.WNDCLASSEXW = .{
.cbSize = @sizeOf(win32.WNDCLASSEXW),
.style = .{ .HREDRAW = 1, .VREDRAW = 1, .OWNDC = 1 },
.lpfnWndProc = wndProc,
.cbClsExtra = 0,
.cbWndExtra = 0,
.hInstance = hinstance,
.hIcon = null,
.hCursor = win32.LoadCursorW(null, win32.IDC_ARROW),
.hbrBackground = null,
.lpszMenuName = null,
.lpszClassName = class_name,
.hIconSm = null,
};
if (win32.RegisterClassExW(&wc) == 0) {
log.err("RegisterClassExW failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
}
const title = win32.L("Ghostty");
self.hwnd = win32.CreateWindowExW(
.{},
class_name,
title,
win32.WS_OVERLAPPEDWINDOW,
win32.CW_USEDEFAULT,
win32.CW_USEDEFAULT,
800,
600,
null,
null,
hinstance,
null,
) orelse {
log.err("CreateWindowExW failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
};
_ = win32.ShowWindow(self.hwnd.?, win32.SW_SHOWNORMAL);
_ = win32.UpdateWindow(self.hwnd.?);
}
fn getApp(hwnd: win32.HWND) ?*App {
const ptr = win32.GetWindowLongPtrW(hwnd, win32.GWLP_USERDATA);
if (ptr == 0) return null;
return @ptrFromInt(@as(usize, @bitCast(ptr)));
}
fn wndProc(hwnd: win32.HWND, msg: u32, wparam: win32.WPARAM, lparam: win32.LPARAM) callconv(.winapi) win32.LRESULT {
switch (msg) {
win32.WM_CLOSE => {
win32.PostQuitMessage(0);
return 0;
},
win32.WM_SIZE => {
if (getApp(hwnd)) |app| {
const width: u32 = @intCast(lparam & 0xFFFF);
const height: u32 = @intCast((lparam >> 16) & 0xFFFF);
if (width > 0 and height > 0) {
app.surface.width = width;
app.surface.height = height;
if (app.surface.core_surface) |core| {
core.sizeCallback(.{
.width = width,
.height = height,
}) catch |err| {
log.err("size callback error: {}", .{err});
};
}
}
}
return 0;
},
win32.WM_PAINT => {
var ps: win32.PAINTSTRUCT = std.mem.zeroes(win32.PAINTSTRUCT);
_ = win32.BeginPaint(hwnd, &ps);
if (getApp(hwnd)) |app| {
app.surface.swapBuffers();
}
_ = win32.EndPaint(hwnd, &ps);
return 0;
},
WM_WAKEUP => {
if (getApp(hwnd)) |app| {
app.core_app.tick(app) catch |err| {
log.err("core app tick failed: {}", .{err});
};
}
return 0;
},
else => return win32.DefWindowProcW(hwnd, msg, wparam, lparam),
}
}

192
src/apprt/win32/Surface.zig Normal file
View File

@ -0,0 +1,192 @@
/// Win32 surface - represents a terminal surface within a window.
/// Manages the WGL OpenGL context and provides the interface
/// expected by CoreSurface.
const Self = @This();
const std = @import("std");
const win32 = @import("win32").everything;
const Allocator = std.mem.Allocator;
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const CoreSurface = @import("../../Surface.zig");
const CoreApp = @import("../../App.zig");
const log = std.log.scoped(.win32_surface);
/// The window this surface belongs to.
hwnd: win32.HWND,
/// Pointer back to the App.
app: ?*App = null,
/// GDI device context.
hdc: ?win32.HDC = null,
/// OpenGL rendering context.
hglrc: ?win32.HGLRC = null,
/// The core surface, if initialized.
core_surface: ?*CoreSurface = null,
/// Window dimensions.
width: u32 = 800,
height: u32 = 600,
const App = @import("App.zig");
pub fn core(self: *Self) *CoreSurface {
return self.core_surface.?;
}
pub fn rtApp(self: *Self) *App {
return self.app.?;
}
pub fn init(self: *Self, hwnd: win32.HWND) !void {
self.* = .{ .hwnd = hwnd };
try self.initOpenGL();
}
pub fn deinit(self: *Self) void {
if (self.core_surface) |surface| {
surface.deinit();
// core_surface is allocated by CoreApp, freed there
}
if (self.hglrc) |hglrc| {
_ = win32.wglMakeCurrent(null, null);
_ = win32.wglDeleteContext(hglrc);
}
if (self.hdc) |hdc| {
_ = win32.ReleaseDC(self.hwnd, hdc);
}
}
fn initOpenGL(self: *Self) !void {
self.hdc = win32.GetDC(self.hwnd) orelse {
log.err("GetDC failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
};
var pfd: win32.PIXELFORMATDESCRIPTOR = std.mem.zeroes(win32.PIXELFORMATDESCRIPTOR);
pfd.nSize = @sizeOf(win32.PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = .{ .DRAW_TO_WINDOW = 1, .SUPPORT_OPENGL = 1, .DOUBLEBUFFER = 1 };
pfd.iPixelType = .RGBA;
pfd.cColorBits = 32;
pfd.cDepthBits = 24;
pfd.cStencilBits = 8;
pfd.iLayerType = .MAIN_PLANE;
const pixel_format = win32.ChoosePixelFormat(self.hdc, &pfd);
if (pixel_format == 0) {
log.err("ChoosePixelFormat failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
}
if (win32.SetPixelFormat(self.hdc, pixel_format, &pfd) == 0) {
log.err("SetPixelFormat failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
}
self.hglrc = win32.wglCreateContext(self.hdc) orelse {
log.err("wglCreateContext failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
};
if (win32.wglMakeCurrent(self.hdc, self.hglrc) == 0) {
log.err("wglMakeCurrent failed: err={d}", .{@intFromEnum(win32.GetLastError())});
return error.Win32Error;
}
log.info("WGL OpenGL context created successfully", .{});
}
pub fn swapBuffers(self: *Self) void {
if (self.hdc) |hdc| {
if (win32.SwapBuffers(hdc) == 0) {
log.warn("SwapBuffers failed: err={d}", .{@intFromEnum(win32.GetLastError())});
}
}
}
/// Make the WGL context current on the calling thread.
pub fn makeContextCurrent(self: *Self) void {
if (self.hdc) |hdc| {
if (self.hglrc) |hglrc| {
if (win32.wglMakeCurrent(hdc, hglrc) == 0) {
log.warn("wglMakeCurrent failed: err={d}", .{@intFromEnum(win32.GetLastError())});
}
}
}
}
/// Release the WGL context from the calling thread.
pub fn releaseContext() void {
if (win32.wglMakeCurrent(null, null) == 0) {
log.warn("wglMakeCurrent(null) failed: err={d}", .{@intFromEnum(win32.GetLastError())});
}
}
/// Release context from the main thread before handing off to renderer thread.
pub fn releaseMainThreadContext(self: *Self) void {
_ = self;
if (win32.wglMakeCurrent(null, null) == 0) {
log.warn("wglMakeCurrent(null) failed: err={d}", .{@intFromEnum(win32.GetLastError())});
}
}
// --- Interface methods required by CoreSurface ---
pub fn getContentScale(_: *const Self) !apprt.ContentScale {
// TODO: query DPI from the monitor
return .{ .x = 1.0, .y = 1.0 };
}
pub fn getSize(self: *const Self) !apprt.SurfaceSize {
return .{
.width = self.width,
.height = self.height,
};
}
pub fn getCursorPos(_: *const Self) !apprt.CursorPos {
// TODO: track mouse position
return .{ .x = 0, .y = 0 };
}
pub fn getTitle(_: *Self) ?[:0]const u8 {
return null;
}
pub fn close(_: *Self, _: bool) void {
// TODO: handle close with confirmation
}
pub fn supportsClipboard(_: *Self, clipboard: apprt.Clipboard) bool {
return clipboard == .standard;
}
pub fn clipboardRequest(
_: *Self,
_: apprt.Clipboard,
_: apprt.ClipboardRequest,
) !bool {
// TODO: implement clipboard read
return false;
}
pub fn setClipboard(
_: *Self,
_: apprt.Clipboard,
_: []const apprt.ClipboardContent,
_: bool,
) !void {
// TODO: implement clipboard write
}
pub fn defaultTermioEnv(_: *Self) !std.process.EnvMap {
// Return an empty env map; the shell will inherit the process env.
return std.process.EnvMap.init(std.heap.page_allocator);
}
pub fn redrawInspector(_: *Self) void {}

View File

@ -605,6 +605,14 @@ pub fn add(
switch (self.config.app_runtime) {
.none => {},
.gtk => try self.addGtkNg(step),
.win32 => {
step.linkSystemLibrary2("user32", .{});
step.linkSystemLibrary2("gdi32", .{});
step.linkSystemLibrary2("opengl32", .{});
if (b.lazyDependency("win32", .{})) |dep| {
step.root_module.addImport("win32", dep.module("win32"));
}
},
}
}

View File

@ -4647,7 +4647,7 @@ pub fn finalize(self: *Config) !void {
// Apprt-specific defaults
switch (build_config.app_runtime) {
.none => {},
.none, .win32 => {},
.gtk => {
switch (self.@"gtk-single-instance") {
.true, .false => {},

View File

@ -169,6 +169,11 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
apprt.gtk,
=> try prepareContext(null),
// Win32: WGL context is already current, load via null (GLAD
// uses opengl32.dll + wglGetProcAddress automatically).
apprt.win32,
=> try prepareContext(null),
apprt.embedded => {
// TODO(mitchellh): this does nothing today to allow libghostty
// to compile for OpenGL targets but libghostty is strictly
@ -190,48 +195,39 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
/// thread for final main thread setup requirements.
pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void {
_ = self;
_ = surface;
switch (apprt.runtime) {
apprt.win32 => {
// Release the WGL context from the main thread so the
// renderer thread can acquire it.
surface.releaseMainThreadContext();
},
else => {},
}
}
/// Callback called by renderer.Thread when it begins.
pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
_ = self;
_ = surface;
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => {
// GTK doesn't support threaded OpenGL operations as far as I can
// tell, so we use the renderer thread to setup all the state
// but then do the actual draws and texture syncs and all that
// on the main thread. As such, we don't do anything here.
},
apprt.embedded => {
// TODO(mitchellh): this does nothing today to allow libghostty
// to compile for OpenGL targets but libghostty is strictly
// broken for rendering on this platforms.
},
if (apprt.runtime == apprt.win32) {
// Win32: make the WGL context current on this thread and
// reload GL function pointers.
surface.makeContextCurrent();
try prepareContext(null);
}
// GTK and embedded don't need thread-specific GL setup.
}
/// Callback called by renderer.Thread when it exits.
pub fn threadExit(self: *const OpenGL) void {
_ = self;
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => {
// We don't need to do any unloading for GTK because we may
// be sharing the global bindings with other windows.
},
apprt.embedded => {
// TODO: see threadEnter
},
if (apprt.runtime == apprt.win32) {
// Release the WGL context from this thread.
apprt.win32.Surface.releaseContext();
}
// GTK and embedded don't need thread-specific GL cleanup.
}
pub fn displayRealized(self: *const OpenGL) void {

View File

@ -513,6 +513,12 @@ fn drawFrame(self: *Thread, now: bool) void {
} else {
self.renderer.drawFrame(false) catch |err|
log.warn("error drawing err={}", .{err});
// On Win32, we need to explicitly swap buffers after rendering
// since there's no toolkit managing the GL context for us.
if (comptime @hasDecl(apprt.runtime.Surface, "swapBuffers")) {
self.surface.swapBuffers();
}
}
}