feat: update config loader to support current flow editor configuration files
This commit is contained in:
parent
9b3f6153da
commit
4afe325669
3 changed files with 155 additions and 44 deletions
|
@ -1 +1,2 @@
|
||||||
theme: []const u8 = "default",
|
theme: []const u8 = "default",
|
||||||
|
include_files: []const u8 = "",
|
||||||
|
|
|
@ -4,51 +4,153 @@ const builtin = @import("builtin");
|
||||||
|
|
||||||
const application_name = "flow";
|
const application_name = "flow";
|
||||||
|
|
||||||
const config = struct {
|
pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const u8 } {
|
||||||
theme: []const u8 = "default",
|
var bufs: [][]const u8 = &[_][]const u8{};
|
||||||
};
|
const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ .{}, bufs };
|
||||||
|
const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len];
|
||||||
pub fn read_config(a: std.mem.Allocator, buf: *?[]const u8) config {
|
var conf: T = .{};
|
||||||
const file_name = get_app_config_file_name(application_name) catch return .{};
|
if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) {
|
||||||
return read_json_config_file(a, file_name, buf) catch .{};
|
_ = read_config_file(T, allocator, &conf, &bufs, json_file_name);
|
||||||
|
}
|
||||||
|
read_nested_include_files(T, allocator, &conf, &bufs);
|
||||||
|
return .{ conf, bufs };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_json_config_file(a: std.mem.Allocator, file_name: []const u8, buf: *?[]const u8) !config {
|
pub fn free_config(allocator: std.mem.Allocator, bufs: [][]const u8) void {
|
||||||
var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch |e| switch (e) {
|
for (bufs) |buf| allocator.free(buf);
|
||||||
error.FileNotFound => return .{},
|
}
|
||||||
else => return e,
|
|
||||||
|
// returns true if the file was found
|
||||||
|
fn read_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8, file_name: []const u8) bool {
|
||||||
|
std.log.info("loading {s}", .{file_name});
|
||||||
|
const err: anyerror = blk: {
|
||||||
|
if (std.mem.endsWith(u8, file_name, ".json")) if (read_json_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e;
|
||||||
|
if (read_text_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e;
|
||||||
};
|
};
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound => return false,
|
||||||
|
else => |e| std.log.err("error reading config file '{s}': {s}", .{ file_name, @errorName(e) }),
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void {
|
||||||
|
var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only });
|
||||||
defer file.close();
|
defer file.close();
|
||||||
const json = try file.readToEndAlloc(a, 64 * 1024);
|
const text = try file.readToEndAlloc(allocator, 64 * 1024);
|
||||||
defer a.free(json);
|
defer allocator.free(text);
|
||||||
const cbor_buf: []u8 = try a.alloc(u8, json.len);
|
var cbor_buf = std.ArrayList(u8).init(allocator);
|
||||||
buf.* = cbor_buf;
|
defer cbor_buf.deinit();
|
||||||
|
const writer = cbor_buf.writer();
|
||||||
|
var it = std.mem.splitScalar(u8, text, '\n');
|
||||||
|
var lineno: u32 = 0;
|
||||||
|
while (it.next()) |line| {
|
||||||
|
lineno += 1;
|
||||||
|
if (line.len == 0 or line[0] == '#')
|
||||||
|
continue;
|
||||||
|
const sep = std.mem.indexOfScalar(u8, line, ' ') orelse {
|
||||||
|
std.log.err("{s}:{}: {s} missing value", .{ file_name, lineno, line });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
const name = line[0..sep];
|
||||||
|
const value_str = line[sep + 1 ..];
|
||||||
|
const cb = cbor.fromJsonAlloc(allocator, value_str) catch {
|
||||||
|
std.log.err("{s}:{}: {s} has bad value: {s}", .{ file_name, lineno, name, value_str });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
defer allocator.free(cb);
|
||||||
|
try cbor.writeValue(writer, name);
|
||||||
|
try cbor_buf.appendSlice(cb);
|
||||||
|
}
|
||||||
|
const cb = try cbor_buf.toOwnedSlice();
|
||||||
|
var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*);
|
||||||
|
bufs.append(allocator, cb) catch @panic("OOM:read_text_config_file");
|
||||||
|
bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_text_config_file");
|
||||||
|
return read_cbor_config(T, conf, file_name, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_json_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void {
|
||||||
|
var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only });
|
||||||
|
defer file.close();
|
||||||
|
const json = try file.readToEndAlloc(allocator, 64 * 1024);
|
||||||
|
defer allocator.free(json);
|
||||||
|
const cbor_buf: []u8 = try allocator.alloc(u8, json.len);
|
||||||
|
var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*);
|
||||||
|
bufs.append(allocator, cbor_buf) catch @panic("OOM:read_json_config_file");
|
||||||
|
bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_json_config_file");
|
||||||
const cb = try cbor.fromJson(json, cbor_buf);
|
const cb = try cbor.fromJson(json, cbor_buf);
|
||||||
var iter = cb;
|
var iter = cb;
|
||||||
var len = try cbor.decodeMapHeader(&iter);
|
_ = try cbor.decodeMapHeader(&iter);
|
||||||
var data: config = .{};
|
return read_cbor_config(T, conf, file_name, iter);
|
||||||
while (len > 0) : (len -= 1) {
|
}
|
||||||
var found = false;
|
|
||||||
var field_name: []const u8 = undefined;
|
fn read_cbor_config(
|
||||||
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidConfig;
|
T: type,
|
||||||
inline for (@typeInfo(config).@"struct".fields) |field_info| {
|
conf: *T,
|
||||||
if (std.mem.eql(u8, field_name, field_info.name)) {
|
file_name: []const u8,
|
||||||
|
cb: []const u8,
|
||||||
|
) !void {
|
||||||
|
var iter = cb;
|
||||||
|
var field_name: []const u8 = undefined;
|
||||||
|
while (cbor.matchString(&iter, &field_name) catch |e| switch (e) {
|
||||||
|
error.TooShort => return,
|
||||||
|
else => return e,
|
||||||
|
}) {
|
||||||
|
var known = false;
|
||||||
|
inline for (@typeInfo(T).@"struct".fields) |field_info|
|
||||||
|
if (comptime std.mem.eql(u8, "include_files", field_info.name)) {
|
||||||
|
if (std.mem.eql(u8, field_name, field_info.name)) {
|
||||||
|
known = true;
|
||||||
|
var value: field_info.type = undefined;
|
||||||
|
if (try cbor.matchValue(&iter, cbor.extract(&value))) {
|
||||||
|
if (conf.include_files.len > 0) {
|
||||||
|
std.log.warn("{s}: ignoring nested 'include_files' value '{s}'", .{ file_name, value });
|
||||||
|
} else {
|
||||||
|
@field(conf, field_info.name) = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try cbor.skipValue(&iter);
|
||||||
|
std.log.err("invalid value for key '{s}'", .{field_name});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (std.mem.eql(u8, field_name, field_info.name)) {
|
||||||
|
known = true;
|
||||||
var value: field_info.type = undefined;
|
var value: field_info.type = undefined;
|
||||||
if (!(try cbor.matchValue(&iter, cbor.extract(&value)))) return error.InvalidConfig;
|
if (try cbor.matchValue(&iter, cbor.extract(&value))) {
|
||||||
@field(data, field_info.name) = value;
|
@field(conf, field_info.name) = value;
|
||||||
found = true;
|
} else {
|
||||||
}
|
try cbor.skipValue(&iter);
|
||||||
|
std.log.err("invalid value for key '{s}'", .{field_name});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!known) {
|
||||||
|
try cbor.skipValue(&iter);
|
||||||
|
std.log.warn("unknown config value '{s}' ignored", .{field_name});
|
||||||
}
|
}
|
||||||
if (!found) try cbor.skipValue(&iter);
|
|
||||||
}
|
}
|
||||||
return data;
|
}
|
||||||
|
|
||||||
|
fn read_nested_include_files(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8) void {
|
||||||
|
if (conf.include_files.len == 0) return;
|
||||||
|
var it = std.mem.splitScalar(u8, conf.include_files, std.fs.path.delimiter);
|
||||||
|
while (it.next()) |path| if (!read_config_file(T, allocator, conf, bufs, path)) {
|
||||||
|
std.log.warn("config include file '{s}' is not found", .{path});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_dir() ![]const u8 {
|
pub fn get_config_dir() ![]const u8 {
|
||||||
return get_app_config_dir(application_name);
|
return get_app_config_dir(application_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_app_config_dir(appname: []const u8) ![]const u8 {
|
pub const ConfigDirError = error{
|
||||||
|
NoSpaceLeft,
|
||||||
|
MakeConfigDirFailed,
|
||||||
|
MakeHomeConfigDirFailed,
|
||||||
|
MakeAppConfigDirFailed,
|
||||||
|
AppConfigDirUnavailable,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 {
|
||||||
const a = std.heap.c_allocator;
|
const a = std.heap.c_allocator;
|
||||||
const local = struct {
|
const local = struct {
|
||||||
var config_dir_buffer: [std.posix.PATH_MAX]u8 = undefined;
|
var config_dir_buffer: [std.posix.PATH_MAX]u8 = undefined;
|
||||||
|
@ -64,7 +166,7 @@ fn get_app_config_dir(appname: []const u8) ![]const u8 {
|
||||||
const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config", .{home});
|
const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config", .{home});
|
||||||
std.fs.makeDirAbsolute(dir) catch |e| switch (e) {
|
std.fs.makeDirAbsolute(dir) catch |e| switch (e) {
|
||||||
error.PathAlreadyExists => {},
|
error.PathAlreadyExists => {},
|
||||||
else => return e,
|
else => return error.MakeHomeConfigDirFailed,
|
||||||
};
|
};
|
||||||
break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname });
|
break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname });
|
||||||
} else if (builtin.os.tag == .windows) ret: {
|
} else if (builtin.os.tag == .windows) ret: {
|
||||||
|
@ -73,7 +175,7 @@ fn get_app_config_dir(appname: []const u8) ![]const u8 {
|
||||||
const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ appdata, appname });
|
const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ appdata, appname });
|
||||||
std.fs.makeDirAbsolute(dir) catch |e| switch (e) {
|
std.fs.makeDirAbsolute(dir) catch |e| switch (e) {
|
||||||
error.PathAlreadyExists => {},
|
error.PathAlreadyExists => {},
|
||||||
else => return e,
|
else => return error.MakeAppConfigDirFailed,
|
||||||
};
|
};
|
||||||
break :ret dir;
|
break :ret dir;
|
||||||
} else return error.AppConfigDirUnavailable;
|
} else return error.AppConfigDirUnavailable;
|
||||||
|
@ -82,21 +184,24 @@ fn get_app_config_dir(appname: []const u8) ![]const u8 {
|
||||||
local.config_dir = config_dir;
|
local.config_dir = config_dir;
|
||||||
std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) {
|
std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) {
|
||||||
error.PathAlreadyExists => {},
|
error.PathAlreadyExists => {},
|
||||||
else => return e,
|
else => return error.MakeConfigDirFailed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var theme_dir_buffer: [std.posix.PATH_MAX]u8 = undefined;
|
||||||
|
std.fs.makeDirAbsolute(try std.fmt.bufPrint(&theme_dir_buffer, "{s}/{s}", .{ config_dir, theme_dir })) catch {};
|
||||||
|
|
||||||
return config_dir;
|
return config_dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_app_config_file_name(appname: []const u8) ![]const u8 {
|
const theme_dir = "themes";
|
||||||
|
|
||||||
|
fn get_app_config_file_name(appname: []const u8, comptime base_name: []const u8) ConfigDirError![]const u8 {
|
||||||
|
return get_app_config_dir_file_name(appname, base_name ++ ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: []const u8) ConfigDirError![]const u8 {
|
||||||
const local = struct {
|
const local = struct {
|
||||||
var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined;
|
var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined;
|
||||||
var config_file: ?[]const u8 = null;
|
|
||||||
};
|
};
|
||||||
const config_file_name = "config.json";
|
return std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name });
|
||||||
const config_file = if (local.config_file) |file|
|
|
||||||
file
|
|
||||||
else
|
|
||||||
try std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name });
|
|
||||||
local.config_file = config_file;
|
|
||||||
return config_file;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,11 @@ var lang_override: ?[]const u8 = null;
|
||||||
var lang_default: []const u8 = "conf";
|
var lang_default: []const u8 = "conf";
|
||||||
const no_highlight = std.math.maxInt(usize);
|
const no_highlight = std.math.maxInt(usize);
|
||||||
|
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
pub const std_options: std.Options = .{
|
||||||
|
.log_level = if (builtin.mode == .Debug) .info else .err,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
const params = comptime clap.parseParamsComptime(
|
const params = comptime clap.parseParamsComptime(
|
||||||
\\-h, --help Display this help and exit.
|
\\-h, --help Display this help and exit.
|
||||||
|
@ -72,8 +77,8 @@ pub fn main() !void {
|
||||||
if (res.args.color == 0 and !stdout_file.supportsAnsiEscapeCodes())
|
if (res.args.color == 0 and !stdout_file.supportsAnsiEscapeCodes())
|
||||||
return plain_cat(res.positionals[0]);
|
return plain_cat(res.positionals[0]);
|
||||||
|
|
||||||
var conf_buf: ?[]const u8 = null;
|
const conf, const conf_bufs = config_loader.read_config(a);
|
||||||
const conf = config_loader.read_config(a, &conf_buf);
|
defer config_loader.free_config(a, conf_bufs);
|
||||||
const theme_name = if (res.args.theme) |theme| theme else conf.theme;
|
const theme_name = if (res.args.theme) |theme| theme else conf.theme;
|
||||||
const limit_lines = res.args.limit;
|
const limit_lines = res.args.limit;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue