diff --git a/src/config.zig b/src/config.zig index eef7cb8..46cbfe1 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,2 +1 @@ theme: []const u8 = "default", -include_files: []const u8 = "", diff --git a/src/config_loader.zig b/src/config_loader.zig index cb47ede..040e39b 100644 --- a/src/config_loader.zig +++ b/src/config_loader.zig @@ -1,158 +1,54 @@ const std = @import("std"); const cbor = @import("cbor"); -const Theme = @import("theme"); -const themes = @import("themes"); const builtin = @import("builtin"); const application_name = "flow"; -pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const u8 } { - 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]; - var conf: T = .{}; - if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) { - _ = read_config_file(T, allocator, &conf, &bufs, json_file_name); - } - read_nested_include_files(T, allocator, &conf, &bufs); - return .{ conf, bufs }; +const config = struct { + theme: []const u8 = "default", +}; + +pub fn read_config(a: std.mem.Allocator, buf: *?[]const u8) config { + const file_name = get_app_config_file_name(application_name) catch return .{}; + return read_json_config_file(a, file_name, buf) catch .{}; } -pub fn free_config(allocator: std.mem.Allocator, bufs: [][]const u8) void { - for (bufs) |buf| allocator.free(buf); -} - -// 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; +fn read_json_config_file(a: std.mem.Allocator, file_name: []const u8, buf: *?[]const u8) !config { + var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch |e| switch (e) { + error.FileNotFound => return .{}, + else => return 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(); - const text = try file.readToEndAlloc(allocator, 64 * 1024); - defer allocator.free(text); - var cbor_buf = std.ArrayList(u8).init(allocator); - 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 json = try file.readToEndAlloc(a, 64 * 1024); + defer a.free(json); + const cbor_buf: []u8 = try a.alloc(u8, json.len); + buf.* = cbor_buf; const cb = try cbor.fromJson(json, cbor_buf); var iter = cb; - _ = try cbor.decodeMapHeader(&iter); - return read_cbor_config(T, conf, file_name, iter); -} - -fn read_cbor_config( - T: type, - conf: *T, - 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 len = try cbor.decodeMapHeader(&iter); + var data: config = .{}; + while (len > 0) : (len -= 1) { + var found = false; + var field_name: []const u8 = undefined; + if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidConfig; + inline for (@typeInfo(config).@"struct".fields) |field_info| { + if (std.mem.eql(u8, field_name, field_info.name)) { var value: field_info.type = undefined; - if (try cbor.matchValue(&iter, cbor.extract(&value))) { - @field(conf, field_info.name) = value; - } 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 (!(try cbor.matchValue(&iter, cbor.extract(&value)))) return error.InvalidConfig; + @field(data, field_info.name) = value; + found = true; + } } + if (!found) try cbor.skipValue(&iter); } -} - -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}); - }; + return data; } pub fn get_config_dir() ![]const u8 { return get_app_config_dir(application_name); } -pub const ConfigDirError = error{ - NoSpaceLeft, - MakeConfigDirFailed, - MakeHomeConfigDirFailed, - MakeAppConfigDirFailed, - AppConfigDirUnavailable, -}; - -fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { +fn get_app_config_dir(appname: []const u8) ![]const u8 { const a = std.heap.c_allocator; const local = struct { var config_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; @@ -168,7 +64,7 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config", .{home}); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return error.MakeHomeConfigDirFailed, + else => return e, }; break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname }); } else if (builtin.os.tag == .windows) ret: { @@ -177,7 +73,7 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ appdata, appname }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return error.MakeAppConfigDirFailed, + else => return e, }; break :ret dir; } else return error.AppConfigDirUnavailable; @@ -186,79 +82,21 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { local.config_dir = config_dir; std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return error.MakeConfigDirFailed, + else => return e, }; - - 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; } -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 { +fn get_app_config_file_name(appname: []const u8) ![]const u8 { const local = struct { var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined; + var config_file: ?[]const u8 = null; }; - return std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name }); -} - -const theme_dir = "themes"; - -fn get_theme_directory() ![]const u8 { - const local = struct { - var dir_buffer: [std.posix.PATH_MAX]u8 = undefined; - }; - const a = std.heap.c_allocator; - if (std.process.getEnvVarOwned(a, "FLOW_THEMES_DIR") catch null) |dir| { - defer a.free(dir); - return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); - } - return try std.fmt.bufPrint(&local.dir_buffer, "{s}/{s}", .{ try get_app_config_dir(application_name), theme_dir }); -} - -pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { - const dir = try get_theme_directory(); - const local = struct { - var file_buffer: [std.posix.PATH_MAX]u8 = undefined; - }; - return try std.fmt.bufPrint(&local.file_buffer, "{s}/{s}.json", .{ dir, theme_name }); -} - -fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const u8 { - const file_name = get_theme_file_name(theme_name) catch return null; - var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; - defer file.close(); - return file.readToEndAlloc(allocator, 64 * 1024) catch null; -} - -fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { - return load_theme_file_internal(allocator, theme_name) catch |e| { - std.log.err("loaded theme from file failed: {}", .{e}); - return e; - }; -} - -fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { - _ = std.json.Scanner; - const json_str = read_theme(allocator, theme_name) orelse return null; - defer allocator.free(json_str); - return try std.json.parseFromSlice(Theme, allocator, json_str, .{ .allocate = .alloc_always }); -} - -pub fn get_theme_by_name(allocator: std.mem.Allocator, name: []const u8) ?struct { Theme, ?std.json.Parsed(Theme) } { - if (load_theme_file(allocator, name) catch null) |parsed_theme| { - std.log.info("loaded theme from file: {s}", .{name}); - return .{ parsed_theme.value, parsed_theme }; - } - - std.log.info("loading theme: {s}", .{name}); - for (themes.themes) |theme_| { - if (std.mem.eql(u8, theme_.name, name)) - return .{ theme_, null }; - } - return null; + const config_file_name = "config.json"; + 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; } diff --git a/src/main.zig b/src/main.zig index 72bca06..9d4668a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,11 +13,6 @@ var lang_override: ?[]const u8 = null; var lang_default: []const u8 = "conf"; 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 { const params = comptime clap.parseParamsComptime( \\-h, --help Display this help and exit. @@ -25,7 +20,6 @@ pub fn main() !void { \\-t, --theme Select theme to use. \\-d, --default Set the language to use if guessing failed (default: conf). \\-s, --show-language Show detected language in output. - \\-T, --show-theme Show selected theme in output. \\-C, --color Always produce color output, even if stdout is not a tty. \\--html Output HTML instead of ansi escape codes. \\--list-themes Show available themes. @@ -78,8 +72,8 @@ pub fn main() !void { if (res.args.color == 0 and !stdout_file.supportsAnsiEscapeCodes()) return plain_cat(res.positionals[0]); - const conf, const conf_bufs = config_loader.read_config(@import("config.zig"), a); - defer config_loader.free_config(a, conf_bufs); + var conf_buf: ?[]const u8 = null; + const conf = config_loader.read_config(a, &conf_buf); const theme_name = if (res.args.theme) |theme| theme else conf.theme; const limit_lines = res.args.limit; @@ -94,15 +88,14 @@ pub fn main() !void { } if (highlight_line_end < highlight_line_start) { - std.log.err("invalid range", .{}); + try std.io.getStdErr().writer().print("invalid range\n", .{}); std.process.exit(1); } - const theme, const parsed_theme = config_loader.get_theme_by_name(a, theme_name) orelse { - std.log.err("theme \"{s}\" not found", .{theme_name}); + const theme = get_theme_by_name(theme_name) orelse { + try std.io.getStdErr().writer().print("theme \"{s}\" not found\n", .{theme_name}); std.process.exit(1); }; - _ = parsed_theme; const set_style: StyleFn = if (res.args.html != 0) set_html_style else set_ansi_style; const unset_style: StyleFn = if (res.args.html != 0) unset_html_style else unset_ansi_style; @@ -129,7 +122,6 @@ pub fn main() !void { arg, &theme, res.args.@"show-language" != 0, - res.args.@"show-theme" != 0, set_style, unset_style, highlight_line_start, @@ -151,7 +143,6 @@ pub fn main() !void { "-", &theme, res.args.@"show-language" != 0, - res.args.@"show-theme" != 0, set_style, unset_style, highlight_line_start, @@ -175,7 +166,7 @@ fn get_parser(a: std.mem.Allocator, content: []const u8, file_path: []const u8, } fn unknown_file_type(name: []const u8) noreturn { - std.log.err("unknown file type \'{s}\'\n", .{name}); + std.io.getStdErr().writer().print("unknown file type \'{s}\'\n", .{name}) catch {}; std.process.exit(1); } @@ -187,8 +178,7 @@ fn render_file( content: []const u8, file_path: []const u8, theme: *const Theme, - show_file_type: bool, - show_theme: bool, + show: bool, set_style: StyleFn, unset_style: StyleFn, highlight_line_start: usize, @@ -213,14 +203,10 @@ fn render_file( const query_cache = try syntax.QueryCache.create(a, .{}); const parser = get_parser(a, content, file_path, query_cache); try parser.refresh_full(content); - if (show_file_type) { + if (show) { try render_file_type(writer, parser.file_type, theme); end_line -= 1; } - if (show_theme) { - try render_theme_indicator(writer, theme); - end_line -= 1; - } const Ctx = struct { writer: @TypeOf(writer), @@ -392,6 +378,14 @@ pub const fallbacks: []const FallBack = &[_]FallBack{ .{ .ts = "field", .tm = "variable" }, }; +fn get_theme_by_name(name: []const u8) ?Theme { + for (themes.themes) |theme| { + if (std.mem.eql(u8, theme.name, name)) + return theme; + } + return null; +} + fn list_themes(writer: Writer) !void { var max_name_len: usize = 0; for (themes.themes) |theme| @@ -494,20 +488,6 @@ fn render_file_type(writer: Writer, file_type: *const syntax.FileType, theme: *c try writer.writeAll("\n"); } -fn render_theme_indicator(writer: Writer, theme: *const Theme) !void { - const style = Theme.Style{ .bg = theme.editor_selection.bg, .fg = theme.editor.fg }; - const reversed = Theme.Style{ .fg = theme.editor_selection.bg }; - const plain: Theme.Style = Theme.Style{ .fg = theme.editor.fg }; - try set_ansi_style(writer, reversed); - try writer.writeAll(""); - try set_ansi_style(writer, style); - try writer.writeAll(theme.name); - try set_ansi_style(writer, reversed); - try writer.writeAll(""); - try set_ansi_style(writer, plain); - try writer.writeAll("\n"); -} - fn plain_cat(files: []const []const u8) !void { const stdout = std.io.getStdOut(); if (files.len == 0) {