mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +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 libuv = @import("libuv/main.zig");
|
||||
const tracy = @import("tracy/tracy.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
@ -24,10 +25,13 @@ window: *Window,
|
||||
// so that users of the loop always have an allocator.
|
||||
loop: libuv.Loop,
|
||||
|
||||
// The configuration for the app.
|
||||
config: *const Config,
|
||||
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||
/// "startup" logic.
|
||||
pub fn init(alloc: Allocator) !App {
|
||||
pub fn init(alloc: Allocator, config: *const Config) !App {
|
||||
// Create the event loop
|
||||
var loop = try libuv.Loop.init(alloc);
|
||||
errdefer loop.deinit(alloc);
|
||||
@ -40,13 +44,14 @@ pub fn init(alloc: Allocator) !App {
|
||||
loop.setData(allocPtr);
|
||||
|
||||
// Create the window
|
||||
var window = try Window.create(alloc, loop);
|
||||
var window = try Window.create(alloc, loop, config);
|
||||
errdefer window.destroy();
|
||||
|
||||
return App{
|
||||
.alloc = alloc,
|
||||
.window = window,
|
||||
.loop = loop,
|
||||
.config = config,
|
||||
};
|
||||
}
|
||||
|
||||
|
11
src/Grid.zig
11
src/Grid.zig
@ -40,6 +40,9 @@ font_atlas: FontAtlas,
|
||||
cursor_visible: bool,
|
||||
cursor_style: CursorStyle,
|
||||
|
||||
/// Default foreground color
|
||||
foreground: Terminal.RGB,
|
||||
|
||||
const CursorStyle = enum(u8) {
|
||||
box = 3,
|
||||
box_hollow = 4,
|
||||
@ -219,6 +222,7 @@ pub fn init(alloc: Allocator) !Grid {
|
||||
.font_atlas = font,
|
||||
.cursor_visible = true,
|
||||
.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.
|
||||
const glyph = try self.font_atlas.addGlyph(self.alloc, cell.char);
|
||||
|
||||
const fg = cell.fg orelse Terminal.RGB{
|
||||
.r = 0xFF,
|
||||
.g = 0xA5,
|
||||
.b = 0,
|
||||
};
|
||||
|
||||
const fg = cell.fg orelse self.foreground;
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = 2,
|
||||
.grid_col = @intCast(u16, x),
|
||||
|
@ -20,6 +20,7 @@ const frame = @import("tracy/tracy.zig").frame;
|
||||
const trace = @import("tracy/tracy.zig").trace;
|
||||
const max_timer = @import("max_timer.zig");
|
||||
const terminal = @import("terminal/main.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
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.
|
||||
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
|
||||
/// 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 {
|
||||
pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Window {
|
||||
var self = try alloc.create(Window);
|
||||
errdefer alloc.destroy(self);
|
||||
|
||||
@ -124,6 +134,11 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
|
||||
const window_size = try window.getSize();
|
||||
var grid = try Grid.init(alloc);
|
||||
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
|
||||
var pty = try Pty.open(.{
|
||||
@ -192,6 +207,11 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
|
||||
.cursor_timer = timer,
|
||||
.render_timer = try RenderTimer.init(loop, self, 16, 96),
|
||||
.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
|
||||
@ -480,7 +500,7 @@ fn renderTimerCallback(t: *libuv.Timer) void {
|
||||
win.grid.updateCells(win.terminal) catch unreachable;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 App = @import("App.zig");
|
||||
const cli_args = @import("cli_args.zig");
|
||||
const tracy = @import("tracy/tracy.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
pub fn main() !void {
|
||||
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
|
||||
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
|
||||
try glfw.init(.{});
|
||||
defer glfw.terminate();
|
||||
|
||||
// Run our app
|
||||
var app = try App.init(alloc);
|
||||
var app = try App.init(alloc, &config);
|
||||
defer app.deinit();
|
||||
try app.run();
|
||||
}
|
||||
@ -41,4 +52,8 @@ test {
|
||||
_ = @import("segmented_pool.zig");
|
||||
_ = @import("libuv/main.zig");
|
||||
_ = @import("terminal/main.zig");
|
||||
|
||||
// TODO
|
||||
_ = @import("config.zig");
|
||||
_ = @import("cli_args.zig");
|
||||
}
|
||||
|
Reference in New Issue
Block a user