Merge pull request #773 from kcbanner/windows_prototype

windows: add support for the glfw backend
This commit is contained in:
Mitchell Hashimoto
2023-11-05 18:30:20 -08:00
committed by GitHub
20 changed files with 949 additions and 274 deletions

View File

@ -10,10 +10,8 @@ jobs:
target: [
aarch64-linux,
x86_64-linux,
# No windows support currently.
# i386-windows,
# x86_64-windows-gnu,
x86-windows-gnu,
x86_64-windows-gnu,
# We don't support cross-compiling to macOS because the macOS build
# requires xcode due to the swift harness.
#aarch64-macos,

View File

@ -48,7 +48,7 @@ things, but I've been using it full time since April 2022.
| ------------------ | ------------------------------------------------------------------------ | -------------------------- |
| macOS | [Tip ("Nightly")](https://github.com/mitchellh/ghostty/releases/tag/tip) | MacOS 12+ Universal Binary |
| Linux | [Build from Source](#developing-ghostty) | |
| Windows | n/a | Not supported yet |
| Windows | [Build from Source](#developing-ghostty) | [Notes](#windows-notes) |
### Configuration
@ -439,13 +439,30 @@ $ sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
...
```
### Windows Notes
Windows support is still a [work-in-progress](https://github.com/mitchellh/ghostty/issues/437).
The current status is that a bare bones glfw-based build _works_! The experience
with this build is super minimal: there are no native experiences, only a
single window is supported, no tabs, etc. Therefore, the current status is
simply that the core terminal experience works.
If you want to help with Windows development, please see the
[tracking issue](https://github.com/mitchellh/ghostty/issues/437). We plan
on vastly improving this experience over time.
### Linting
Ghostty's docs and resources (not including Zig code) are linted using [Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI check will fail builds with improper formatting. Therefore, if you are modifying anything Prettier will lint, you may want to install it locally and run this from the repo root before you commit:
Ghostty's docs and resources (not including Zig code) are linted using
[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI
check will fail builds with improper formatting. Therefore, if you are
modifying anything Prettier will lint, you may want to install it locally and
run this from the repo root before you commit:
```
npm install -g prettier
prettier --write .
```
Or simply install one of the many Prettier extensions out there for your editor of choice.
Or simply install one of the many Prettier extensions out there for your
editor of choice.

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/mitchellh/libxev/archive/1b46c2d6f32754a3029d1863275dd0f877163831.tar.gz",
.hash = "12208dc0796bffa6ea9edb60193108e34f835f35fc152e853717aca2b13ba17f3be2",
},
.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

@ -14,16 +14,12 @@
//! * posix_spawn is used for Mac, but doesn't support the necessary
//! features for tty setup.
//!
//! TODO:
//!
//! * Windows
//! * Mac
//!
const Command = @This();
const std = @import("std");
const builtin = @import("builtin");
const internal_os = @import("os/main.zig");
const windows = internal_os.windows;
const TempDir = internal_os.TempDir;
const mem = std.mem;
const os = std.os;
@ -64,15 +60,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 +112,13 @@ pub fn start(self: *Command, alloc: Allocator) !void {
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
switch (builtin.os.tag) {
.windows => try self.startWindows(arena),
else => try self.startPosix(arena),
}
}
fn startPosix(self: *Command, arena: Allocator) !void {
// 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 +132,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) {
@ -150,6 +157,110 @@ pub fn start(self: *Command, alloc: Allocator) !void {
_ = std.os.execveZ(pathZ, argsZ, envp) catch null;
}
fn startWindows(self: *Command, arena: Allocator) !void {
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;
}
fn setupFd(src: File.Handle, target: i32) !void {
switch (builtin.os.tag) {
.linux => {
@ -190,8 +301,22 @@ 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 (comptime 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 +450,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.
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 +576,30 @@ 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 +609,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 +619,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 +650,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 +686,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,149 +0,0 @@
//! 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 fd_t = std.os.fd_t;
const c = switch (builtin.os.tag) {
.macos => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
.windows => {},
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
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 {
ws_row: u16 = 100,
ws_col: u16 = 80,
ws_xpixel: u16 = 800,
ws_ypixel: u16 = 600,
};
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,
/// Open a new PTY with the given initial size.
pub fn open(size: winsize) !Pty {
if (builtin.os.tag == .windows) return error.NotImplementedOnWindows;
// Need to copy so that it becomes non-const.
var sizeCopy = size;
var master_fd: fd_t = undefined;
var slave_fd: fd_t = undefined;
if (c.openpty(
&master_fd,
&slave_fd,
null,
null,
@ptrCast(&sizeCopy),
) < 0)
return error.OpenptyFailed;
errdefer {
_ = std.os.system.close(master_fd);
_ = std.os.system.close(slave_fd);
}
// Enable UTF-8 mode. I think this is on by default on Linux but it
// is NOT on by default on macOS so we ensure that it is always set.
var attrs: c.termios = undefined;
if (c.tcgetattr(master_fd, &attrs) != 0)
return error.OpenptyFailed;
attrs.c_iflag |= c.IUTF8;
if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
return Pty{
.master = master_fd,
.slave = slave_fd,
};
}
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;
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;
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 {
// Create a new process group
if (setsid() < 0) return error.ProcessGroupFailed;
// Set controlling terminal
switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) {
.SUCCESS => {},
else => |err| {
log.err("error setting controlling terminal errno={}", .{err});
return error.SetControllingTerminalFailed;
},
}
// Can close master/slave pair now
std.os.close(self.slave);
std.os.close(self.master);
// TODO: reset signals
}
test {
if (builtin.os.tag == .windows) return error.SkipZigTest;
var ws: winsize = .{
.ws_row = 50,
.ws_col = 80,
.ws_xpixel = 1,
.ws_ypixel = 1,
};
var pty = try open(ws);
defer pty.deinit();
// Initialize size should match what we gave it
try testing.expectEqual(ws, try pty.getSize());
// Can set and read new sizes
ws.ws_row *= 2;
try pty.setSize(ws);
try testing.expectEqual(ws, try pty.getSize());
}

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

@ -57,6 +57,9 @@ pub const Runtime = enum {
// The Linux default is GTK because it is full featured.
if (target.isLinux()) return .gtk;
// Windows we currently only support glfw
if (target.isWindows()) return .glfw;
// Otherwise, we do NONE so we don't create an exe. The GLFW
// build is opt-in because it is missing so many features compared
// to the other builds that are impossible due to the GLFW interface.

View File

@ -1134,6 +1134,22 @@ pub fn finalize(self: *Config) !void {
}
}
switch (builtin.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) {
@ -1153,6 +1169,8 @@ pub fn finalize(self: *Config) !void {
if (self.command == null) {
log.warn("no default shell found, will default to using sh", .{});
}
},
}
}
}

View File

@ -243,8 +243,9 @@ 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 (v.len > 0) {
if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| {
defer v.deinit(self.alloc);
if (v.value.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
@ -284,7 +285,7 @@ pub const GlobalState = struct {
};
test {
_ = @import("circ_buf.zig");
_ = @import("Pty.zig");
_ = @import("pty.zig");
_ = @import("Command.zig");
_ = @import("font/main.zig");
_ = @import("apprt.zig");

View File

@ -25,6 +25,61 @@ pub fn appendEnv(
});
}
/// The result of getenv, with a shared deinit to properly handle allocation
/// on Windows.
pub const GetEnvResult = struct {
value: []const u8,
pub fn deinit(self: GetEnvResult, alloc: Allocator) void {
switch (builtin.os.tag) {
.windows => alloc.free(self.value),
else => {},
}
}
};
/// Gets the value of an environment variable, or null if not found.
/// This will allocate on Windows but not on other platforms. The returned
/// value should have deinit called to do the proper cleanup no matter what
/// platform you are on.
pub fn getenv(alloc: Allocator, key: []const u8) !?GetEnvResult {
return switch (builtin.os.tag) {
// Non-Windows doesn't need to allocate
else => if (std.os.getenv(key)) |v| .{ .value = v } else null,
// Windows needs to allocate
.windows => if (std.process.getEnvVarOwned(alloc, key)) |v| .{
.value = v,
} else |err| switch (err) {
error.EnvironmentVariableNotFound => null,
else => err,
},
};
}
pub fn setenv(key: [:0]const u8, value: [:0]const u8) c_int {
return switch (builtin.os.tag) {
.windows => c._putenv_s(key.ptr, value.ptr),
else => c.setenv(key.ptr, value.ptr, 1),
};
}
pub fn unsetenv(key: [:0]const u8) c_int {
return switch (builtin.os.tag) {
.windows => c._putenv_s(key.ptr, ""),
else => c.unsetenv(key.ptr),
};
}
const c = struct {
// POSIX
extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int;
extern "c" fn unsetenv(name: ?[*]const u8) c_int;
// Windows
extern "c" fn _putenv_s(varname: ?[*]const u8, value_string: ?[*]const u8) c_int;
};
test "appendEnv empty" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -103,6 +103,7 @@ fn homeWindows(buf: []u8) !?[]u8 {
if (homepath.ptr != path_buf.ptr) @panic("codebug");
break :blk homepath.len;
};
return buf[0 .. drive_len + path_len];
}

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.getenv(alloc, "LANG");
defer if (lang) |v| v.deinit(alloc);
// 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
@ -19,7 +21,7 @@ pub fn ensureLocale() void {
// process.
if (comptime builtin.target.isDarwin()) {
// Set the lang if it is not set or if its empty.
if (lang.len == 0) {
if (lang == null or lang.?.value.len == 0) {
setLangFromCocoa();
}
}
@ -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 (old_lang.len > 0) {
if ((try internal_os.getenv(alloc, "LANG"))) |old_lang| {
defer old_lang.deinit(alloc);
if (old_lang.value.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) < 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

@ -9,7 +9,9 @@ pub usingnamespace @import("homedir.zig");
pub usingnamespace @import("locale.zig");
pub usingnamespace @import("macos_version.zig");
pub usingnamespace @import("mouse.zig");
pub usingnamespace @import("pipe.zig");
pub usingnamespace @import("resourcesdir.zig");
pub const TempDir = @import("TempDir.zig");
pub const passwd = @import("passwd.zig");
pub const xdg = @import("xdg.zig");
pub const windows = @import("windows.zig");

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 = .{

19
src/os/pipe.zig Normal file
View File

@ -0,0 +1,19 @@
const std = @import("std");
const builtin = @import("builtin");
const windows = @import("windows.zig");
/// pipe() that works on Windows and POSIX.
pub fn pipe() ![2]std.os.fd_t {
switch (builtin.os.tag) {
else => return try std.os.pipe(),
.windows => {
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());
}
return .{ read, write };
},
}
}

View File

@ -4,20 +4,19 @@ 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;
}
// Note: we ALWAYS want to allocate here because the result is always
// freed, do not try to use internal_os.getenv or posix getenv.
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 +29,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);
}
}

100
src/os/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);
};

290
src/pty.zig Normal file
View File

@ -0,0 +1,290 @@
const std = @import("std");
const builtin = @import("builtin");
const windows = @import("os/main.zig").windows;
const log = std.log.scoped(.pty);
/// 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.
pub const winsize = extern struct {
ws_row: u16 = 100,
ws_col: u16 = 80,
ws_xpixel: u16 = 800,
ws_ypixel: u16 = 600,
};
pub const Pty = if (builtin.os.tag == .windows)
WindowsPty
else
PosixPty;
/// 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 PosixPty = struct {
pub const Fd = std.os.fd_t;
// 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;
extern "c" fn setsid() std.c.pid_t;
const c = struct {
usingnamespace switch (builtin.os.tag) {
.macos => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
};
/// The file descriptors for the master and slave side of the pty.
master: Fd,
slave: Fd,
/// 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;
var master_fd: Fd = undefined;
var slave_fd: Fd = undefined;
if (c.openpty(
&master_fd,
&slave_fd,
null,
null,
@ptrCast(&sizeCopy),
) < 0)
return error.OpenptyFailed;
errdefer {
_ = std.os.system.close(master_fd);
_ = std.os.system.close(slave_fd);
}
// Enable UTF-8 mode. I think this is on by default on Linux but it
// is NOT on by default on macOS so we ensure that it is always set.
var attrs: c.termios = undefined;
if (c.tcgetattr(master_fd, &attrs) != 0)
return error.OpenptyFailed;
attrs.c_iflag |= c.IUTF8;
if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
return Pty{
.master = master_fd,
.slave = slave_fd,
};
}
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 {
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 (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 {
// Create a new process group
if (setsid() < 0) return error.ProcessGroupFailed;
// Set controlling terminal
switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) {
.SUCCESS => {},
else => |err| {
log.err("error setting controlling terminal errno={}", .{err});
return error.SetControllingTerminalFailed;
},
}
// Can close master/slave pair now
std.os.close(self.slave);
std.os.close(self.master);
// TODO: reset signals
}
};
/// Windows PTY creation and management.
const WindowsPty = struct {
pub const Fd = windows.HANDLE;
// 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;
}
};
test {
const testing = std.testing;
var ws: winsize = .{
.ws_row = 50,
.ws_col = 80,
.ws_xpixel = 1,
.ws_ypixel = 1,
};
var pty = try Pty.open(ws);
defer pty.deinit();
// Initialize size should match what we gave it
try testing.expectEqual(ws, try pty.getSize());
// Can set and read new sizes
ws.ws_row *= 2;
try pty.setSize(ws);
try testing.expectEqual(ws, try pty.getSize());
}

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");
@ -21,6 +21,7 @@ const trace = tracy.trace;
const apprt = @import("../apprt.zig");
const fastmem = @import("../fastmem.zig");
const internal_os = @import("../os/main.zig");
const windows = internal_os.windows;
const configpkg = @import("../config.zig");
const shell_integration = @import("shell_integration.zig");
@ -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);
const pty_fds = try self.subprocess.start(alloc);
errdefer self.subprocess.stop();
const pid = pid: {
const command = self.subprocess.command orelse return error.ProcessNotStarted;
@ -193,7 +194,7 @@ 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 = try internal_os.pipe();
errdefer std.os.close(pipe[0]);
errdefer std.os.close(pipe[1]);
@ -202,7 +203,7 @@ 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);
var stream = xev.Stream.initFd(pty_fds.write);
errdefer stream.deinit();
// Wakeup watcher for the writer thread.
@ -264,8 +265,8 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
// Start our reader thread
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,
.{ pty_fds.read, ev_data_ptr, pipe[0] },
);
read_thread.setName("io-reader") catch {};
@ -275,6 +276,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) pty_fds.read else {},
};
}
@ -291,6 +293,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 (comptime 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 +489,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 +513,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,8 +676,13 @@ 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
return error.CommandNotFound;
const path = try Command.expandPath(
alloc,
opts.full_config.command orelse switch (builtin.os.tag) {
.windows => "cmd.exe",
else => "sh",
},
) orelse return error.CommandNotFound;
// On macOS, we launch the program as a login shell. This is a Mac-specific
// behavior (see other terminals). Terminals in general should NOT be
@ -833,7 +852,10 @@ 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) !struct {
read: Pty.Fd,
write: Pty.Fd,
} {
assert(self.pty == null and self.command == null);
// Create our pty
@ -887,7 +909,10 @@ const Subprocess = struct {
// parent and detect EOF.
_ = std.os.close(pty.slave);
return pty.master;
return .{
.read = pty.master,
.write = pty.master,
};
}
// If we can't access the cwd, then don't set any cwd and inherit.
@ -908,10 +933,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|
@ -921,11 +947,23 @@ const Subprocess = struct {
.data = &self.pty.?,
};
try cmd.start(alloc);
errdefer killCommand(cmd);
errdefer killCommand(&cmd) catch |err| {
log.warn("error killing command during cleanup err={}", .{err});
};
log.info("started subcommand path={s} pid={?}", .{ self.path, cmd.pid });
self.command = cmd;
return pty.master;
return switch (builtin.os.tag) {
.windows => .{
.read = pty.out_pipe,
.write = pty.in_pipe,
},
else => .{
.read = pty.master,
.write = pty.master,
},
};
}
/// Called to notify that we exited externally so we can unset our
@ -969,7 +1007,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 +1021,14 @@ const Subprocess = struct {
/// process. This doesn't wait for the child process to be exited.
fn killCommand(command: *Command) !void {
if (command.pid) |pid| {
switch (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);
@ -1007,6 +1053,8 @@ const Subprocess = struct {
return error.KillFailed;
}
}
},
}
}
}
@ -1034,8 +1082,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 +1159,44 @@ const ReadThread = struct {
}
}
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 +1998,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;