From 3b54d05aeca9b3fe1116a975e0c7df470722e0e6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 May 2022 14:00:35 -0700 Subject: [PATCH] CLI parsing, can set default foreground/background color --- src/App.zig | 9 ++- src/Grid.zig | 11 ++-- src/Window.zig | 24 ++++++- src/cli_args.zig | 158 +++++++++++++++++++++++++++++++++++++++++++++++ src/config.zig | 60 ++++++++++++++++++ src/main.zig | 17 ++++- 6 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 src/cli_args.zig create mode 100644 src/config.zig diff --git a/src/App.zig b/src/App.zig index 70451e09d..2332c5304 100644 --- a/src/App.zig +++ b/src/App.zig @@ -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, }; } diff --git a/src/Grid.zig b/src/Grid.zig index b9cc03825..b65f990eb 100644 --- a/src/Grid.zig +++ b/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), diff --git a/src/Window.zig b/src/Window.zig index 57f5ed997..42b07148c 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -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 diff --git a/src/cli_args.zig b/src/cli_args.zig new file mode 100644 index 000000000..e438aacff --- /dev/null +++ b/src/cli_args.zig @@ -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); +} diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 000000000..e5a6d031c --- /dev/null +++ b/src/config.zig @@ -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")); +} diff --git a/src/main.zig b/src/main.zig index 34f8c64c4..4ef31ce6a 100644 --- a/src/main.zig +++ b/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"); }