windows: add support for the glfw backend on Windows

Changes:
- Add WindowsPty, which uses the ConPTY API to create a pseudo console
- Pty now selects between PosixPty and WindowsPty
- Windows support in Command, including the ability to launch a process with a pseudo console
- Enable Command tests on windows
- Add some environment variable abstractions to handle the missing libc APIs on Windows
- Windows version of ReadThread
This commit is contained in:
kcbanner
2023-10-29 04:03:06 -04:00
committed by Mitchell Hashimoto
parent 04ef21653f
commit 232df8de8f
13 changed files with 795 additions and 234 deletions

View File

@ -5,8 +5,8 @@
.dependencies = .{
// Zig libs
.libxev = .{
.url = "https://github.com/mitchellh/libxev/archive/5ecbc871f3bfa80fb7bf0fa853866cb93b99bc18.tar.gz",
.hash = "1220416854e424601ecc9814afb461a5dc9cf95db5917d82f794594a58ffc723b82c",
.url = "https://github.com/kcbanner/libxev/archive/d71ddeb19c6697875c2e0e68a49d04c05bc26199.tar.gz",
.hash = "1220a26265c30f5677ce28daf04afbd7a6ef5a17f5d3b5506f767fdfee7dc4225d2b",
},
.mach_glfw = .{
.url = "https://github.com/hexops/mach-glfw/archive/16dc95cc7f74ebbbdd848d9a2c3cc4afc5717708.tar.gz",

View File

@ -61,11 +61,8 @@ pub fn create(
// Find our resources directory once for the app so every launch
// hereafter can use this cached value.
var resources_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const resources_dir = if (try internal_os.resourcesDir(&resources_buf)) |dir|
try alloc.dupe(u8, dir)
else
null;
const resources_dir = try internal_os.resourcesDir(alloc);
errdefer if (resources_dir) |dir| alloc.free(dir);
app.* = .{
.alloc = alloc,

View File

@ -16,7 +16,6 @@
//!
//! TODO:
//!
//! * Windows
//! * Mac
//!
const Command = @This();
@ -24,6 +23,7 @@ const Command = @This();
const std = @import("std");
const builtin = @import("builtin");
const internal_os = @import("os/main.zig");
const windows = @import("windows.zig");
const TempDir = internal_os.TempDir;
const mem = std.mem;
const os = std.os;
@ -64,15 +64,22 @@ stderr: ?File = null,
/// exec process takes over, such as signal handlers, setsid, setuid, etc.
pre_exec: ?*const PreExecFn = null,
/// If set, then the process will be created attached to this pseudo console.
/// `stdin`, `stdout`, and `stderr` will be ignored if set.
pseudo_console: if (builtin.os.tag == .windows) ?windows.exp.HPCON else void =
if (builtin.os.tag == .windows) null else {},
/// User data that is sent to the callback. Set with setData and getData
/// for a more user-friendly API.
data: ?*anyopaque = null,
/// Process ID is set after start is called.
pid: ?i32 = null,
pid: ?os.pid_t = null,
/// The various methods a process may exit.
pub const Exit = union(enum) {
pub const Exit = if (builtin.os.tag == .windows) union(enum) {
Exited: u32,
} else union(enum) {
/// Exited by normal exit call, value is exit status
Exited: u8,
@ -109,6 +116,108 @@ pub fn start(self: *Command, alloc: Allocator) !void {
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
if (builtin.os.tag == .windows) {
const application_w = try std.unicode.utf8ToUtf16LeWithNull(arena, self.path);
const cwd_w = if (self.cwd) |cwd| try std.unicode.utf8ToUtf16LeWithNull(arena, cwd) else null;
const command_line_w = if (self.args.len > 0) b: {
const command_line = try windowsCreateCommandLine(arena, self.args);
break :b try std.unicode.utf8ToUtf16LeWithNull(arena, command_line);
} else null;
const env_w = if (self.env) |env_map| try createWindowsEnvBlock(arena, env_map) else null;
const any_null_fd = self.stdin == null or self.stdout == null or self.stderr == null;
const null_fd = if (any_null_fd) try windows.OpenFile(
&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' },
.{
.access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE,
.share_access = windows.FILE_SHARE_READ,
.creation = windows.OPEN_EXISTING,
.io_mode = .blocking,
},
) else null;
defer if (null_fd) |fd| std.os.close(fd);
// TODO: In the case of having FDs instead of pty, need to set up attributes such that the
// child process only inherits these handles, then set bInheritsHandles below.
const attribute_list, const stdin, const stdout, const stderr = if (self.pseudo_console) |pseudo_console| b: {
var attribute_list_size: usize = undefined;
_ = windows.exp.kernel32.InitializeProcThreadAttributeList(
null,
1,
0,
&attribute_list_size,
);
const attribute_list_buf = try arena.alloc(u8, attribute_list_size);
if (windows.exp.kernel32.InitializeProcThreadAttributeList(
attribute_list_buf.ptr,
1,
0,
&attribute_list_size,
) == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
if (windows.exp.kernel32.UpdateProcThreadAttribute(
attribute_list_buf.ptr,
0,
windows.exp.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
pseudo_console,
@sizeOf(windows.exp.HPCON),
null,
null,
) == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
break :b .{ attribute_list_buf.ptr, null, null, null };
} else b: {
const stdin = if (self.stdin) |f| f.handle else null_fd.?;
const stdout = if (self.stdout) |f| f.handle else null_fd.?;
const stderr = if (self.stderr) |f| f.handle else null_fd.?;
break :b .{ null, stdin, stdout, stderr };
};
var startup_info_ex = windows.exp.STARTUPINFOEX{
.StartupInfo = .{
.cb = if (attribute_list != null) @sizeOf(windows.exp.STARTUPINFOEX) else @sizeOf(windows.STARTUPINFOW),
.hStdError = stderr,
.hStdOutput = stdout,
.hStdInput = stdin,
.dwFlags = windows.STARTF_USESTDHANDLES,
.lpReserved = null,
.lpDesktop = null,
.lpTitle = null,
.dwX = 0,
.dwY = 0,
.dwXSize = 0,
.dwYSize = 0,
.dwXCountChars = 0,
.dwYCountChars = 0,
.dwFillAttribute = 0,
.wShowWindow = 0,
.cbReserved2 = 0,
.lpReserved2 = null,
},
.lpAttributeList = attribute_list,
};
var flags: windows.DWORD = windows.exp.CREATE_UNICODE_ENVIRONMENT;
if (attribute_list != null) flags |= windows.exp.EXTENDED_STARTUPINFO_PRESENT;
var process_information: windows.PROCESS_INFORMATION = undefined;
if (windows.exp.kernel32.CreateProcessW(
application_w.ptr,
if (command_line_w) |w| w.ptr else null,
null,
null,
windows.TRUE,
flags,
if (env_w) |w| w.ptr else null,
if (cwd_w) |w| w.ptr else null,
@ptrCast(&startup_info_ex.StartupInfo),
&process_information,
) == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
self.pid = process_information.hProcess;
} else {
// Null-terminate all our arguments
const pathZ = try arena.dupeZ(u8, self.path);
const argsZ = try arena.allocSentinel(?[*:0]u8, self.args.len, null);
@ -122,9 +231,6 @@ pub fn start(self: *Command, alloc: Allocator) !void {
else
@compileError("missing env vars");
if (builtin.os.tag == .windows)
@panic("start not implemented on windows");
// Fork
const pid = try std.os.fork();
if (pid != 0) {
@ -148,6 +254,7 @@ pub fn start(self: *Command, alloc: Allocator) !void {
// Finally, replace our process.
_ = std.os.execveZ(pathZ, argsZ, envp) catch null;
}
}
fn setupFd(src: File.Handle, target: i32) !void {
@ -190,8 +297,18 @@ fn setupFd(src: File.Handle, target: i32) !void {
/// Wait for the command to exit and return information about how it exited.
pub fn wait(self: Command, block: bool) !Exit {
if (builtin.os.tag == .windows)
@panic("wait not implemented on windows");
if (builtin.os.tag == .windows) {
// Block until the process exits. This returns immediately if the process already exited.
const result = windows.kernel32.WaitForSingleObject(self.pid.?, windows.INFINITE);
if (result == windows.WAIT_FAILED) return windows.unexpectedError(windows.kernel32.GetLastError());
var exit_code: windows.DWORD = undefined;
var has_code = windows.kernel32.GetExitCodeProcess(self.pid.?, &exit_code) != 0;
if (!has_code) return windows.unexpectedError(windows.kernel32.GetLastError());
return .{ .Exited = exit_code };
}
const res = if (block) std.os.waitpid(self.pid.?, 0) else res: {
// We specify NOHANG because its not our fault if the process we launch
@ -325,6 +442,79 @@ fn createNullDelimitedEnvMap(arena: mem.Allocator, env_map: *const EnvMap) ![:nu
return envp_buf;
}
// Copied from Zig. This is a publicly exported function but there is no
// way to get it from the std package.
pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 {
// count bytes needed
const max_chars_needed = x: {
var max_chars_needed: usize = 4; // 4 for the final 4 null bytes
var it = env_map.iterator();
while (it.next()) |pair| {
// +1 for '='
// +1 for null byte
max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2;
}
break :x max_chars_needed;
};
const result = try allocator.alloc(u16, max_chars_needed);
errdefer allocator.free(result);
var it = env_map.iterator();
var i: usize = 0;
while (it.next()) |pair| {
i += try std.unicode.utf8ToUtf16Le(result[i..], pair.key_ptr.*);
result[i] = '=';
i += 1;
i += try std.unicode.utf8ToUtf16Le(result[i..], pair.value_ptr.*);
result[i] = 0;
i += 1;
}
result[i] = 0;
i += 1;
result[i] = 0;
i += 1;
result[i] = 0;
i += 1;
result[i] = 0;
i += 1;
return try allocator.realloc(result, i);
}
/// Copied from Zig. This function could be made public in child_process.zig instead.
fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 {
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
for (argv, 0..) |arg, arg_i| {
if (arg_i != 0) try buf.append(' ');
if (mem.indexOfAny(u8, arg, " \t\n\"") == null) {
try buf.appendSlice(arg);
continue;
}
try buf.append('"');
var backslash_count: usize = 0;
for (arg) |byte| {
switch (byte) {
'\\' => backslash_count += 1,
'"' => {
try buf.appendNTimes('\\', backslash_count * 2 + 1);
try buf.append('"');
backslash_count = 0;
},
else => {
try buf.appendNTimes('\\', backslash_count);
try buf.append(byte);
backslash_count = 0;
},
}
}
try buf.appendNTimes('\\', backslash_count * 2);
try buf.append('"');
}
return buf.toOwnedSliceSentinel(0);
}
test "createNullDelimitedEnvMap" {
const allocator = testing.allocator;
var envmap = EnvMap.init(allocator);
@ -378,14 +568,26 @@ test "Command: pre exec" {
try testing.expect(exit.Exited == 42);
}
fn createTestStdout(dir: std.fs.Dir) !File {
const file = try dir.createFile("stdout.txt", .{ .read = true });
if (builtin.os.tag == .windows) {
try windows.SetHandleInformation(file.handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT);
}
return file;
}
test "Command: redirect stdout to file" {
if (builtin.os.tag == .windows) return error.SkipZigTest;
var td = try TempDir.init();
defer td.deinit();
var stdout = try td.dir.createFile("stdout.txt", .{ .read = true });
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var cmd: Command = .{
var cmd: Command = if (builtin.os.tag == .windows) .{
.path = "C:\\Windows\\System32\\whoami.exe",
.args = &.{"C:\\Windows\\System32\\whoami.exe"},
.stdout = stdout,
} else .{
.path = "/usr/bin/env",
.args = &.{ "/usr/bin/env", "-v" },
.stdout = stdout,
@ -395,7 +597,7 @@ test "Command: redirect stdout to file" {
try testing.expect(cmd.pid != null);
const exit = try cmd.wait(true);
try testing.expect(exit == .Exited);
try testing.expect(exit.Exited == 0);
try testing.expectEqual(@as(u32, 0), @as(u32, exit.Exited));
// Read our stdout
try stdout.seekTo(0);
@ -405,17 +607,21 @@ test "Command: redirect stdout to file" {
}
test "Command: custom env vars" {
if (builtin.os.tag == .windows) return error.SkipZigTest;
var td = try TempDir.init();
defer td.deinit();
var stdout = try td.dir.createFile("stdout.txt", .{ .read = true });
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var env = EnvMap.init(testing.allocator);
defer env.deinit();
try env.put("VALUE", "hello");
var cmd: Command = .{
var cmd: Command = if (builtin.os.tag == .windows) .{
.path = "C:\\Windows\\System32\\cmd.exe",
.args = &.{ "C:\\Windows\\System32\\cmd.exe", "/C", "echo %VALUE%" },
.stdout = stdout,
.env = &env,
} else .{
.path = "/usr/bin/env",
.args = &.{ "/usr/bin/env", "sh", "-c", "echo $VALUE" },
.stdout = stdout,
@ -432,17 +638,26 @@ test "Command: custom env vars" {
try stdout.seekTo(0);
const contents = try stdout.readToEndAlloc(testing.allocator, 4096);
defer testing.allocator.free(contents);
if (builtin.os.tag == .windows) {
try testing.expectEqualStrings("hello\r\n", contents);
} else {
try testing.expectEqualStrings("hello\n", contents);
}
}
test "Command: custom working directory" {
if (builtin.os.tag == .windows) return error.SkipZigTest;
var td = try TempDir.init();
defer td.deinit();
var stdout = try td.dir.createFile("stdout.txt", .{ .read = true });
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var cmd: Command = .{
var cmd: Command = if (builtin.os.tag == .windows) .{
.path = "C:\\Windows\\System32\\cmd.exe",
.args = &.{ "C:\\Windows\\System32\\cmd.exe", "/C", "cd" },
.stdout = stdout,
.cwd = "C:\\Windows\\System32",
} else .{
.path = "/usr/bin/env",
.args = &.{ "/usr/bin/env", "sh", "-c", "pwd" },
.stdout = stdout,
@ -459,5 +674,10 @@ test "Command: custom working directory" {
try stdout.seekTo(0);
const contents = try stdout.readToEndAlloc(testing.allocator, 4096);
defer testing.allocator.free(contents);
if (builtin.os.tag == .windows) {
try testing.expectEqualStrings("C:\\Windows\\System32\r\n", contents);
} else {
try testing.expectEqualStrings("/usr/bin\n", contents);
}
}

View File

@ -1,11 +1,6 @@
//! Linux PTY creation and management. This is just a thin layer on top
//! of Linux syscalls. The caller is responsible for detail-oriented handling
//! of the returned file handles.
const Pty = @This();
const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const windows = @import("windows.zig");
const fd_t = std.os.fd_t;
const c = switch (builtin.os.tag) {
@ -13,7 +8,6 @@ const c = switch (builtin.os.tag) {
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
.windows => {},
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
@ -22,16 +16,10 @@ const c = switch (builtin.os.tag) {
const log = std.log.scoped(.pty);
// https://github.com/ziglang/zig/issues/13277
// Once above is fixed, use `c.TIOCSCTTY`
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ;
const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ;
/// Redeclare this winsize struct so we can just use a Zig struct. This
/// layout should be correct on all tested platforms. The defaults on this
/// are some reasonable screen size but you should probably not use them.
const winsize = extern struct {
pub const winsize = extern struct {
ws_row: u16 = 100,
ws_col: u16 = 80,
ws_xpixel: u16 = 800,
@ -40,14 +28,28 @@ const winsize = extern struct {
pub extern "c" fn setsid() std.c.pid_t;
/// The file descriptors for the master and slave side of the pty.
master: fd_t,
slave: fd_t,
pub const Pty = if (builtin.os.tag == .windows)
WindowsPty
else
PosixPty;
/// Open a new PTY with the given initial size.
pub fn open(size: winsize) !Pty {
if (builtin.os.tag == .windows) return error.NotImplementedOnWindows;
/// Linux PTY creation and management. This is just a thin layer on top
/// of Linux syscalls. The caller is responsible for detail-oriented handling
/// of the returned file handles.
pub const PosixPty = struct {
// https://github.com/ziglang/zig/issues/13277
// Once above is fixed, use `c.TIOCSCTTY`
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ;
const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ;
/// The file descriptors for the master and slave side of the pty.
master: fd_t,
slave: fd_t,
/// Open a new PTY with the given initial size.
pub fn open(size: winsize) !Pty {
// Need to copy so that it becomes non-const.
var sizeCopy = size;
@ -79,35 +81,32 @@ pub fn open(size: winsize) !Pty {
.master = master_fd,
.slave = slave_fd,
};
}
}
pub fn deinit(self: *Pty) void {
pub fn deinit(self: *Pty) void {
_ = std.os.system.close(self.master);
_ = std.os.system.close(self.slave);
self.* = undefined;
}
/// Return the size of the pty.
pub fn getSize(self: Pty) !winsize {
if (builtin.os.tag == .windows) return error.NotImplementedOnWindows;
}
/// Return the size of the pty.
pub fn getSize(self: Pty) !winsize {
var ws: winsize = undefined;
if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0)
return error.IoctlFailed;
return ws;
}
}
/// Set the size of the pty.
pub fn setSize(self: Pty, size: winsize) !void {
if (builtin.os.tag == .windows) return error.NotImplementedOnWindows;
/// Set the size of the pty.
pub fn setSize(self: *Pty, size: winsize) !void {
if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0)
return error.IoctlFailed;
}
}
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
pub fn childPreExec(self: Pty) !void {
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
pub fn childPreExec(self: Pty) !void {
// Create a new process group
if (setsid() < 0) return error.ProcessGroupFailed;
@ -125,10 +124,137 @@ pub fn childPreExec(self: Pty) !void {
std.os.close(self.master);
// TODO: reset signals
}
}
};
/// Windows PTY creation and management.
pub const WindowsPty = struct {
// Process-wide counter for pipe names
var pipe_name_counter = std.atomic.Atomic(u32).init(1);
out_pipe: windows.HANDLE,
in_pipe: windows.HANDLE,
out_pipe_pty: windows.HANDLE,
in_pipe_pty: windows.HANDLE,
pseudo_console: windows.exp.HPCON,
size: winsize,
/// Open a new PTY with the given initial size.
pub fn open(size: winsize) !Pty {
var pty: Pty = undefined;
var pipe_path_buf: [128]u8 = undefined;
var pipe_path_buf_w: [128]u16 = undefined;
const pipe_path = std.fmt.bufPrintZ(
&pipe_path_buf,
"\\\\.\\pipe\\LOCAL\\ghostty-pty-{d}-{d}",
.{ windows.kernel32.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .Monotonic) },
) catch unreachable;
const pipe_path_w_len = std.unicode.utf8ToUtf16Le(&pipe_path_buf_w, pipe_path) catch unreachable;
pipe_path_buf_w[pipe_path_w_len] = 0;
const pipe_path_w = pipe_path_buf_w[0..pipe_path_w_len :0];
const security_attributes = windows.SECURITY_ATTRIBUTES{
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
.bInheritHandle = windows.FALSE,
.lpSecurityDescriptor = null,
};
pty.in_pipe = windows.kernel32.CreateNamedPipeW(
pipe_path_w.ptr,
windows.PIPE_ACCESS_OUTBOUND | windows.exp.FILE_FLAG_FIRST_PIPE_INSTANCE | windows.FILE_FLAG_OVERLAPPED,
windows.PIPE_TYPE_BYTE,
1,
4096,
4096,
0,
&security_attributes,
);
if (pty.in_pipe == windows.INVALID_HANDLE_VALUE) return windows.unexpectedError(windows.kernel32.GetLastError());
errdefer _ = windows.kernel32.CloseHandle(pty.in_pipe);
var security_attributes_read = security_attributes;
pty.in_pipe_pty = windows.kernel32.CreateFileW(
pipe_path_w.ptr,
windows.GENERIC_READ,
0,
&security_attributes_read,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
null,
);
if (pty.in_pipe_pty == windows.INVALID_HANDLE_VALUE) return windows.unexpectedError(windows.kernel32.GetLastError());
errdefer _ = windows.kernel32.CloseHandle(pty.in_pipe_pty);
// The in_pipe needs to be created as a named pipe, since anonymous pipes created with CreatePipe do not
// support overlapped operations, and the IOCP backend of libxev only uses overlapped operations on files.
//
// It would be ideal to use CreatePipe here, so that our pipe isn't visible to any other processes.
// if (windows.exp.kernel32.CreatePipe(&pty.in_pipe_pty, &pty.in_pipe, null, 0) == 0) {
// return windows.unexpectedError(windows.kernel32.GetLastError());
// }
// errdefer {
// _ = windows.kernel32.CloseHandle(pty.in_pipe_pty);
// _ = windows.kernel32.CloseHandle(pty.in_pipe);
// }
if (windows.exp.kernel32.CreatePipe(&pty.out_pipe, &pty.out_pipe_pty, null, 0) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
errdefer {
_ = windows.kernel32.CloseHandle(pty.out_pipe);
_ = windows.kernel32.CloseHandle(pty.out_pipe_pty);
}
try windows.SetHandleInformation(pty.in_pipe, windows.HANDLE_FLAG_INHERIT, 0);
try windows.SetHandleInformation(pty.in_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0);
try windows.SetHandleInformation(pty.out_pipe, windows.HANDLE_FLAG_INHERIT, 0);
try windows.SetHandleInformation(pty.out_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0);
const result = windows.exp.kernel32.CreatePseudoConsole(
.{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) },
pty.in_pipe_pty,
pty.out_pipe_pty,
0,
&pty.pseudo_console,
);
if (result != windows.S_OK) return error.Unexpected;
pty.size = size;
return pty;
}
pub fn deinit(self: *Pty) void {
_ = windows.kernel32.CloseHandle(self.in_pipe_pty);
_ = windows.kernel32.CloseHandle(self.in_pipe);
_ = windows.kernel32.CloseHandle(self.out_pipe_pty);
_ = windows.kernel32.CloseHandle(self.out_pipe);
_ = windows.exp.kernel32.ClosePseudoConsole(self.pseudo_console);
self.* = undefined;
}
/// Return the size of the pty.
pub fn getSize(self: Pty) !winsize {
return self.size;
}
/// Set the size of the pty.
pub fn setSize(self: *Pty, size: winsize) !void {
const result = windows.exp.kernel32.ResizePseudoConsole(
self.pseudo_console,
.{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) },
);
if (result != windows.S_OK) return error.ResizeFailed;
self.size = size;
}
};
const testing = std.testing;
test {
if (builtin.os.tag == .windows) return error.SkipZigTest;
var ws: winsize = .{
.ws_row = 50,
.ws_col = 80,
@ -136,7 +262,7 @@ test {
.ws_ypixel = 1,
};
var pty = try open(ws);
var pty = try Pty.open(ws);
defer pty.deinit();
// Initialize size should match what we gave it

View File

@ -24,7 +24,7 @@ const renderer = @import("renderer.zig");
const termio = @import("termio.zig");
const objc = @import("objc");
const imgui = @import("imgui");
const Pty = @import("Pty.zig");
const Pty = @import("Pty.zig").Pty;
const font = @import("font/main.zig");
const Command = @import("Command.zig");
const trace = @import("tracy").trace;
@ -941,7 +941,7 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void {
/// This queues a render operation with the renderer thread. The render
/// isn't guaranteed to happen immediately but it will happen as soon as
/// practical.
fn queueRender(self: *const Surface) !void {
fn queueRender(self: *Surface) !void {
try self.renderer_thread.wakeup.notify();
}

View File

@ -1134,6 +1134,19 @@ pub fn finalize(self: *Config) !void {
}
}
if (builtin.target.os.tag == .windows) {
if (self.command == null) {
log.warn("no default shell found, will default to using cmd", .{});
self.command = "cmd.exe";
}
if (wd_home) {
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
if (try internal_os.home(&buf)) |home| {
self.@"working-directory" = try alloc.dupe(u8, home);
}
}
} else {
// We need the passwd entry for the remainder
const pw = try internal_os.passwd.get(alloc);
if (self.command == null) {
@ -1155,6 +1168,7 @@ pub fn finalize(self: *Config) !void {
}
}
}
}
// If we have the special value "inherit" then set it to null which
// does the same. In the future we should change to a tagged union.

View File

@ -243,7 +243,8 @@ pub const GlobalState = struct {
// maybe once for logging) so for now this is an easy way to do
// this. Env vars are useful for logging too because they are
// easy to set.
if (std.os.getenv("GHOSTTY_LOG")) |v| {
if ((try internal_os.getEnvVarOwned(self.alloc, "GHOSTTY_LOG"))) |v| {
defer self.alloc.free(v);
if (v.len > 0) {
self.logging = .{ .stderr = {} };
}
@ -265,7 +266,7 @@ pub const GlobalState = struct {
// We need to make sure the process locale is set properly. Locale
// affects a lot of behaviors in a shell.
internal_os.ensureLocale();
try internal_os.ensureLocale(self.alloc);
}
/// Cleans up the global state. This doesn't _need_ to be called but

View File

@ -46,3 +46,36 @@ test "appendEnv existing" {
try testing.expectEqualStrings(result, "a:b:foo");
}
}
extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int;
extern "c" fn unsetenv(name: ?[*]const u8) c_int;
extern "c" fn _putenv_s(varname: ?[*]const u8, value_string: ?[*]const u8) c_int;
pub fn setEnv(key: [:0]const u8, value: [:0]const u8) c_int {
if (builtin.os.tag == .windows) {
return _putenv_s(key.ptr, value.ptr);
} else {
return setenv(key.ptr, value.ptr, 1);
}
}
pub fn unsetEnv(key: [:0]const u8) c_int {
if (builtin.os.tag == .windows) {
return _putenv_s(key.ptr, "");
} else {
return unsetenv(key.ptr);
}
}
/// Returns the value of an environment variable, or null if not found.
/// The returned value is always allocated so it must be freed.
pub fn getEnvVarOwned(alloc: std.mem.Allocator, key: []const u8) !?[]u8 {
if (std.process.getEnvVarOwned(alloc, key)) |v| {
return v;
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
}
return null;
}

View File

@ -2,16 +2,18 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const objc = @import("objc");
const internal_os = @import("main.zig");
const log = std.log.scoped(.os);
/// Ensure that the locale is set.
pub fn ensureLocale() void {
pub fn ensureLocale(alloc: std.mem.Allocator) !void {
assert(builtin.link_libc);
// Get our LANG env var. We use this many times but we also need
// the original value later.
const lang = std.os.getenv("LANG") orelse "";
const lang = try internal_os.getEnvVarOwned(alloc, "LANG");
defer if (lang) |v| alloc.free(v);
// On macOS, pre-populate the LANG env var with system preferences.
// When launching the .app, LANG is not set so we must query it from the
@ -33,12 +35,13 @@ pub fn ensureLocale() void {
// setlocale failed. This is probably because the LANG env var is
// invalid. Try to set it without the LANG var set to use the system
// default.
if (std.os.getenv("LANG")) |old_lang| {
if ((try internal_os.getEnvVarOwned(alloc, "LANG"))) |old_lang| {
defer alloc.free(old_lang);
if (old_lang.len > 0) {
// We don't need to do both of these things but we do them
// both to be sure that lang is either empty or unset completely.
_ = setenv("LANG", "", 1);
_ = unsetenv("LANG");
_ = internal_os.setEnv("LANG", "");
_ = internal_os.unsetEnv("LANG");
if (setlocale(LC_ALL, "")) |v| {
log.info("setlocale after unset lang result={s}", .{v});
@ -54,7 +57,7 @@ pub fn ensureLocale() void {
// Failure again... fallback to en_US.UTF-8
log.warn("setlocale failed with LANG and system default. Falling back to en_US.UTF-8", .{});
if (setlocale(LC_ALL, "en_US.UTF-8")) |v| {
_ = setenv("LANG", "en_US.UTF-8", 1);
_ = internal_os.setEnv("LANG", "en_US.UTF-8");
log.info("setlocale default result={s}", .{v});
return;
} else log.err("setlocale failed even with the fallback, uncertain results", .{});
@ -95,7 +98,7 @@ fn setLangFromCocoa() void {
log.info("detected system locale={s}", .{env_value});
// Set it onto our environment
if (setenv("LANG", env_value.ptr, 1) < 0) {
if (internal_os.setEnv("LANG", env_value.ptr) < 0) {
log.err("error setting locale env var", .{});
return;
}
@ -104,8 +107,6 @@ fn setLangFromCocoa() void {
const LC_ALL: c_int = 6; // from locale.h
const LC_ALL_MASK: c_int = 0x7fffffff; // from locale.h
const locale_t = ?*anyopaque;
extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int;
extern "c" fn unsetenv(name: ?[*]const u8) c_int;
extern "c" fn setlocale(category: c_int, locale: ?[*]const u8) ?[*:0]u8;
extern "c" fn newlocale(category: c_int, locale: ?[*]const u8, base: locale_t) locale_t;
extern "c" fn freelocale(v: locale_t) void;

View File

@ -30,7 +30,7 @@ pub const Entry = struct {
/// Get the passwd entry for the currently executing user.
pub fn get(alloc: Allocator) !Entry {
if (builtin.os.tag == .windows) @panic("todo: windows");
if (builtin.os.tag == .windows) @compileError("passwd is not available on windows");
var buf: [1024]u8 = undefined;
var pw: c.struct_passwd = undefined;
@ -63,7 +63,7 @@ pub fn get(alloc: Allocator) !Entry {
// Note: we wrap our getent call in a /bin/sh login shell because
// some operating systems (NixOS tested) don't set the PATH for various
// utilities properly until we get a login shell.
const Pty = @import("../Pty.zig");
const Pty = @import("../Pty.zig").Pty;
var pty = try Pty.open(.{});
defer pty.deinit();
var cmd: internal_os.FlatpakHostCommand = .{

View File

@ -4,20 +4,17 @@ const Allocator = std.mem.Allocator;
/// Gets the directory to the bundled resources directory, if it
/// exists (not all platforms or packages have it). The output is
/// written to the given buffer if we need to allocate. Note that
/// the output is not ALWAYS written to the buffer and may refer to
/// static memory.
/// owned by the caller.
///
/// This is highly Ghostty-specific and can likely be generalized at
/// some point but we can cross that bridge if we ever need to.
///
/// This returns error.OutOfMemory is buffer is not big enough.
pub fn resourcesDir(buf: []u8) !?[]const u8 {
pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// If we have an environment variable set, we always use that.
if (std.os.getenv("GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) {
return dir;
}
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
}
// This is the sentinel value we look for in the path to know
@ -30,21 +27,22 @@ pub fn resourcesDir(buf: []u8) !?[]const u8 {
// We have an exe path! Climb the tree looking for the terminfo
// bundle as we expect it.
var dir_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
while (std.fs.path.dirname(exe)) |dir| {
exe = dir;
// On MacOS, we look for the app bundle path.
if (comptime builtin.target.isDarwin()) {
if (try maybeDir(buf, dir, "Contents/Resources", sentinel)) |v| {
return v;
if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| {
return try alloc.dupe(u8, v);
}
}
// On all platforms, we look for a /usr/share style path. This
// is valid even on Mac since there is nothing that requires
// Ghostty to be in an app bundle.
if (try maybeDir(buf, dir, "share", sentinel)) |v| {
return v;
if (try maybeDir(&dir_buf, dir, "share", sentinel)) |v| {
return try alloc.dupe(u8, v);
}
}

View File

@ -10,7 +10,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const EnvMap = std.process.EnvMap;
const termio = @import("../termio.zig");
const Command = @import("../Command.zig");
const Pty = @import("../Pty.zig");
const Pty = @import("../Pty.zig").Pty;
const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool;
const terminal = @import("../terminal/main.zig");
const terminfo = @import("../terminfo/main.zig");
@ -23,6 +23,7 @@ const fastmem = @import("../fastmem.zig");
const internal_os = @import("../os/main.zig");
const configpkg = @import("../config.zig");
const shell_integration = @import("shell_integration.zig");
const windows = @import("../windows.zig");
const log = std.log.scoped(.io_exec);
@ -184,7 +185,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
const alloc = self.alloc;
// Start our subprocess
const master_fd = try self.subprocess.start(alloc);
try self.subprocess.start(alloc);
errdefer self.subprocess.stop();
const pid = pid: {
const command = self.subprocess.command orelse return error.ProcessNotStarted;
@ -193,7 +194,15 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
// Create our pipe that we'll use to kill our read thread.
// pipe[0] is the read end, pipe[1] is the write end.
const pipe = try std.os.pipe();
const pipe = if (builtin.os.tag == .windows) pipe: {
var read: windows.HANDLE = undefined;
var write: windows.HANDLE = undefined;
if (windows.exp.kernel32.CreatePipe(&read, &write, null, 0) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
break :pipe .{ read, write };
} else try std.os.pipe();
errdefer std.os.close(pipe[0]);
errdefer std.os.close(pipe[1]);
@ -202,7 +211,8 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
errdefer alloc.destroy(ev_data_ptr);
// Setup our stream so that we can write.
var stream = xev.Stream.initFd(master_fd);
const write_fd = if (builtin.os.tag == .windows) self.subprocess.pty.?.in_pipe else self.subprocess.pty.?.master;
var stream = xev.Stream.initFd(write_fd);
errdefer stream.deinit();
// Wakeup watcher for the writer thread.
@ -262,10 +272,11 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
);
// Start our reader thread
const read_fd = if (builtin.os.tag == .windows) self.subprocess.pty.?.out_pipe else self.subprocess.pty.?.master;
const read_thread = try std.Thread.spawn(
.{},
ReadThread.threadMain,
.{ master_fd, ev_data_ptr, pipe[0] },
if (builtin.os.tag == .windows) ReadThread.threadMainWindows else ReadThread.threadMainPosix,
.{ read_fd, ev_data_ptr, pipe[0] },
);
read_thread.setName("io-reader") catch {};
@ -275,6 +286,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
.ev = ev_data_ptr,
.read_thread = read_thread,
.read_thread_pipe = pipe[1],
.read_thread_fd = if (builtin.os.tag == .windows) read_fd else {},
};
}
@ -291,6 +303,17 @@ pub fn threadExit(self: *Exec, data: ThreadData) void {
// a particularly noisy process.
_ = std.os.write(data.read_thread_pipe, "x") catch |err|
log.warn("error writing to read thread quit pipe err={}", .{err});
if (builtin.os.tag == .windows) {
// Interrupt the blocking read so the thread can see the quit message
if (windows.kernel32.CancelIoEx(data.read_thread_fd, null) == 0) {
switch (windows.kernel32.GetLastError()) {
.NOT_FOUND => {},
else => |err| log.warn("error interrupting read thread err={}", .{err}),
}
}
}
data.read_thread.join();
}
@ -476,7 +499,7 @@ pub inline fn queueWrite(self: *Exec, data: []const u8, linefeed: bool) !void {
break :slice buf[0..buf_i];
};
// for (slice) |b| log.warn("write: {x}", .{b});
//for (slice) |b| log.warn("write: {x}", .{b});
ev.data_stream.queueWrite(
ev.loop,
@ -500,6 +523,7 @@ const ThreadData = struct {
/// Our read thread
read_thread: std.Thread,
read_thread_pipe: std.os.fd_t,
read_thread_fd: if (builtin.os.tag == .windows) std.os.fd_t else void,
pub fn deinit(self: *ThreadData) void {
std.os.close(self.read_thread_pipe);
@ -662,7 +686,8 @@ const Subprocess = struct {
const alloc = arena.allocator();
// Determine the path to the binary we're executing
const path = (try Command.expandPath(alloc, opts.full_config.command orelse "sh")) orelse
const default_command = if (builtin.os.tag == .windows) "cmd.exe" else "sh";
const path = (try Command.expandPath(alloc, opts.full_config.command orelse default_command)) orelse
return error.CommandNotFound;
// On macOS, we launch the program as a login shell. This is a Mac-specific
@ -833,7 +858,7 @@ const Subprocess = struct {
/// Start the subprocess. If the subprocess is already started this
/// will crash.
pub fn start(self: *Subprocess, alloc: Allocator) !std.os.fd_t {
pub fn start(self: *Subprocess, alloc: Allocator) !void {
assert(self.pty == null and self.command == null);
// Create our pty
@ -886,8 +911,6 @@ const Subprocess = struct {
// wait right now but that is fine too. This lets us read the
// parent and detect EOF.
_ = std.os.close(pty.slave);
return pty.master;
}
// If we can't access the cwd, then don't set any cwd and inherit.
@ -908,10 +931,11 @@ const Subprocess = struct {
.args = self.args,
.env = &self.env,
.cwd = cwd,
.stdin = .{ .handle = pty.slave },
.stdout = .{ .handle = pty.slave },
.stderr = .{ .handle = pty.slave },
.pre_exec = (struct {
.stdin = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave },
.stdout = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave },
.stderr = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave },
.pseudo_console = if (builtin.os.tag == .windows) pty.pseudo_console else {},
.pre_exec = if (builtin.os.tag == .windows) null else (struct {
fn callback(cmd: *Command) void {
const p = cmd.getData(Pty) orelse unreachable;
p.childPreExec() catch |err|
@ -925,7 +949,6 @@ const Subprocess = struct {
log.info("started subcommand path={s} pid={?}", .{ self.path, cmd.pid });
self.command = cmd;
return pty.master;
}
/// Called to notify that we exited externally so we can unset our
@ -969,7 +992,7 @@ const Subprocess = struct {
self.grid_size = grid_size;
self.screen_size = screen_size;
if (self.pty) |pty| {
if (self.pty) |*pty| {
try pty.setSize(.{
.ws_row = @intCast(grid_size.rows),
.ws_col = @intCast(grid_size.columns),
@ -983,6 +1006,11 @@ const Subprocess = struct {
/// process. This doesn't wait for the child process to be exited.
fn killCommand(command: *Command) !void {
if (command.pid) |pid| {
if (builtin.os.tag == .windows) {
if (windows.kernel32.TerminateProcess(pid, 0) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
} else {
const pgid_: ?c.pid_t = pgid: {
const pgid = c.getpgid(pid);
@ -1009,6 +1037,7 @@ const Subprocess = struct {
}
}
}
}
/// Kill the underlying process started via Flatpak host command.
/// This sends a signal via the Flatpak API.
@ -1034,8 +1063,7 @@ const Subprocess = struct {
/// fds and this is still much faster and lower overhead than any async
/// mechanism.
const ReadThread = struct {
/// The main entrypoint for the thread.
fn threadMain(fd: std.os.fd_t, ev: *EventData, quit: std.os.fd_t) void {
fn threadMainPosix(fd: std.os.fd_t, ev: *EventData, quit: std.os.fd_t) void {
// Always close our end of the pipe when we exit.
defer std.os.close(quit);
@ -1112,6 +1140,44 @@ const ReadThread = struct {
}
}
/// The main entrypoint for the thread.
fn threadMainWindows(fd: std.os.fd_t, ev: *EventData, quit: std.os.fd_t) void {
// Always close our end of the pipe when we exit.
defer std.os.close(quit);
var buf: [1024]u8 = undefined;
while (true) {
while (true) {
var n: windows.DWORD = 0;
if (windows.kernel32.ReadFile(fd, &buf, buf.len, &n, null) == 0) {
const err = windows.kernel32.GetLastError();
switch (err) {
// Check for a quit signal
.OPERATION_ABORTED => break,
else => {
log.err("io reader error err={}", .{err});
unreachable;
},
}
}
@call(.always_inline, process, .{ ev, buf[0..n] });
}
var quit_bytes: windows.DWORD = 0;
if (windows.exp.kernel32.PeekNamedPipe(quit, null, 0, null, &quit_bytes, null) == 0) {
const err = windows.kernel32.GetLastError();
log.err("quit pipe reader error err={}", .{err});
unreachable;
}
if (quit_bytes > 0) {
log.info("read thread got quit signal", .{});
return;
}
}
}
fn process(
ev: *EventData,
buf: []const u8,
@ -1913,6 +1979,11 @@ const StreamHandler = struct {
}
pub fn reportPwd(self: *StreamHandler, url: []const u8) !void {
if (builtin.os.tag == .windows) {
log.warn("reportPwd unimplemented on windows", .{});
return;
}
const uri = std.Uri.parse(url) catch |e| {
log.warn("invalid url in OSC 7: {}", .{e});
return;

100
src/windows.zig Normal file
View File

@ -0,0 +1,100 @@
const std = @import("std");
const windows = std.os.windows;
pub usingnamespace std.os.windows;
pub const exp = struct {
pub const HPCON = windows.LPVOID;
pub const CREATE_UNICODE_ENVIRONMENT = 0x00000400;
pub const EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
pub const LPPROC_THREAD_ATTRIBUTE_LIST = ?*anyopaque;
pub const FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000;
pub const STATUS_PENDING = 0x00000103;
pub const STILL_ACTIVE = STATUS_PENDING;
pub const STARTUPINFOEX = extern struct {
StartupInfo: windows.STARTUPINFOW,
lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST,
};
pub const kernel32 = struct {
pub extern "kernel32" fn CreatePipe(
hReadPipe: *windows.HANDLE,
hWritePipe: *windows.HANDLE,
lpPipeAttributes: ?*const windows.SECURITY_ATTRIBUTES,
nSize: windows.DWORD,
) callconv(windows.WINAPI) windows.BOOL;
pub extern "kernel32" fn CreatePseudoConsole(
size: windows.COORD,
hInput: windows.HANDLE,
hOutput: windows.HANDLE,
dwFlags: windows.DWORD,
phPC: *HPCON,
) callconv(windows.WINAPI) windows.HRESULT;
pub extern "kernel32" fn ResizePseudoConsole(hPC: HPCON, size: windows.COORD) callconv(windows.WINAPI) windows.HRESULT;
pub extern "kernel32" fn ClosePseudoConsole(hPC: HPCON) callconv(windows.WINAPI) void;
pub extern "kernel32" fn InitializeProcThreadAttributeList(
lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST,
dwAttributeCount: windows.DWORD,
dwFlags: windows.DWORD,
lpSize: *windows.SIZE_T,
) callconv(windows.WINAPI) windows.BOOL;
pub extern "kernel32" fn UpdateProcThreadAttribute(
lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST,
dwFlags: windows.DWORD,
Attribute: windows.DWORD_PTR,
lpValue: windows.PVOID,
cbSize: windows.SIZE_T,
lpPreviousValue: ?windows.PVOID,
lpReturnSize: ?*windows.SIZE_T,
) callconv(windows.WINAPI) windows.BOOL;
pub extern "kernel32" fn PeekNamedPipe(
hNamedPipe: windows.HANDLE,
lpBuffer: ?windows.LPVOID,
nBufferSize: windows.DWORD,
lpBytesRead: ?*windows.DWORD,
lpTotalBytesAvail: ?*windows.DWORD,
lpBytesLeftThisMessage: ?*windows.DWORD,
) callconv(windows.WINAPI) windows.BOOL;
// Duplicated here because lpCommandLine is not marked optional in zig std
pub extern "kernel32" fn CreateProcessW(
lpApplicationName: ?windows.LPWSTR,
lpCommandLine: ?windows.LPWSTR,
lpProcessAttributes: ?*windows.SECURITY_ATTRIBUTES,
lpThreadAttributes: ?*windows.SECURITY_ATTRIBUTES,
bInheritHandles: windows.BOOL,
dwCreationFlags: windows.DWORD,
lpEnvironment: ?*anyopaque,
lpCurrentDirectory: ?windows.LPWSTR,
lpStartupInfo: *windows.STARTUPINFOW,
lpProcessInformation: *windows.PROCESS_INFORMATION,
) callconv(windows.WINAPI) windows.BOOL;
};
pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF;
pub const PROC_THREAD_ATTRIBUTE_THREAD = 0x00010000;
pub const PROC_THREAD_ATTRIBUTE_INPUT = 0x00020000;
pub const PROC_THREAD_ATTRIBUTE_ADDITIVE = 0x00040000;
pub const ProcThreadAttributeNumber = enum(windows.DWORD) {
ProcThreadAttributePseudoConsole = 22,
_,
};
/// Corresponds to the ProcThreadAttributeValue define in WinBase.h
pub fn ProcThreadAttributeValue(
comptime attribute: ProcThreadAttributeNumber,
comptime thread: bool,
comptime input: bool,
comptime additive: bool,
) windows.DWORD {
return (@intFromEnum(attribute) & PROC_THREAD_ATTRIBUTE_NUMBER) |
(if (thread) PROC_THREAD_ATTRIBUTE_THREAD else 0) |
(if (input) PROC_THREAD_ATTRIBUTE_INPUT else 0) |
(if (additive) PROC_THREAD_ATTRIBUTE_ADDITIVE else 0);
}
pub const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = ProcThreadAttributeValue(.ProcThreadAttributePseudoConsole, false, true, false);
};