mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
Merge pull request #2454 from ghostty-org/diags
Configuration errors improvements: exit on CLI errors, include line number and filepath
This commit is contained in:
@ -291,7 +291,7 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
const char* message;
|
||||
} ghostty_error_s;
|
||||
} ghostty_diagnostic_s;
|
||||
|
||||
typedef struct {
|
||||
double tl_px_x;
|
||||
@ -607,7 +607,6 @@ ghostty_info_s ghostty_info(void);
|
||||
ghostty_config_t ghostty_config_new();
|
||||
void ghostty_config_free(ghostty_config_t);
|
||||
void ghostty_config_load_cli_args(ghostty_config_t);
|
||||
void ghostty_config_load_string(ghostty_config_t, const char*, uintptr_t);
|
||||
void ghostty_config_load_default_files(ghostty_config_t);
|
||||
void ghostty_config_load_recursive_files(ghostty_config_t);
|
||||
void ghostty_config_finalize(ghostty_config_t);
|
||||
@ -615,8 +614,8 @@ bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
|
||||
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
|
||||
const char*,
|
||||
uintptr_t);
|
||||
uint32_t ghostty_config_errors_count(ghostty_config_t);
|
||||
ghostty_error_s ghostty_config_get_error(ghostty_config_t, uint32_t);
|
||||
uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
|
||||
ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
|
||||
void ghostty_config_open();
|
||||
|
||||
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||
|
@ -22,15 +22,15 @@ extension Ghostty {
|
||||
var errors: [String] {
|
||||
guard let cfg = self.config else { return [] }
|
||||
|
||||
var errors: [String] = [];
|
||||
let errCount = ghostty_config_errors_count(cfg)
|
||||
for i in 0..<errCount {
|
||||
let err = ghostty_config_get_error(cfg, UInt32(i))
|
||||
let message = String(cString: err.message)
|
||||
errors.append(message)
|
||||
var diags: [String] = [];
|
||||
let diagsCount = ghostty_config_diagnostics_count(cfg)
|
||||
for i in 0..<diagsCount {
|
||||
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
|
||||
let message = String(cString: diag.message)
|
||||
diags.append(message)
|
||||
}
|
||||
|
||||
return errors
|
||||
return diags
|
||||
}
|
||||
|
||||
init() {
|
||||
@ -69,14 +69,14 @@ extension Ghostty {
|
||||
|
||||
// Log any configuration errors. These will be automatically shown in a
|
||||
// pop-up window too.
|
||||
let errCount = ghostty_config_errors_count(cfg)
|
||||
if errCount > 0 {
|
||||
logger.warning("config error: \(errCount) configuration errors on reload")
|
||||
var errors: [String] = [];
|
||||
for i in 0..<errCount {
|
||||
let err = ghostty_config_get_error(cfg, UInt32(i))
|
||||
let message = String(cString: err.message)
|
||||
errors.append(message)
|
||||
let diagsCount = ghostty_config_diagnostics_count(cfg)
|
||||
if diagsCount > 0 {
|
||||
logger.warning("config error: \(diagsCount) configuration errors on reload")
|
||||
var diags: [String] = [];
|
||||
for i in 0..<diagsCount {
|
||||
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
|
||||
let message = String(cString: diag.message)
|
||||
diags.append(message)
|
||||
logger.warning("config error: \(message)")
|
||||
}
|
||||
}
|
||||
|
11
src/App.zig
11
src/App.zig
@ -231,10 +231,19 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
.open_config => try self.performAction(rt_app, .open_config),
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.close => |surface| self.closeSurface(surface),
|
||||
.quit => self.setQuit(),
|
||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||
.redraw_surface => |surface| self.redrawSurface(rt_app, surface),
|
||||
.redraw_inspector => |surface| self.redrawInspector(rt_app, surface),
|
||||
|
||||
// If we're quitting, then we set the quit flag and stop
|
||||
// draining the mailbox immediately. This lets us defer
|
||||
// mailbox processing to the next tick so that the apprt
|
||||
// can try to quick as quickly as possible.
|
||||
.quit => {
|
||||
log.info("quit message received, short circuiting mailbox drain", .{});
|
||||
self.setQuit();
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ const Allocator = std.mem.Allocator;
|
||||
const glfw = @import("glfw");
|
||||
const macos = @import("macos");
|
||||
const objc = @import("objc");
|
||||
const cli = @import("../cli.zig");
|
||||
const input = @import("../input.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
@ -69,13 +70,26 @@ pub const App = struct {
|
||||
errdefer config.deinit();
|
||||
|
||||
// If we had configuration errors, then log them.
|
||||
if (!config._errors.empty()) {
|
||||
for (config._errors.list.items) |err| {
|
||||
log.warn("configuration error: {s}", .{err.message});
|
||||
if (!config._diagnostics.empty()) {
|
||||
var buf = std.ArrayList(u8).init(core_app.alloc);
|
||||
defer buf.deinit();
|
||||
for (config._diagnostics.items()) |diag| {
|
||||
try diag.write(buf.writer());
|
||||
log.warn("configuration error: {s}", .{buf.items});
|
||||
buf.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
// If we have any CLI errors, exit.
|
||||
if (config._diagnostics.containsLocation(.cli)) {
|
||||
log.warn("CLI errors detected, exiting", .{});
|
||||
_ = core_app.mailbox.push(.{
|
||||
.quit = {},
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
}
|
||||
|
||||
// Queue a single new window that starts on launch
|
||||
// Note: above we may send a quit so this may never happen
|
||||
_ = core_app.mailbox.push(.{
|
||||
.new_window = .{},
|
||||
}, .{ .forever = {} });
|
||||
|
@ -123,9 +123,19 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
errdefer config.deinit();
|
||||
|
||||
// If we had configuration errors, then log them.
|
||||
if (!config._errors.empty()) {
|
||||
for (config._errors.list.items) |err| {
|
||||
log.warn("configuration error: {s}", .{err.message});
|
||||
if (!config._diagnostics.empty()) {
|
||||
var buf = std.ArrayList(u8).init(core_app.alloc);
|
||||
defer buf.deinit();
|
||||
for (config._diagnostics.items()) |diag| {
|
||||
try diag.write(buf.writer());
|
||||
log.warn("configuration error: {s}", .{buf.items});
|
||||
buf.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
// If we have any CLI errors, exit.
|
||||
if (config._diagnostics.containsLocation(.cli)) {
|
||||
log.warn("CLI errors detected, exiting", .{});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -815,7 +825,7 @@ fn syncConfigChanges(self: *App) !void {
|
||||
/// there are new configuration errors and hide the window if the errors
|
||||
/// are resolved.
|
||||
fn updateConfigErrors(self: *App) !void {
|
||||
if (!self.config._errors.empty()) {
|
||||
if (!self.config._diagnostics.empty()) {
|
||||
if (self.config_errors_window == null) {
|
||||
try ConfigErrorsWindow.create(self);
|
||||
assert(self.config_errors_window != null);
|
||||
@ -1364,10 +1374,7 @@ fn gtkActionQuit(
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
self.core_app.setQuit() catch |err| {
|
||||
log.warn("error setting quit err={}", .{err});
|
||||
return;
|
||||
};
|
||||
self.core_app.setQuit();
|
||||
}
|
||||
|
||||
/// Action sent by the window manager asking us to present a specific surface to
|
||||
|
@ -28,7 +28,7 @@ pub fn create(app: *App) !void {
|
||||
}
|
||||
|
||||
pub fn update(self: *ConfigErrors) void {
|
||||
if (self.app.config._errors.empty()) {
|
||||
if (self.app.config._diagnostics.empty()) {
|
||||
c.gtk_window_destroy(@ptrCast(self.window));
|
||||
return;
|
||||
}
|
||||
@ -130,8 +130,21 @@ const PrimaryView = struct {
|
||||
const buf = c.gtk_text_buffer_new(null);
|
||||
errdefer c.g_object_unref(buf);
|
||||
|
||||
for (config._errors.list.items) |err| {
|
||||
c.gtk_text_buffer_insert_at_cursor(buf, err.message, @intCast(err.message.len));
|
||||
var msg_buf: [4096]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&msg_buf);
|
||||
|
||||
for (config._diagnostics.items()) |diag| {
|
||||
fbs.reset();
|
||||
diag.write(fbs.writer()) catch |err| {
|
||||
log.warn(
|
||||
"error writing diagnostic to buffer err={}",
|
||||
.{err},
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
const msg = fbs.getWritten();
|
||||
c.gtk_text_buffer_insert_at_cursor(buf, msg.ptr, @intCast(msg.len));
|
||||
c.gtk_text_buffer_insert_at_cursor(buf, "\n", -1);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,10 @@
|
||||
const diags = @import("cli/diagnostics.zig");
|
||||
|
||||
pub const args = @import("cli/args.zig");
|
||||
pub const Action = @import("cli/action.zig").Action;
|
||||
pub const DiagnosticList = diags.DiagnosticList;
|
||||
pub const Diagnostic = diags.Diagnostic;
|
||||
pub const Location = diags.Location;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
288
src/cli/args.zig
288
src/cli/args.zig
@ -3,8 +3,9 @@ const mem = std.mem;
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const ErrorList = @import("../config/ErrorList.zig");
|
||||
const diags = @import("diagnostics.zig");
|
||||
const Diagnostic = diags.Diagnostic;
|
||||
const DiagnosticList = diags.DiagnosticList;
|
||||
|
||||
// TODO:
|
||||
// - Only `--long=value` format is accepted. Do we want to allow
|
||||
@ -32,13 +33,18 @@ pub const Error = error{
|
||||
/// an arena allocator will be created (or reused if set already) for any
|
||||
/// allocations. Allocations are necessary for certain types, like `[]const u8`.
|
||||
///
|
||||
/// If the destination type has a field "_errors" of type "ErrorList" then
|
||||
/// errors will be added to that list. In this case, the only error returned by
|
||||
/// parse are allocation errors.
|
||||
/// If the destination type has a field "_diagnostics", it must be of type
|
||||
/// "DiagnosticList" and any diagnostic messages will be added to that list.
|
||||
/// When diagnostics are present, only allocation errors will be returned.
|
||||
///
|
||||
/// Note: If the arena is already non-null, then it will be used. In this
|
||||
/// case, in the case of an error some memory might be leaked into the arena.
|
||||
pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
||||
pub fn parse(
|
||||
comptime T: type,
|
||||
alloc: Allocator,
|
||||
dst: *T,
|
||||
iter: anytype,
|
||||
) !void {
|
||||
const info = @typeInfo(T);
|
||||
assert(info == .Struct);
|
||||
|
||||
@ -69,7 +75,11 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
||||
while (iter.next()) |arg| {
|
||||
// Do manual parsing if we have a hook for it.
|
||||
if (@hasDecl(T, "parseManuallyHook")) {
|
||||
if (!try dst.parseManuallyHook(arena_alloc, arg, iter)) return;
|
||||
if (!try dst.parseManuallyHook(
|
||||
arena_alloc,
|
||||
arg,
|
||||
iter,
|
||||
)) return;
|
||||
}
|
||||
|
||||
// If the destination supports help then we check for it, call
|
||||
@ -83,7 +93,22 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
||||
}
|
||||
}
|
||||
|
||||
if (mem.startsWith(u8, arg, "--")) {
|
||||
// If this doesn't start with "--" then it isn't a config
|
||||
// flag. We don't support positional arguments or configuration
|
||||
// values set with spaces so this is an error.
|
||||
if (!mem.startsWith(u8, arg, "--")) {
|
||||
if (comptime !canTrackDiags(T)) return Error.InvalidField;
|
||||
|
||||
// Add our diagnostic
|
||||
try dst._diagnostics.append(arena_alloc, .{
|
||||
.key = try arena_alloc.dupeZ(u8, arg),
|
||||
.message = "invalid field",
|
||||
.location = diags.Location.fromIter(iter),
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var key: []const u8 = arg[2..];
|
||||
const value: ?[]const u8 = value: {
|
||||
// If the arg has "=" then the value is after the "=".
|
||||
@ -96,56 +121,38 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
||||
};
|
||||
|
||||
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
|
||||
if (comptime !canTrackErrors(T)) return err;
|
||||
if (comptime !canTrackDiags(T)) return err;
|
||||
|
||||
// The error set is dependent on comptime T, so we always add
|
||||
// an extra error so we can have the "else" below.
|
||||
const ErrSet = @TypeOf(err) || error{Unknown};
|
||||
switch (@as(ErrSet, @errorCast(err))) {
|
||||
const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) {
|
||||
// OOM is not recoverable since we need to allocate to
|
||||
// track more error messages.
|
||||
error.OutOfMemory => return err,
|
||||
|
||||
error.InvalidField => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
error.InvalidField => "unknown field",
|
||||
error.ValueRequired => "value required",
|
||||
error.InvalidValue => "invalid value",
|
||||
else => try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: unknown field",
|
||||
.{key},
|
||||
"unknown error {}",
|
||||
.{err},
|
||||
),
|
||||
}),
|
||||
|
||||
error.ValueRequired => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: value required",
|
||||
.{key},
|
||||
),
|
||||
}),
|
||||
|
||||
error.InvalidValue => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: invalid value",
|
||||
.{key},
|
||||
),
|
||||
}),
|
||||
|
||||
else => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: unknown error {}",
|
||||
.{ key, err },
|
||||
),
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add our diagnostic
|
||||
try dst._diagnostics.append(arena_alloc, .{
|
||||
.key = try arena_alloc.dupeZ(u8, key),
|
||||
.message = message,
|
||||
.location = diags.Location.fromIter(iter),
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this type can track errors.
|
||||
fn canTrackErrors(comptime T: type) bool {
|
||||
return @hasField(T, "_errors");
|
||||
/// Returns true if this type can track diagnostics.
|
||||
fn canTrackDiags(comptime T: type) bool {
|
||||
return @hasField(T, "_diagnostics");
|
||||
}
|
||||
|
||||
/// Parse a single key/value pair into the destination type T.
|
||||
@ -199,15 +206,6 @@ fn parseIntoField(
|
||||
// 3 arg = (self, alloc, input) => void
|
||||
3 => try @field(dst, field.name).parseCLI(alloc, value),
|
||||
|
||||
// 4 arg = (self, alloc, errors, input) => void
|
||||
4 => if (comptime canTrackErrors(T)) {
|
||||
try @field(dst, field.name).parseCLI(alloc, &dst._errors, value);
|
||||
} else {
|
||||
var list: ErrorList = .{};
|
||||
try @field(dst, field.name).parseCLI(alloc, &list, value);
|
||||
if (!list.empty()) return error.InvalidValue;
|
||||
},
|
||||
|
||||
else => @compileError("parseCLI invalid argument count"),
|
||||
}
|
||||
|
||||
@ -468,7 +466,28 @@ test "parse: empty value resets to default" {
|
||||
try testing.expect(!data.b);
|
||||
}
|
||||
|
||||
test "parse: error tracking" {
|
||||
test "parse: positional arguments are invalid" {
|
||||
const testing = std.testing;
|
||||
|
||||
var data: struct {
|
||||
a: u8 = 42,
|
||||
_arena: ?ArenaAllocator = null,
|
||||
} = .{};
|
||||
defer if (data._arena) |arena| arena.deinit();
|
||||
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
testing.allocator,
|
||||
"--a=84 what",
|
||||
);
|
||||
defer iter.deinit();
|
||||
try testing.expectError(
|
||||
error.InvalidField,
|
||||
parse(@TypeOf(data), testing.allocator, &data, &iter),
|
||||
);
|
||||
try testing.expectEqual(@as(u8, 84), data.a);
|
||||
}
|
||||
|
||||
test "parse: diagnostic tracking" {
|
||||
const testing = std.testing;
|
||||
|
||||
var data: struct {
|
||||
@ -476,7 +495,7 @@ test "parse: error tracking" {
|
||||
b: enum { one } = .one,
|
||||
|
||||
_arena: ?ArenaAllocator = null,
|
||||
_errors: ErrorList = .{},
|
||||
_diagnostics: DiagnosticList = .{},
|
||||
} = .{};
|
||||
defer if (data._arena) |arena| arena.deinit();
|
||||
|
||||
@ -488,7 +507,48 @@ test "parse: error tracking" {
|
||||
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
||||
try testing.expect(data._arena != null);
|
||||
try testing.expectEqualStrings("42", data.a);
|
||||
try testing.expect(!data._errors.empty());
|
||||
try testing.expect(data._diagnostics.items().len == 1);
|
||||
{
|
||||
const diag = data._diagnostics.items()[0];
|
||||
try testing.expectEqual(diags.Location.none, diag.location);
|
||||
try testing.expectEqualStrings("what", diag.key);
|
||||
try testing.expectEqualStrings("unknown field", diag.message);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: diagnostic location" {
|
||||
const testing = std.testing;
|
||||
|
||||
var data: struct {
|
||||
a: []const u8 = "",
|
||||
b: enum { one, two } = .one,
|
||||
|
||||
_arena: ?ArenaAllocator = null,
|
||||
_diagnostics: DiagnosticList = .{},
|
||||
} = .{};
|
||||
defer if (data._arena) |arena| arena.deinit();
|
||||
|
||||
var fbs = std.io.fixedBufferStream(
|
||||
\\a=42
|
||||
\\what
|
||||
\\b=two
|
||||
);
|
||||
const r = fbs.reader();
|
||||
|
||||
const Iter = LineIterator(@TypeOf(r));
|
||||
var iter: Iter = .{ .r = r, .filepath = "test" };
|
||||
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
||||
try testing.expect(data._arena != null);
|
||||
try testing.expectEqualStrings("42", data.a);
|
||||
try testing.expect(data.b == .two);
|
||||
try testing.expect(data._diagnostics.items().len == 1);
|
||||
{
|
||||
const diag = data._diagnostics.items()[0];
|
||||
try testing.expectEqualStrings("what", diag.key);
|
||||
try testing.expectEqualStrings("unknown field", diag.message);
|
||||
try testing.expectEqualStrings("test", diag.location.file.path);
|
||||
try testing.expectEqual(2, diag.location.file.line);
|
||||
}
|
||||
}
|
||||
|
||||
test "parseIntoField: ignore underscore-prefixed fields" {
|
||||
@ -738,62 +798,6 @@ test "parseIntoField: struct with parse func" {
|
||||
try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
|
||||
}
|
||||
|
||||
test "parseIntoField: struct with parse func with error tracking" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
a: struct {
|
||||
const Self = @This();
|
||||
|
||||
pub fn parseCLI(
|
||||
_: Self,
|
||||
parse_alloc: Allocator,
|
||||
errors: *ErrorList,
|
||||
value: ?[]const u8,
|
||||
) !void {
|
||||
_ = value;
|
||||
try errors.add(parse_alloc, .{ .message = "OH NO!" });
|
||||
}
|
||||
} = .{},
|
||||
|
||||
_errors: ErrorList = .{},
|
||||
} = .{};
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
|
||||
try testing.expect(!data._errors.empty());
|
||||
}
|
||||
|
||||
test "parseIntoField: struct with parse func with unsupported error tracking" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
a: struct {
|
||||
const Self = @This();
|
||||
|
||||
pub fn parseCLI(
|
||||
_: Self,
|
||||
parse_alloc: Allocator,
|
||||
errors: *ErrorList,
|
||||
value: ?[]const u8,
|
||||
) !void {
|
||||
_ = value;
|
||||
try errors.add(parse_alloc, .{ .message = "OH NO!" });
|
||||
}
|
||||
} = .{},
|
||||
} = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidValue,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "a", "42"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseIntoField: tagged union" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
@ -887,6 +891,34 @@ test "parseIntoField: tagged union missing tag" {
|
||||
);
|
||||
}
|
||||
|
||||
/// An iterator that considers its location to be CLI args. It
|
||||
/// iterates through an underlying iterator and increments a counter
|
||||
/// to track the current CLI arg index.
|
||||
pub fn ArgsIterator(comptime Iterator: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// The underlying args iterator.
|
||||
iterator: Iterator,
|
||||
|
||||
/// Our current index into the iterator. This is 1-indexed.
|
||||
/// The 0 value is used to indicate that we haven't read any
|
||||
/// values yet.
|
||||
index: usize = 0,
|
||||
|
||||
pub fn next(self: *Self) ?[]const u8 {
|
||||
const value = self.iterator.next() orelse return null;
|
||||
self.index += 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Returns a location for a diagnostic message.
|
||||
pub fn location(self: *const Self) ?diags.Location {
|
||||
return .{ .cli = self.index };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns an iterator (implements "next") that reads CLI args by line.
|
||||
/// Each CLI arg is expected to be a single line. This is used to implement
|
||||
/// configuration files.
|
||||
@ -899,7 +931,21 @@ pub fn LineIterator(comptime ReaderType: type) type {
|
||||
/// like 4 years and be wrong about this.
|
||||
pub const MAX_LINE_SIZE = 4096;
|
||||
|
||||
/// Our stateful reader.
|
||||
r: ReaderType,
|
||||
|
||||
/// Filepath that is used for diagnostics. This is only used for
|
||||
/// diagnostic messages so it can be formatted however you want.
|
||||
/// It is prefixed to the messages followed by the line number.
|
||||
filepath: []const u8 = "",
|
||||
|
||||
/// The current line that we're on. This is 1-indexed because
|
||||
/// lines are generally 1-indexed in the real world. The value
|
||||
/// can be zero if we haven't read any lines yet.
|
||||
line: usize = 0,
|
||||
|
||||
/// This is the buffer where we store the current entry that
|
||||
/// is formatted to be compatible with the parse function.
|
||||
entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)),
|
||||
|
||||
pub fn next(self: *Self) ?[]const u8 {
|
||||
@ -912,6 +958,9 @@ pub fn LineIterator(comptime ReaderType: type) type {
|
||||
unreachable;
|
||||
} orelse return null;
|
||||
|
||||
// Increment our line counter
|
||||
self.line += 1;
|
||||
|
||||
// Trim any whitespace (including CR) around it
|
||||
const trim = std.mem.trim(u8, entry, whitespace ++ "\r");
|
||||
if (trim.len != entry.len) {
|
||||
@ -959,11 +1008,22 @@ pub fn LineIterator(comptime ReaderType: type) type {
|
||||
// as CLI args.
|
||||
return self.entry[0 .. buf.len + 2];
|
||||
}
|
||||
|
||||
/// Returns a location for a diagnostic message.
|
||||
pub fn location(self: *const Self) ?diags.Location {
|
||||
// If we have no filepath then we have no location.
|
||||
if (self.filepath.len == 0) return null;
|
||||
|
||||
return .{ .file = .{
|
||||
.path = self.filepath,
|
||||
.line = self.line,
|
||||
} };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Constructs a LineIterator (see docs for that).
|
||||
pub fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) {
|
||||
fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) {
|
||||
return .{ .r = reader };
|
||||
}
|
||||
|
||||
|
139
src/cli/diagnostics.zig
Normal file
139
src/cli/diagnostics.zig
Normal file
@ -0,0 +1,139 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("../build_config.zig");
|
||||
|
||||
/// A diagnostic message from parsing. This is used to provide additional
|
||||
/// human-friendly warnings and errors about the parsed data.
|
||||
///
|
||||
/// All of the memory for the diagnostic is allocated from the arena
|
||||
/// associated with the config structure. If an arena isn't available
|
||||
/// then diagnostics are not supported.
|
||||
pub const Diagnostic = struct {
|
||||
location: Location = .none,
|
||||
key: [:0]const u8 = "",
|
||||
message: [:0]const u8,
|
||||
|
||||
/// Write the full user-friendly diagnostic message to the writer.
|
||||
pub fn write(self: *const Diagnostic, writer: anytype) !void {
|
||||
switch (self.location) {
|
||||
.none => {},
|
||||
.cli => |index| try writer.print("cli:{}:", .{index}),
|
||||
.file => |file| try writer.print(
|
||||
"{s}:{}:",
|
||||
.{ file.path, file.line },
|
||||
),
|
||||
}
|
||||
|
||||
if (self.key.len > 0) {
|
||||
try writer.print("{s}: ", .{self.key});
|
||||
} else if (self.location != .none) {
|
||||
try writer.print(" ", .{});
|
||||
}
|
||||
|
||||
try writer.print("{s}", .{self.message});
|
||||
}
|
||||
};
|
||||
|
||||
/// The possible locations for a diagnostic message. This is used
|
||||
/// to provide context for the message.
|
||||
pub const Location = union(enum) {
|
||||
none,
|
||||
cli: usize,
|
||||
file: struct {
|
||||
path: []const u8,
|
||||
line: usize,
|
||||
},
|
||||
|
||||
pub const Key = @typeInfo(Location).Union.tag_type.?;
|
||||
|
||||
pub fn fromIter(iter: anytype) Location {
|
||||
const Iter = t: {
|
||||
const T = @TypeOf(iter);
|
||||
break :t switch (@typeInfo(T)) {
|
||||
.Pointer => |v| v.child,
|
||||
.Struct => T,
|
||||
else => return .none,
|
||||
};
|
||||
};
|
||||
|
||||
if (!@hasDecl(Iter, "location")) return .none;
|
||||
return iter.location() orelse .none;
|
||||
}
|
||||
};
|
||||
|
||||
/// A list of diagnostics. The "_diagnostics" field must be this type
|
||||
/// for diagnostics to be supported. If this field is an incorrect type
|
||||
/// a compile-time error will be raised.
|
||||
///
|
||||
/// This is implemented as a simple wrapper around an array list
|
||||
/// so that we can inject some logic around adding diagnostics
|
||||
/// and potentially in the future structure them differently.
|
||||
pub const DiagnosticList = struct {
|
||||
/// The list of diagnostics.
|
||||
list: std.ArrayListUnmanaged(Diagnostic) = .{},
|
||||
|
||||
/// Precomputed data for diagnostics. This is used specifically
|
||||
/// when we build libghostty so that we can precompute the messages
|
||||
/// and return them via the C API without allocating memory at
|
||||
/// call time.
|
||||
precompute: Precompute = precompute_init,
|
||||
|
||||
const precompute_enabled = switch (build_config.artifact) {
|
||||
// We enable precompute for tests so that the logic is
|
||||
// semantically analyzed and run.
|
||||
.exe, .wasm_module => builtin.is_test,
|
||||
|
||||
// We specifically want precompute for libghostty.
|
||||
.lib => true,
|
||||
};
|
||||
const Precompute = if (precompute_enabled) struct {
|
||||
messages: std.ArrayListUnmanaged([:0]const u8) = .{},
|
||||
} else void;
|
||||
const precompute_init: Precompute = if (precompute_enabled) .{} else {};
|
||||
|
||||
pub fn append(
|
||||
self: *DiagnosticList,
|
||||
alloc: Allocator,
|
||||
diag: Diagnostic,
|
||||
) Allocator.Error!void {
|
||||
try self.list.append(alloc, diag);
|
||||
errdefer _ = self.list.pop();
|
||||
|
||||
if (comptime precompute_enabled) {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
try diag.write(buf.writer());
|
||||
|
||||
const owned: [:0]const u8 = try buf.toOwnedSliceSentinel(0);
|
||||
errdefer alloc.free(owned);
|
||||
|
||||
try self.precompute.messages.append(alloc, owned);
|
||||
errdefer _ = self.precompute.messages.pop();
|
||||
|
||||
assert(self.precompute.messages.items.len == self.list.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(self: *const DiagnosticList) bool {
|
||||
return self.list.items.len == 0;
|
||||
}
|
||||
|
||||
pub fn items(self: *const DiagnosticList) []const Diagnostic {
|
||||
return self.list.items;
|
||||
}
|
||||
|
||||
/// Returns true if there are any diagnostics for the given
|
||||
/// location type.
|
||||
pub fn containsLocation(
|
||||
self: *const DiagnosticList,
|
||||
location: Location.Key,
|
||||
) bool {
|
||||
for (self.list.items) |diag| {
|
||||
if (diag.location == location) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
@ -882,7 +882,7 @@ const Preview = struct {
|
||||
next_start += child.height;
|
||||
}
|
||||
|
||||
if (!config._errors.empty()) {
|
||||
if (config._diagnostics.items().len > 0) {
|
||||
const child = win.child(
|
||||
.{
|
||||
.x_off = x_off,
|
||||
@ -891,7 +891,7 @@ const Preview = struct {
|
||||
.limit = width,
|
||||
},
|
||||
.height = .{
|
||||
.limit = if (config._errors.empty()) 0 else 2 + config._errors.list.items.len,
|
||||
.limit = if (config._diagnostics.items().len == 0) 0 else 2 + config._diagnostics.items().len,
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -908,10 +908,14 @@ const Preview = struct {
|
||||
},
|
||||
);
|
||||
}
|
||||
for (config._errors.list.items, 0..) |err, i| {
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
for (config._diagnostics.items(), 0..) |diag, i| {
|
||||
try diag.write(buf.writer());
|
||||
_ = try child.printSegment(
|
||||
.{
|
||||
.text = err.message,
|
||||
.text = buf.items,
|
||||
.style = self.ui_err(),
|
||||
},
|
||||
.{
|
||||
@ -919,6 +923,7 @@ const Preview = struct {
|
||||
.col_offset = 2,
|
||||
},
|
||||
);
|
||||
buf.clearRetainingCapacity();
|
||||
}
|
||||
next_start += child.height;
|
||||
}
|
||||
|
@ -55,9 +55,14 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
|
||||
|
||||
try cfg.finalize();
|
||||
|
||||
if (!cfg._errors.empty()) {
|
||||
for (cfg._errors.list.items) |err| {
|
||||
try stdout.print("{s}\n", .{err.message});
|
||||
if (cfg._diagnostics.items().len > 0) {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
for (cfg._diagnostics.items()) |diag| {
|
||||
try diag.write(buf.writer());
|
||||
try stdout.print("{s}\n", .{buf.items});
|
||||
buf.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
@ -39,24 +39,6 @@ export fn ghostty_config_load_cli_args(self: *Config) void {
|
||||
};
|
||||
}
|
||||
|
||||
/// Load the configuration from a string in the same format as
|
||||
/// the file-based syntax for the desktop version of the terminal.
|
||||
export fn ghostty_config_load_string(
|
||||
self: *Config,
|
||||
str: [*]const u8,
|
||||
len: usize,
|
||||
) void {
|
||||
config_load_string_(self, str[0..len]) catch |err| {
|
||||
log.err("error loading config err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn config_load_string_(self: *Config, str: []const u8) !void {
|
||||
var fbs = std.io.fixedBufferStream(str);
|
||||
var iter = cli.args.lineIterator(fbs.reader());
|
||||
try cli.args.parse(Config, global.alloc, self, &iter);
|
||||
}
|
||||
|
||||
/// Load the configuration from the default file locations. This
|
||||
/// is usually done first. The default file locations are locations
|
||||
/// such as the home directory.
|
||||
@ -112,14 +94,15 @@ fn config_trigger_(
|
||||
return trigger.cval();
|
||||
}
|
||||
|
||||
export fn ghostty_config_errors_count(self: *Config) u32 {
|
||||
return @intCast(self._errors.list.items.len);
|
||||
export fn ghostty_config_diagnostics_count(self: *Config) u32 {
|
||||
return @intCast(self._diagnostics.items().len);
|
||||
}
|
||||
|
||||
export fn ghostty_config_get_error(self: *Config, idx: u32) Error {
|
||||
if (idx >= self._errors.list.items.len) return .{};
|
||||
const err = self._errors.list.items[idx];
|
||||
return .{ .message = err.message.ptr };
|
||||
export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic {
|
||||
const items = self._diagnostics.items();
|
||||
if (idx >= items.len) return .{};
|
||||
const message = self._diagnostics.precompute.messages.items[idx];
|
||||
return .{ .message = message.ptr };
|
||||
}
|
||||
|
||||
export fn ghostty_config_open() void {
|
||||
@ -128,7 +111,7 @@ export fn ghostty_config_open() void {
|
||||
};
|
||||
}
|
||||
|
||||
/// Sync with ghostty_error_s
|
||||
const Error = extern struct {
|
||||
/// Sync with ghostty_diagnostic_s
|
||||
const Diagnostic = extern struct {
|
||||
message: [*:0]const u8 = "",
|
||||
};
|
||||
|
@ -1662,10 +1662,9 @@ term: []const u8 = "xterm-ghostty",
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
/// List of errors that occurred while loading. This can be accessed directly
|
||||
/// by callers. It is only underscore-prefixed so it can't be set by the
|
||||
/// configuration file.
|
||||
_errors: ErrorList = .{},
|
||||
/// List of diagnostics that were generated during the loading of
|
||||
/// the configuration.
|
||||
_diagnostics: cli.DiagnosticList = .{},
|
||||
|
||||
/// The steps we can use to reload the configuration after it has been loaded
|
||||
/// without reopening the files. This is used in very specific cases such
|
||||
@ -2261,7 +2260,9 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
|
||||
std.log.info("reading configuration file path={s}", .{path});
|
||||
|
||||
var buf_reader = std.io.bufferedReader(file.reader());
|
||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||
const reader = buf_reader.reader();
|
||||
const Iter = cli.args.LineIterator(@TypeOf(reader));
|
||||
var iter: Iter = .{ .r = reader, .filepath = path };
|
||||
try self.loadIter(alloc, &iter);
|
||||
try self.expandPaths(std.fs.path.dirname(path).?);
|
||||
}
|
||||
@ -2371,7 +2372,11 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
||||
if (iter.next()) |argv0| log.debug("skipping argv0 value={s}", .{argv0});
|
||||
|
||||
// Parse the config from the CLI args
|
||||
try self.loadIter(alloc_gpa, &iter);
|
||||
{
|
||||
const ArgsIter = cli.args.ArgsIterator(@TypeOf(iter));
|
||||
var args_iter: ArgsIter = .{ .iterator = iter };
|
||||
try self.loadIter(alloc_gpa, &args_iter);
|
||||
}
|
||||
|
||||
// If we are not loading the default files, then we need to
|
||||
// replay the steps up to this point so that we can rebuild
|
||||
@ -2446,7 +2451,7 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
||||
|
||||
// We must only load a unique file once
|
||||
if (try loaded.fetchPut(path, {}) != null) {
|
||||
try self._errors.add(arena_alloc, .{
|
||||
try self._diagnostics.append(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"config-file {s}: cycle detected",
|
||||
@ -2458,7 +2463,7 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
||||
|
||||
var file = cwd.openFile(path, .{}) catch |err| {
|
||||
if (err != error.FileNotFound or !optional) {
|
||||
try self._errors.add(arena_alloc, .{
|
||||
try self._diagnostics.append(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"error opening config-file {s}: {}",
|
||||
@ -2472,7 +2477,9 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
||||
|
||||
log.info("loading config-file path={s}", .{path});
|
||||
var buf_reader = std.io.bufferedReader(file.reader());
|
||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||
const reader = buf_reader.reader();
|
||||
const Iter = cli.args.LineIterator(@TypeOf(reader));
|
||||
var iter: Iter = .{ .r = reader, .filepath = path };
|
||||
try self.loadIter(alloc_gpa, &iter);
|
||||
try self.expandPaths(std.fs.path.dirname(path).?);
|
||||
}
|
||||
@ -2495,7 +2502,7 @@ fn expandPaths(self: *Config, base: []const u8) !void {
|
||||
try @field(self, field.name).expand(
|
||||
arena_alloc,
|
||||
base,
|
||||
&self._errors,
|
||||
&self._diagnostics,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2503,11 +2510,13 @@ fn expandPaths(self: *Config, base: []const u8) !void {
|
||||
|
||||
fn loadTheme(self: *Config, theme: []const u8) !void {
|
||||
// Find our theme file and open it. See the open function for details.
|
||||
const file: std.fs.File = (try themepkg.open(
|
||||
const themefile = (try themepkg.open(
|
||||
self._arena.?.allocator(),
|
||||
theme,
|
||||
&self._errors,
|
||||
&self._diagnostics,
|
||||
)) orelse return;
|
||||
const path = themefile.path;
|
||||
const file = themefile.file;
|
||||
defer file.close();
|
||||
|
||||
// From this point onwards, we load the theme and do a bit of a dance
|
||||
@ -2533,7 +2542,9 @@ fn loadTheme(self: *Config, theme: []const u8) !void {
|
||||
|
||||
// Load our theme
|
||||
var buf_reader = std.io.bufferedReader(file.reader());
|
||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||
const reader = buf_reader.reader();
|
||||
const Iter = cli.args.LineIterator(@TypeOf(reader));
|
||||
var iter: Iter = .{ .r = reader, .filepath = path };
|
||||
try new_config.loadIter(alloc_gpa, &iter);
|
||||
|
||||
// Replay our previous inputs so that we can override values
|
||||
@ -2697,7 +2708,12 @@ pub fn finalize(self: *Config) !void {
|
||||
|
||||
/// Callback for src/cli/args.zig to allow us to handle special cases
|
||||
/// like `--help` or `-e`. Returns "false" if the CLI parsing should halt.
|
||||
pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: anytype) !bool {
|
||||
pub fn parseManuallyHook(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
arg: []const u8,
|
||||
iter: anytype,
|
||||
) !bool {
|
||||
// Keep track of our input args no matter what..
|
||||
try self._replay_steps.append(alloc, .{ .arg = try alloc.dupe(u8, arg) });
|
||||
|
||||
@ -2714,7 +2730,8 @@ pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter:
|
||||
}
|
||||
|
||||
if (command.items.len == 0) {
|
||||
try self._errors.add(alloc, .{
|
||||
try self._diagnostics.append(alloc, .{
|
||||
.location = cli.Location.fromIter(iter),
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
alloc,
|
||||
"missing command after {s}",
|
||||
@ -3506,7 +3523,7 @@ pub const RepeatablePath = struct {
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
base: []const u8,
|
||||
errors: *ErrorList,
|
||||
diags: *cli.DiagnosticList,
|
||||
) !void {
|
||||
assert(std.fs.path.isAbsolute(base));
|
||||
var dir = try std.fs.cwd().openDir(base, .{});
|
||||
@ -3533,7 +3550,7 @@ pub const RepeatablePath = struct {
|
||||
break :abs buf[0..resolved.len];
|
||||
}
|
||||
|
||||
try errors.add(alloc, .{
|
||||
try diags.append(alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
alloc,
|
||||
"error resolving file path {s}: {}",
|
||||
|
@ -4,7 +4,7 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const global_state = &@import("../global.zig").state;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const ErrorList = @import("ErrorList.zig");
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
/// Location of possible themes. The order of this enum matters because it
|
||||
/// defines the priority of theme search (from top to bottom).
|
||||
@ -107,19 +107,25 @@ pub const LocationIterator = struct {
|
||||
pub fn open(
|
||||
arena_alloc: Allocator,
|
||||
theme: []const u8,
|
||||
errors: *ErrorList,
|
||||
) error{OutOfMemory}!?std.fs.File {
|
||||
diags: *cli.DiagnosticList,
|
||||
) error{OutOfMemory}!?struct {
|
||||
path: []const u8,
|
||||
file: std.fs.File,
|
||||
} {
|
||||
|
||||
// Absolute themes are loaded a different path.
|
||||
if (std.fs.path.isAbsolute(theme)) return try openAbsolute(
|
||||
if (std.fs.path.isAbsolute(theme)) {
|
||||
const file: std.fs.File = try openAbsolute(
|
||||
arena_alloc,
|
||||
theme,
|
||||
errors,
|
||||
);
|
||||
diags,
|
||||
) orelse return null;
|
||||
return .{ .path = theme, .file = file };
|
||||
}
|
||||
|
||||
const basename = std.fs.path.basename(theme);
|
||||
if (!std.mem.eql(u8, theme, basename)) {
|
||||
try errors.add(arena_alloc, .{
|
||||
try diags.append(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"theme \"{s}\" cannot include path separators unless it is an absolute path",
|
||||
@ -135,15 +141,16 @@ pub fn open(
|
||||
const cwd = std.fs.cwd();
|
||||
while (try it.next()) |loc| {
|
||||
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
|
||||
if (cwd.openFile(path, .{})) |file| {
|
||||
return file;
|
||||
if (cwd.openFile(path, .{})) |file| return .{
|
||||
.path = path,
|
||||
.file = file,
|
||||
} else |err| switch (err) {
|
||||
// Not an error, just continue to the next location.
|
||||
error.FileNotFound => {},
|
||||
|
||||
// Anything else is an error we log and give up on.
|
||||
else => {
|
||||
try errors.add(arena_alloc, .{
|
||||
try diags.append(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"failed to load theme \"{s}\" from the file \"{s}\": {}",
|
||||
@ -163,7 +170,7 @@ pub fn open(
|
||||
it.reset();
|
||||
while (try it.next()) |loc| {
|
||||
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
|
||||
try errors.add(arena_alloc, .{
|
||||
try diags.append(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"theme \"{s}\" not found, tried path \"{s}\"",
|
||||
@ -186,18 +193,18 @@ pub fn open(
|
||||
pub fn openAbsolute(
|
||||
arena_alloc: Allocator,
|
||||
theme: []const u8,
|
||||
errors: *ErrorList,
|
||||
diags: *cli.DiagnosticList,
|
||||
) error{OutOfMemory}!?std.fs.File {
|
||||
return std.fs.openFileAbsolute(theme, .{}) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => try errors.add(arena_alloc, .{
|
||||
error.FileNotFound => try diags.append(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"failed to load theme from the path \"{s}\"",
|
||||
.{theme},
|
||||
),
|
||||
}),
|
||||
else => try errors.add(arena_alloc, .{
|
||||
else => try diags.append(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"failed to load theme from the path \"{s}\": {}",
|
||||
|
Reference in New Issue
Block a user