CLI parsing, can set default foreground/background color

This commit is contained in:
Mitchell Hashimoto
2022-05-19 14:00:35 -07:00
parent 208bed34ad
commit 3b54d05aec
6 changed files with 268 additions and 11 deletions

View File

@ -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,
}; };
} }

View File

@ -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),

View File

@ -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
View 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
View 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"));
}

View File

@ -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");
} }