mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
196 lines
6.1 KiB
Zig
196 lines
6.1 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 assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
const Grid = @import("Grid.zig");
|
|
const glfw = @import("glfw");
|
|
const gl = @import("opengl.zig");
|
|
const Pty = @import("Pty.zig");
|
|
const Terminal = @import("terminal/Terminal.zig");
|
|
|
|
const log = std.log.scoped(.window);
|
|
|
|
/// 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 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,
|
|
|
|
/// 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) !*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,
|
|
});
|
|
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 terminal
|
|
var term = Terminal.init(grid.size.columns, grid.size.rows);
|
|
errdefer term.deinit(alloc);
|
|
try term.append(alloc, "> ");
|
|
|
|
self.* = .{
|
|
.alloc = alloc,
|
|
.window = window,
|
|
.grid = grid,
|
|
.pty = pty,
|
|
.terminal = term,
|
|
};
|
|
|
|
// Setup our callbacks and user data
|
|
window.setUserPointer(self);
|
|
window.setSizeCallback(sizeCallback);
|
|
window.setCharCallback(charCallback);
|
|
window.setKeyCallback(keyCallback);
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn destroy(self: *Window) void {
|
|
self.terminal.deinit(self.alloc);
|
|
self.pty.deinit();
|
|
self.grid.deinit();
|
|
self.window.destroy();
|
|
self.alloc.destroy(self);
|
|
}
|
|
|
|
pub fn run(self: Window) !void {
|
|
while (!self.window.shouldClose()) {
|
|
// Set our background
|
|
gl.clearColor(0.2, 0.3, 0.3, 1.0);
|
|
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
|
|
|
// Render the grid
|
|
try self.grid.render();
|
|
|
|
// Swap
|
|
try self.window.swapBuffers();
|
|
try glfw.waitEvents();
|
|
}
|
|
}
|
|
|
|
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
|
|
// 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);
|
|
|
|
// TODO: this is not the right place for this
|
|
win.grid.updateCells(win.terminal) catch unreachable;
|
|
|
|
// 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});
|
|
}
|
|
|
|
fn charCallback(window: glfw.Window, codepoint: u21) void {
|
|
const win = window.getUserPointer(Window) orelse return;
|
|
|
|
// Append this character to the terminal
|
|
win.terminal.appendChar(win.alloc, @intCast(u8, codepoint)) catch unreachable;
|
|
|
|
// Update the cells for drawing
|
|
win.grid.updateCells(win.terminal) catch unreachable;
|
|
}
|
|
|
|
fn keyCallback(
|
|
window: glfw.Window,
|
|
key: glfw.Key,
|
|
scancode: i32,
|
|
action: glfw.Action,
|
|
mods: glfw.Mods,
|
|
) void {
|
|
_ = scancode;
|
|
_ = mods;
|
|
|
|
//log.info("KEY {} {}", .{ key, action });
|
|
if (key == .enter and (action == .press or action == .repeat)) {
|
|
const win = window.getUserPointer(Window) orelse return;
|
|
win.terminal.append(win.alloc, "\r\n> ") catch unreachable;
|
|
win.grid.updateCells(win.terminal) catch unreachable;
|
|
}
|
|
}
|