ghostty/src/Window.zig
2022-05-01 16:02:33 -07:00

478 lines
16 KiB
Zig

//! Window represents a single OS window.
//!
//! NOTE(multi-window): This may be premature, but this abstraction is here
//! to pave the way One Day(tm) for multi-window support. At the time of
//! writing, we support exactly one window.
const Window = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Grid = @import("Grid.zig");
const glfw = @import("glfw");
const gl = @import("opengl.zig");
const libuv = @import("libuv/main.zig");
const Pty = @import("Pty.zig");
const Command = @import("Command.zig");
const Terminal = @import("terminal/Terminal.zig");
const SegmentedPool = @import("segmented_pool.zig").SegmentedPool;
const frame = @import("tracy/tracy.zig").frame;
const trace = @import("tracy/tracy.zig").trace;
const max_timer = @import("max_timer.zig");
const RenderTimer = max_timer.MaxTimer(renderTimerCallback);
const log = std.log.scoped(.window);
// The preallocation size for the write request pool. This should be big
// enough to satisfy most write requests. It must be a power of 2.
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
/// Allocator
alloc: Allocator,
/// The glfw window handle.
window: glfw.Window,
/// The terminal grid attached to this window.
grid: Grid,
/// The underlying pty for this window.
pty: Pty,
/// The command we're running for our tty.
command: Command,
/// The terminal emulator internal state. This is the abstract "terminal"
/// that manages input, grid updating, etc. and is renderer-agnostic. It
/// just stores internal state about a grid. This is connected back to
/// a renderer.
terminal: Terminal,
/// Timer that blinks the cursor.
cursor_timer: libuv.Timer,
/// Render at least 60fps.
render_timer: RenderTimer,
/// The reader/writer stream for the pty.
pty_stream: libuv.Tty,
/// This is the pool of available (unused) write requests. If you grab
/// one from the pool, you must put it back when you're done!
write_req_pool: SegmentedPool(libuv.WriteReq.T, WRITE_REQ_PREALLOC) = .{},
/// The pool of available buffers for writing to the pty.
/// TODO: [1]u8 is probably not right.
write_buf_pool: SegmentedPool([1]u8, WRITE_REQ_PREALLOC) = .{},
/// Set this to true whenver an event occurs that we may want to wake up
/// the event loop. Only set this from the main thread.
wakeup: bool = false,
/// Create a new window. This allocates and returns a pointer because we
/// need a stable pointer for user data callbacks. Therefore, a stack-only
/// initialization is not currently possible.
pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
var self = try alloc.create(Window);
errdefer alloc.destroy(self);
// Create our window
const window = try glfw.Window.create(640, 480, "ghostty", null, null, .{
.context_version_major = 3,
.context_version_minor = 3,
.opengl_profile = .opengl_core_profile,
.opengl_forward_compat = true,
.cocoa_graphics_switching = builtin.os.tag == .macos,
// We need to disable this for now since this causes all sorts
// of artifacts and issues to debug. This probably SHOULD be re-enable
// at some point but only when we're ready to debug.
.cocoa_retina_framebuffer = false,
});
errdefer window.destroy();
// NOTE(multi-window): We'll need to extract all the below into a
// dedicated renderer and consider the multi-threading (or at the very
// least: multi-OpenGL-context) implications. Since we don't support
// multiple windows right now, we just do it all here.
// Setup OpenGL
try glfw.makeContextCurrent(window);
try glfw.swapInterval(1);
// Load OpenGL bindings
const version = try gl.glad.load(glfw.getProcAddress);
log.info("loaded OpenGL {}.{}", .{
gl.glad.versionMajor(version),
gl.glad.versionMinor(version),
});
// Culling, probably not necessary. We have to change the winding
// order since our 0,0 is top-left.
gl.c.glEnable(gl.c.GL_CULL_FACE);
gl.c.glFrontFace(gl.c.GL_CW);
// Blending for text
gl.c.glEnable(gl.c.GL_BLEND);
gl.c.glBlendFunc(gl.c.GL_SRC_ALPHA, gl.c.GL_ONE_MINUS_SRC_ALPHA);
// Create our terminal grid with the initial window size
const window_size = try window.getSize();
var grid = try Grid.init(alloc);
try grid.setScreenSize(.{ .width = window_size.width, .height = window_size.height });
// Create our pty
var pty = try Pty.open(.{
.ws_row = @intCast(u16, grid.size.rows),
.ws_col = @intCast(u16, grid.size.columns),
.ws_xpixel = @intCast(u16, window_size.width),
.ws_ypixel = @intCast(u16, window_size.height),
});
errdefer pty.deinit();
// Create our child process
const path = (try Command.expandPath(alloc, "sh")) orelse
return error.CommandNotFound;
defer alloc.free(path);
var env = try std.process.getEnvMap(alloc);
defer env.deinit();
try env.put("TERM", "dumb");
var cmd: Command = .{
.path = path,
.args = &[_][]const u8{path},
.env = &env,
.pre_exec = (struct {
fn callback(c: *Command) void {
const p = c.getData(Pty) orelse unreachable;
p.childPreExec() catch |err|
log.err("error initializing child: {}", .{err});
}
}).callback,
.data = &pty,
};
// note: can't set these in the struct initializer because it
// sets the handle to "0". Probably a stage1 zig bug.
cmd.stdin = std.fs.File{ .handle = pty.slave };
cmd.stdout = cmd.stdin;
cmd.stderr = cmd.stdin;
try cmd.start(alloc);
log.debug("started subcommand path={s} pid={}", .{ path, cmd.pid });
// Read data
var stream = try libuv.Tty.init(alloc, loop, pty.master);
errdefer stream.deinit(alloc);
stream.setData(self);
try stream.readStart(ttyReadAlloc, ttyRead);
// Create our terminal
var term = try Terminal.init(alloc, grid.size.columns, grid.size.rows);
errdefer term.deinit(alloc);
// Setup a timer for blinking the cursor
var timer = try libuv.Timer.init(alloc, loop);
errdefer timer.deinit(alloc);
errdefer timer.close(null);
timer.setData(self);
try timer.start(cursorTimerCallback, 600, 600);
self.* = .{
.alloc = alloc,
.window = window,
.grid = grid,
.pty = pty,
.command = cmd,
.terminal = term,
.cursor_timer = timer,
.render_timer = try RenderTimer.init(loop, self, 16, 96),
.pty_stream = stream,
};
// Setup our callbacks and user data
window.setUserPointer(self);
window.setSizeCallback(sizeCallback);
window.setCharCallback(charCallback);
window.setKeyCallback(keyCallback);
window.setFocusCallback(focusCallback);
return self;
}
pub fn destroy(self: *Window) void {
// Deinitialize the pty. This closes the pty handles. This should
// cause a close in the our subprocess so just wait for that.
self.pty.deinit();
_ = self.command.wait() catch |err|
log.err("error waiting for command to exit: {}", .{err});
self.terminal.deinit(self.alloc);
self.grid.deinit();
self.window.destroy();
self.cursor_timer.close((struct {
fn callback(t: *libuv.Timer) void {
const alloc = t.loop().getData(Allocator).?.*;
t.deinit(alloc);
}
}).callback);
self.render_timer.deinit();
// We have to dealloc our window in the close callback because
// we can't free some of the memory associated with the window
// until the stream is closed.
self.pty_stream.readStop();
self.pty_stream.close((struct {
fn callback(t: *libuv.Tty) void {
const win = t.getData(Window).?;
const alloc = win.alloc;
t.deinit(alloc);
win.write_req_pool.deinit(alloc);
win.write_buf_pool.deinit(alloc);
win.alloc.destroy(win);
}
}).callback);
}
pub fn shouldClose(self: Window) bool {
return self.window.shouldClose();
}
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
const tracy = trace(@src());
defer tracy.end();
// glfw gives us signed integers, but negative width/height is n
// non-sensical so we use unsigned throughout, so assert.
assert(width >= 0);
assert(height >= 0);
// Update our grid so that the projections on render are correct.
const win = window.getUserPointer(Window) orelse return;
win.grid.setScreenSize(.{
.width = @intCast(u32, width),
.height = @intCast(u32, height),
}) catch |err| log.err("error updating grid screen size err={}", .{err});
// Update the size of our terminal state
win.terminal.resize(win.grid.size.columns, win.grid.size.rows);
// Update the size of our pty
win.pty.setSize(.{
.ws_row = @intCast(u16, win.grid.size.rows),
.ws_col = @intCast(u16, win.grid.size.columns),
.ws_xpixel = @intCast(u16, width),
.ws_ypixel = @intCast(u16, height),
}) catch |err| log.err("error updating pty screen size err={}", .{err});
// Update our viewport for this context to be the entire window
gl.viewport(0, 0, width, height) catch |err|
log.err("error updating OpenGL viewport err={}", .{err});
// Draw
win.render_timer.schedule() catch unreachable;
}
fn charCallback(window: glfw.Window, codepoint: u21) void {
const tracy = trace(@src());
defer tracy.end();
const win = window.getUserPointer(Window) orelse return;
// Write the character to the pty
const req = win.write_req_pool.get() catch unreachable;
const buf = win.write_buf_pool.get() catch unreachable;
buf[0] = @intCast(u8, codepoint);
win.pty_stream.write(
.{ .req = req },
&[1][]u8{buf[0..1]},
ttyWrite,
) catch unreachable;
}
fn keyCallback(
window: glfw.Window,
key: glfw.Key,
scancode: i32,
action: glfw.Action,
mods: glfw.Mods,
) void {
const tracy = trace(@src());
defer tracy.end();
_ = scancode;
_ = mods;
//log.info("KEY {} {} {} {}", .{ key, scancode, mods, action });
if (action == .press or action == .repeat) {
const c: u8 = switch (key) {
// Lots more of these:
// https://www.physics.udel.edu/~watson/scen103/ascii.html
.a => if (mods.control and !mods.shift) 0x01 else return,
.b => if (mods.control and !mods.shift) 0x02 else return,
.c => if (mods.control and !mods.shift) 0x03 else return,
.d => if (mods.control and !mods.shift) 0x04 else return,
.e => if (mods.control and !mods.shift) 0x05 else return,
.f => if (mods.control and !mods.shift) 0x06 else return,
.g => if (mods.control and !mods.shift) 0x07 else return,
.h => if (mods.control and !mods.shift) 0x08 else return,
.i => if (mods.control and !mods.shift) 0x09 else return,
.j => if (mods.control and !mods.shift) 0x0A else return,
.k => if (mods.control and !mods.shift) 0x0B else return,
.l => if (mods.control and !mods.shift) 0x0C else return,
.m => if (mods.control and !mods.shift) 0x0D else return,
.n => if (mods.control and !mods.shift) 0x0E else return,
.o => if (mods.control and !mods.shift) 0x0F else return,
.p => if (mods.control and !mods.shift) 0x10 else return,
.q => if (mods.control and !mods.shift) 0x11 else return,
.r => if (mods.control and !mods.shift) 0x12 else return,
.s => if (mods.control and !mods.shift) 0x13 else return,
.t => if (mods.control and !mods.shift) 0x14 else return,
.u => if (mods.control and !mods.shift) 0x15 else return,
.v => if (mods.control and !mods.shift) 0x16 else return,
.x => if (mods.control and !mods.shift) 0x17 else return,
.y => if (mods.control and !mods.shift) 0x18 else return,
.z => if (mods.control and !mods.shift) 0x19 else return,
.backspace => 0x08,
.enter => '\n',
.tab => '\t',
else => return,
};
const win = window.getUserPointer(Window) orelse return;
const req = win.write_req_pool.get() catch unreachable;
const buf = win.write_buf_pool.get() catch unreachable;
buf[0] = c;
win.pty_stream.write(
.{ .req = req },
&[1][]u8{buf[0..1]},
ttyWrite,
) catch unreachable;
}
}
fn focusCallback(window: glfw.Window, focused: bool) void {
const tracy = trace(@src());
defer tracy.end();
const win = window.getUserPointer(Window) orelse return;
// We have to schedule a render because no matter what we're changing
// the cursor.
win.render_timer.schedule() catch unreachable;
if (focused) {
win.wakeup = true;
win.cursor_timer.start(cursorTimerCallback, 0, win.cursor_timer.getRepeat()) catch unreachable;
win.grid.cursor_style = .box;
win.grid.cursor_visible = false;
} else {
win.grid.cursor_visible = true;
win.grid.cursor_style = .box_hollow;
win.cursor_timer.stop() catch unreachable;
}
}
fn cursorTimerCallback(t: *libuv.Timer) void {
const tracy = trace(@src());
defer tracy.end();
const win = t.getData(Window) orelse return;
win.grid.cursor_visible = !win.grid.cursor_visible;
win.render_timer.schedule() catch unreachable;
}
fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 {
const tracy = trace(@src());
defer tracy.end();
const alloc = t.loop().getData(Allocator).?.*;
return alloc.alloc(u8, size) catch null;
}
fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
const tracy = trace(@src());
tracy.color(0xEAEA7F); // yellow-ish
defer tracy.end();
const win = t.getData(Window).?;
defer win.alloc.free(buf);
// log.info("DATA: {d}", .{n});
// log.info("DATA: {any}", .{buf[0..@intCast(usize, n)]});
// First check for errors in the case n is less than 0.
libuv.convertError(@intCast(i32, n)) catch |err| {
switch (err) {
// ignore EOF because it should end the process.
libuv.Error.EOF => {},
else => log.err("read error: {}", .{err}),
}
return;
};
// Add this character to the terminal buffer.
win.terminal.append(win.alloc, buf[0..@intCast(usize, n)]) catch |err|
log.err("error writing terminal data: {}", .{err});
// Whenever a character is typed, we ensure the cursor is visible
// and we restart the cursor timer.
win.grid.cursor_visible = true;
if (win.cursor_timer.isActive() catch false) {
_ = win.cursor_timer.again() catch null;
}
// Schedule a render
win.render_timer.schedule() catch unreachable;
}
fn ttyWrite(req: *libuv.WriteReq, status: i32) void {
const tracy = trace(@src());
defer tracy.end();
const tty = req.handle(libuv.Tty).?;
const win = tty.getData(Window).?;
win.write_req_pool.put();
win.write_buf_pool.put();
libuv.convertError(status) catch |err|
log.err("write error: {}", .{err});
//log.info("WROTE: {d}", .{status});
}
fn renderTimerCallback(t: *libuv.Timer) void {
const tracy = trace(@src());
tracy.color(0x006E7F); // blue-ish
defer tracy.end();
const win = t.getData(Window).?;
// Update the cells for drawing
win.grid.updateCells(win.terminal) catch unreachable;
// Set our background
gl.clearColor(0.2, 0.3, 0.3, 1.0);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
// Render the grid
win.grid.render() catch |err| {
log.err("error rendering grid: {}", .{err});
return;
};
// Swap
win.window.swapBuffers() catch |err| {
log.err("error swapping buffers: {}", .{err});
return;
};
// Record our run
win.render_timer.tick();
}