mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
CLI parsing, can set default foreground/background color
This commit is contained in:
@ -9,6 +9,7 @@ const glfw = @import("glfw");
|
|||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
const libuv = @import("libuv/main.zig");
|
const libuv = @import("libuv/main.zig");
|
||||||
const tracy = @import("tracy/tracy.zig");
|
const tracy = @import("tracy/tracy.zig");
|
||||||
|
const Config = @import("config.zig").Config;
|
||||||
|
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
|
||||||
@ -24,10 +25,13 @@ window: *Window,
|
|||||||
// so that users of the loop always have an allocator.
|
// so that users of the loop always have an allocator.
|
||||||
loop: libuv.Loop,
|
loop: libuv.Loop,
|
||||||
|
|
||||||
|
// The configuration for the app.
|
||||||
|
config: *const Config,
|
||||||
|
|
||||||
/// Initialize the main app instance. This creates the main window, sets
|
/// Initialize the main app instance. This creates the main window, sets
|
||||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||||
/// "startup" logic.
|
/// "startup" logic.
|
||||||
pub fn init(alloc: Allocator) !App {
|
pub fn init(alloc: Allocator, config: *const Config) !App {
|
||||||
// Create the event loop
|
// Create the event loop
|
||||||
var loop = try libuv.Loop.init(alloc);
|
var loop = try libuv.Loop.init(alloc);
|
||||||
errdefer loop.deinit(alloc);
|
errdefer loop.deinit(alloc);
|
||||||
@ -40,13 +44,14 @@ pub fn init(alloc: Allocator) !App {
|
|||||||
loop.setData(allocPtr);
|
loop.setData(allocPtr);
|
||||||
|
|
||||||
// Create the window
|
// Create the window
|
||||||
var window = try Window.create(alloc, loop);
|
var window = try Window.create(alloc, loop, config);
|
||||||
errdefer window.destroy();
|
errdefer window.destroy();
|
||||||
|
|
||||||
return App{
|
return App{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.window = window,
|
.window = window,
|
||||||
.loop = loop,
|
.loop = loop,
|
||||||
|
.config = config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
src/Grid.zig
11
src/Grid.zig
@ -40,6 +40,9 @@ font_atlas: FontAtlas,
|
|||||||
cursor_visible: bool,
|
cursor_visible: bool,
|
||||||
cursor_style: CursorStyle,
|
cursor_style: CursorStyle,
|
||||||
|
|
||||||
|
/// Default foreground color
|
||||||
|
foreground: Terminal.RGB,
|
||||||
|
|
||||||
const CursorStyle = enum(u8) {
|
const CursorStyle = enum(u8) {
|
||||||
box = 3,
|
box = 3,
|
||||||
box_hollow = 4,
|
box_hollow = 4,
|
||||||
@ -219,6 +222,7 @@ pub fn init(alloc: Allocator) !Grid {
|
|||||||
.font_atlas = font,
|
.font_atlas = font,
|
||||||
.cursor_visible = true,
|
.cursor_visible = true,
|
||||||
.cursor_style = .box,
|
.cursor_style = .box,
|
||||||
|
.foreground = .{ .r = 255, .g = 255, .b = 255 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,12 +309,7 @@ pub fn updateCells(self: *Grid, term: Terminal) !void {
|
|||||||
// TODO: if we add a glyph, I think we need to rerender the texture.
|
// TODO: if we add a glyph, I think we need to rerender the texture.
|
||||||
const glyph = try self.font_atlas.addGlyph(self.alloc, cell.char);
|
const glyph = try self.font_atlas.addGlyph(self.alloc, cell.char);
|
||||||
|
|
||||||
const fg = cell.fg orelse Terminal.RGB{
|
const fg = cell.fg orelse self.foreground;
|
||||||
.r = 0xFF,
|
|
||||||
.g = 0xA5,
|
|
||||||
.b = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = 2,
|
.mode = 2,
|
||||||
.grid_col = @intCast(u16, x),
|
.grid_col = @intCast(u16, x),
|
||||||
|
@ -20,6 +20,7 @@ const frame = @import("tracy/tracy.zig").frame;
|
|||||||
const trace = @import("tracy/tracy.zig").trace;
|
const trace = @import("tracy/tracy.zig").trace;
|
||||||
const max_timer = @import("max_timer.zig");
|
const max_timer = @import("max_timer.zig");
|
||||||
const terminal = @import("terminal/main.zig");
|
const terminal = @import("terminal/main.zig");
|
||||||
|
const Config = @import("config.zig").Config;
|
||||||
|
|
||||||
const RenderTimer = max_timer.MaxTimer(renderTimerCallback);
|
const RenderTimer = max_timer.MaxTimer(renderTimerCallback);
|
||||||
|
|
||||||
@ -73,10 +74,19 @@ write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{},
|
|||||||
/// the event loop. Only set this from the main thread.
|
/// the event loop. Only set this from the main thread.
|
||||||
wakeup: bool = false,
|
wakeup: bool = false,
|
||||||
|
|
||||||
|
/// The app configuration
|
||||||
|
config: *const Config,
|
||||||
|
|
||||||
|
/// Window background color
|
||||||
|
bg_r: f32,
|
||||||
|
bg_g: f32,
|
||||||
|
bg_b: f32,
|
||||||
|
bg_a: f32,
|
||||||
|
|
||||||
/// Create a new window. This allocates and returns a pointer because we
|
/// Create a new window. This allocates and returns a pointer because we
|
||||||
/// need a stable pointer for user data callbacks. Therefore, a stack-only
|
/// need a stable pointer for user data callbacks. Therefore, a stack-only
|
||||||
/// initialization is not currently possible.
|
/// initialization is not currently possible.
|
||||||
pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
|
pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Window {
|
||||||
var self = try alloc.create(Window);
|
var self = try alloc.create(Window);
|
||||||
errdefer alloc.destroy(self);
|
errdefer alloc.destroy(self);
|
||||||
|
|
||||||
@ -124,6 +134,11 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
|
|||||||
const window_size = try window.getSize();
|
const window_size = try window.getSize();
|
||||||
var grid = try Grid.init(alloc);
|
var grid = try Grid.init(alloc);
|
||||||
try grid.setScreenSize(.{ .width = window_size.width, .height = window_size.height });
|
try grid.setScreenSize(.{ .width = window_size.width, .height = window_size.height });
|
||||||
|
grid.foreground = .{
|
||||||
|
.r = config.foreground.r,
|
||||||
|
.g = config.foreground.g,
|
||||||
|
.b = config.foreground.b,
|
||||||
|
};
|
||||||
|
|
||||||
// Create our pty
|
// Create our pty
|
||||||
var pty = try Pty.open(.{
|
var pty = try Pty.open(.{
|
||||||
@ -192,6 +207,11 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
|
|||||||
.cursor_timer = timer,
|
.cursor_timer = timer,
|
||||||
.render_timer = try RenderTimer.init(loop, self, 16, 96),
|
.render_timer = try RenderTimer.init(loop, self, 16, 96),
|
||||||
.pty_stream = stream,
|
.pty_stream = stream,
|
||||||
|
.config = config,
|
||||||
|
.bg_r = @intToFloat(f32, config.background.r) / 255.0,
|
||||||
|
.bg_g = @intToFloat(f32, config.background.g) / 255.0,
|
||||||
|
.bg_b = @intToFloat(f32, config.background.b) / 255.0,
|
||||||
|
.bg_a = 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup our callbacks and user data
|
// Setup our callbacks and user data
|
||||||
@ -480,7 +500,7 @@ fn renderTimerCallback(t: *libuv.Timer) void {
|
|||||||
win.grid.updateCells(win.terminal) catch unreachable;
|
win.grid.updateCells(win.terminal) catch unreachable;
|
||||||
|
|
||||||
// Set our background
|
// Set our background
|
||||||
gl.clearColor(0.2, 0.3, 0.3, 1.0);
|
gl.clearColor(win.bg_r, win.bg_g, win.bg_b, win.bg_a);
|
||||||
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
// Render the grid
|
// Render the grid
|
||||||
|
158
src/cli_args.zig
Normal file
158
src/cli_args.zig
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Only `--long=value` format is accepted. Do we want to allow
|
||||||
|
// `--long value`? Not currently allowed.
|
||||||
|
|
||||||
|
/// Parse the command line arguments from iter into dst.
|
||||||
|
///
|
||||||
|
/// dst must be a struct. The fields and their types will be used to determine
|
||||||
|
/// the valid CLI flags. See the tests in this file as an example. For field
|
||||||
|
/// types that are structs, the struct can implement the `parseCLI` function
|
||||||
|
/// to do custom parsing.
|
||||||
|
pub fn parse(comptime T: type, dst: *T, iter: anytype) !void {
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
assert(info == .Struct);
|
||||||
|
|
||||||
|
while (iter.next()) |arg| {
|
||||||
|
if (mem.startsWith(u8, arg, "--")) {
|
||||||
|
var key: []const u8 = arg[2..];
|
||||||
|
const value: ?[]const u8 = value: {
|
||||||
|
// If the arg has "=" then the value is after the "=".
|
||||||
|
if (mem.indexOf(u8, key, "=")) |idx| {
|
||||||
|
defer key = key[0..idx];
|
||||||
|
break :value key[idx + 1 ..];
|
||||||
|
}
|
||||||
|
|
||||||
|
break :value null;
|
||||||
|
};
|
||||||
|
|
||||||
|
try parseIntoField(T, dst, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a single key/value pair into the destination type T.
|
||||||
|
fn parseIntoField(comptime T: type, dst: *T, key: []const u8, value: ?[]const u8) !void {
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
assert(info == .Struct);
|
||||||
|
|
||||||
|
inline for (info.Struct.fields) |field| {
|
||||||
|
if (mem.eql(u8, field.name, key)) {
|
||||||
|
@field(dst, field.name) = field: {
|
||||||
|
const Field = field.field_type;
|
||||||
|
const fieldInfo = @typeInfo(Field);
|
||||||
|
|
||||||
|
// If the type implements a parse function, call that.
|
||||||
|
if (fieldInfo == .Struct and @hasDecl(Field, "parseCLI"))
|
||||||
|
break :field try Field.parseCLI(value);
|
||||||
|
|
||||||
|
// Otherwise infer based on type
|
||||||
|
break :field switch (Field) {
|
||||||
|
[]const u8 => value orelse return error.ValueRequired,
|
||||||
|
bool => try parseBool(value orelse "t"),
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseBool(v: []const u8) !bool {
|
||||||
|
const t = &[_][]const u8{ "1", "t", "T", "true" };
|
||||||
|
const f = &[_][]const u8{ "0", "f", "F", "false" };
|
||||||
|
|
||||||
|
inline for (t) |str| {
|
||||||
|
if (mem.eql(u8, v, str)) return true;
|
||||||
|
}
|
||||||
|
inline for (f) |str| {
|
||||||
|
if (mem.eql(u8, v, str)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidBooleanValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse: simple" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var data: struct {
|
||||||
|
a: []const u8,
|
||||||
|
b: bool,
|
||||||
|
@"b-f": bool,
|
||||||
|
} = undefined;
|
||||||
|
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
testing.allocator,
|
||||||
|
"--a=42 --b --b-f=false",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
try parse(@TypeOf(data), &data, &iter);
|
||||||
|
try testing.expectEqualStrings("42", data.a);
|
||||||
|
try testing.expect(data.b);
|
||||||
|
try testing.expect(!data.@"b-f");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseIntoField: string" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var data: struct {
|
||||||
|
a: []const u8,
|
||||||
|
} = undefined;
|
||||||
|
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "42");
|
||||||
|
try testing.expectEqual(@as([]const u8, "42"), data.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseIntoField: bool" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var data: struct {
|
||||||
|
a: bool,
|
||||||
|
} = undefined;
|
||||||
|
|
||||||
|
// True
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "1");
|
||||||
|
try testing.expectEqual(true, data.a);
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "t");
|
||||||
|
try testing.expectEqual(true, data.a);
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "T");
|
||||||
|
try testing.expectEqual(true, data.a);
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "true");
|
||||||
|
try testing.expectEqual(true, data.a);
|
||||||
|
|
||||||
|
// False
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "0");
|
||||||
|
try testing.expectEqual(false, data.a);
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "f");
|
||||||
|
try testing.expectEqual(false, data.a);
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "F");
|
||||||
|
try testing.expectEqual(false, data.a);
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "false");
|
||||||
|
try testing.expectEqual(false, data.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseIntoField: struct with parse func" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var data: struct {
|
||||||
|
a: struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
v: []const u8,
|
||||||
|
|
||||||
|
pub fn parseCLI(value: ?[]const u8) !Self {
|
||||||
|
_ = value;
|
||||||
|
return Self{ .v = "HELLO!" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} = undefined;
|
||||||
|
|
||||||
|
try parseIntoField(@TypeOf(data), &data, "a", "42");
|
||||||
|
try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
|
||||||
|
}
|
60
src/config.zig
Normal file
60
src/config.zig
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const Config = struct {
|
||||||
|
/// Background color for the window.
|
||||||
|
background: Color = .{ .r = 0, .g = 0, .b = 0 },
|
||||||
|
|
||||||
|
/// Foreground color for the window.
|
||||||
|
foreground: Color = .{ .r = 0xFF, .g = 0xA5, .b = 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Color represents a color using RGB.
|
||||||
|
pub const Color = struct {
|
||||||
|
r: u8,
|
||||||
|
g: u8,
|
||||||
|
b: u8,
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
InvalidFormat,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parseCLI(input: ?[]const u8) !Color {
|
||||||
|
return fromHex(input orelse return error.ValueRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fromHex parses a color from a hex value such as #RRGGBB. The "#"
|
||||||
|
/// is optional.
|
||||||
|
pub fn fromHex(input: []const u8) !Color {
|
||||||
|
// Trim the beginning '#' if it exists
|
||||||
|
const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input;
|
||||||
|
|
||||||
|
// We expect exactly 6 for RRGGBB
|
||||||
|
if (trimmed.len != 6) return Error.InvalidFormat;
|
||||||
|
|
||||||
|
// Parse the colors two at a time.
|
||||||
|
var result: Color = undefined;
|
||||||
|
comptime var i: usize = 0;
|
||||||
|
inline while (i < 6) : (i += 2) {
|
||||||
|
const v: u8 =
|
||||||
|
((try std.fmt.charToDigit(trimmed[i], 16)) * 10) +
|
||||||
|
try std.fmt.charToDigit(trimmed[i + 1], 16);
|
||||||
|
|
||||||
|
@field(result, switch (i) {
|
||||||
|
0 => "r",
|
||||||
|
2 => "g",
|
||||||
|
4 => "b",
|
||||||
|
else => unreachable,
|
||||||
|
}) = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "Color.fromHex" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
try testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.fromHex("#000000"));
|
||||||
|
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C"));
|
||||||
|
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C"));
|
||||||
|
}
|
17
src/main.zig
17
src/main.zig
@ -3,7 +3,9 @@ const std = @import("std");
|
|||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
|
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
|
const cli_args = @import("cli_args.zig");
|
||||||
const tracy = @import("tracy/tracy.zig");
|
const tracy = @import("tracy/tracy.zig");
|
||||||
|
const Config = @import("config.zig").Config;
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
|
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
@ -13,12 +15,21 @@ pub fn main() !void {
|
|||||||
// If we're tracing, then wrap memory so we can trace allocations
|
// If we're tracing, then wrap memory so we can trace allocations
|
||||||
const alloc = if (!tracy.enabled) gpa else tracy.allocator(gpa, null).allocator();
|
const alloc = if (!tracy.enabled) gpa else tracy.allocator(gpa, null).allocator();
|
||||||
|
|
||||||
|
// Parse the config from the CLI args
|
||||||
|
const config = config: {
|
||||||
|
var result: Config = .{};
|
||||||
|
var iter = try std.process.argsWithAllocator(alloc);
|
||||||
|
defer iter.deinit();
|
||||||
|
try cli_args.parse(Config, &result, &iter);
|
||||||
|
break :config result;
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize glfw
|
// Initialize glfw
|
||||||
try glfw.init(.{});
|
try glfw.init(.{});
|
||||||
defer glfw.terminate();
|
defer glfw.terminate();
|
||||||
|
|
||||||
// Run our app
|
// Run our app
|
||||||
var app = try App.init(alloc);
|
var app = try App.init(alloc, &config);
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
try app.run();
|
try app.run();
|
||||||
}
|
}
|
||||||
@ -41,4 +52,8 @@ test {
|
|||||||
_ = @import("segmented_pool.zig");
|
_ = @import("segmented_pool.zig");
|
||||||
_ = @import("libuv/main.zig");
|
_ = @import("libuv/main.zig");
|
||||||
_ = @import("terminal/main.zig");
|
_ = @import("terminal/main.zig");
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
_ = @import("config.zig");
|
||||||
|
_ = @import("cli_args.zig");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user