From a3fc96418fa689faf542ed4031f63a8729a33159 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 2 Dec 2024 20:53:15 +0100 Subject: [PATCH 1/9] feat: add odin support --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 2b7a7ef..fe4f4d6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,8 +11,8 @@ .hash = "122095b1a6110b920571c7e49e61c124cd9a164fe9b1b0faa1bd11d04d89822d3304", }, .syntax = .{ - .url = "https://github.com/neurocyte/flow-syntax/archive/2345f2f3b1def47bbc9b34f1404cd4bc9a02c05e.tar.gz", - .hash = "1220ea8e786a7f7960b4ea7af84e340f630de5ff145dc94aedde00951ae8f58111f4", + .url = "https://github.com/neurocyte/flow-syntax/archive/cba2de651f03e3677bed91c46d2c841eb4137dd4.tar.gz", + .hash = "12201f879b91fc86fd718947a45488e08445c765137857b29a0536a6e7245f7dc522", }, .thespian = .{ .url = "https://github.com/neurocyte/thespian/archive/d7dd27116398b17c8ab68327c384885f161d0cc1.tar.gz", From 50d5b969a92034e362bacf25a17c6128c508882a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 12 Dec 2024 18:13:49 +0100 Subject: [PATCH 2/9] feat: add mail file type support --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index fe4f4d6..c7c0e8f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,8 +11,8 @@ .hash = "122095b1a6110b920571c7e49e61c124cd9a164fe9b1b0faa1bd11d04d89822d3304", }, .syntax = .{ - .url = "https://github.com/neurocyte/flow-syntax/archive/cba2de651f03e3677bed91c46d2c841eb4137dd4.tar.gz", - .hash = "12201f879b91fc86fd718947a45488e08445c765137857b29a0536a6e7245f7dc522", + .url = "https://github.com/neurocyte/flow-syntax/archive/ba10d89670e19004bd3b958a407afc087c33ca9f.tar.gz", + .hash = "122054eecb1f4c9b669555225c8ae31b419f10231288004eec3247d6854f677a420b", }, .thespian = .{ .url = "https://github.com/neurocyte/thespian/archive/d7dd27116398b17c8ab68327c384885f161d0cc1.tar.gz", From ef0088a9235ad10fcd517cb41eda847a502be215 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 12 Feb 2025 12:02:23 +0100 Subject: [PATCH 3/9] feat: add sql and astro file types and mellow and zenbones themes --- build.zig.zon | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index c7c0e8f..ffeae3c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,12 +7,12 @@ .hash = "12207ee987ce045596cb992cfb15b0d6d9456e50d4721c3061c69dabc2962053644d", }, .themes = .{ - .url = "https://github.com/neurocyte/flow-themes/releases/download/master-5f1ca2fd3c784d430306a5cd1df237681a196333/flow-themes.tar.gz", - .hash = "122095b1a6110b920571c7e49e61c124cd9a164fe9b1b0faa1bd11d04d89822d3304", + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-618a7801d3383049adfe18cc09f5f5086c66995f/flow-themes.tar.gz", + .hash = "1220019ed92f48fb94d4ae82bba17b11d0ba06f17ed31cd66613b3c048b1d2382095", }, .syntax = .{ - .url = "https://github.com/neurocyte/flow-syntax/archive/ba10d89670e19004bd3b958a407afc087c33ca9f.tar.gz", - .hash = "122054eecb1f4c9b669555225c8ae31b419f10231288004eec3247d6854f677a420b", + .url = "https://github.com/neurocyte/flow-syntax/archive/28bc77f4615488aaa269c25fc862864f4b3a7460.tar.gz", + .hash = "1220abddc10ca8f8b6b5477f8c007948c168504b9dd3516899fe37251890eeabf4ab", }, .thespian = .{ .url = "https://github.com/neurocyte/thespian/archive/d7dd27116398b17c8ab68327c384885f161d0cc1.tar.gz", From a6358d5156af0902600e542337e3548e957fd8aa Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 18 Feb 2025 17:48:21 +0100 Subject: [PATCH 4/9] feat: add -C,--color option to enable color if stdout is not a tty --- src/main.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 5741878..6b354ff 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,6 +20,7 @@ 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. + \\-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. \\--list-languages Show available language parsers. @@ -68,7 +69,7 @@ pub fn main() !void { if (res.args.@"list-languages" != 0) return list_langs(writer); - if (!stdout_file.supportsAnsiEscapeCodes()) + if (res.args.color == 0 and !stdout_file.supportsAnsiEscapeCodes()) return plain_cat(res.positionals); var conf_buf: ?[]const u8 = null; From 66629f0f9deae14f4c2ddea6893e0be2ff886880 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 19 Feb 2025 10:09:51 +0100 Subject: [PATCH 5/9] feat: add --color to example fzf-grep script --- scripts/fzf-grep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fzf-grep b/scripts/fzf-grep index bc13fdc..a881787 100755 --- a/scripts/fzf-grep +++ b/scripts/fzf-grep @@ -15,4 +15,4 @@ rg \ --layout=reverse \ --ansi \ --tiebreak=index \ - --preview 'zat --highlight {2} --limit $FZF_PREVIEW_LINES {1}' + --preview 'zat --color --highlight {2} --limit $FZF_PREVIEW_LINES {1}' From 9b3f6153dac683d0b981bb35890bc96546051c32 Mon Sep 17 00:00:00 2001 From: Robert Kroeger Date: Tue, 22 Apr 2025 08:30:28 +0800 Subject: [PATCH 6/9] feat: build with Zig 0.14 Multiple changes needed to build with Zig 0.14: * upgraded ansi_term to version that also builds with 0.14 and made the necessary code changes here. * used the new separate cbor from flow-syntax and removed thespian * updated for 0.14 renaming of some std enum values * updated for a new 0.14 version of clap * update flow-themes --- build.zig | 12 +++++++----- build.zig.version | 2 +- build.zig.zon | 25 +++++++++++-------------- src/config_loader.zig | 2 +- src/main.zig | 31 ++++++++++++++++--------------- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/build.zig b/build.zig index 7a108e6..e3b6d80 100644 --- a/build.zig +++ b/build.zig @@ -86,12 +86,14 @@ pub fn build_exe( strip: bool, pie: ?bool, ) void { - const clap_dep = b.dependency("clap", .{ .target = target, .optimize = optimize }); - const ansi_term_dep = b.dependency("ansi-term", .{ .target = target, .optimize = optimize }); + const ansi_term_dep = b.dependency("ansi_term", .{ .target = target, .optimize = optimize }); const themes_dep = b.dependency("themes", .{}); const syntax_dep = b.dependency("syntax", .{ .target = target, .optimize = optimize }); - const thespian_dep = b.dependency("thespian", .{}); + const cbor_dep = syntax_dep.builder.dependency("cbor", .{ + .target = target, + .optimize = optimize, + }); const exe = b.addExecutable(.{ .name = "zat", @@ -105,8 +107,8 @@ pub fn build_exe( exe.root_module.addImport("theme", themes_dep.module("theme")); exe.root_module.addImport("themes", themes_dep.module("themes")); exe.root_module.addImport("clap", clap_dep.module("clap")); - exe.root_module.addImport("ansi-term", ansi_term_dep.module("ansi-term")); - exe.root_module.addImport("cbor", b.createModule(.{ .root_source_file = thespian_dep.path("src/cbor.zig") })); + exe.root_module.addImport("ansi_term", ansi_term_dep.module("ansi_term")); + exe.root_module.addImport("cbor", cbor_dep.module("cbor")); const exe_install = b.addInstallArtifact(exe, exe_install_options); b.getInstallStep().dependOn(&exe_install.step); diff --git a/build.zig.version b/build.zig.version index 54d1a4f..a803cc2 100644 --- a/build.zig.version +++ b/build.zig.version @@ -1 +1 @@ -0.13.0 +0.14.0 diff --git a/build.zig.zon b/build.zig.zon index ffeae3c..42c83f1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,26 +1,23 @@ .{ - .name = "zat", + .name = .zat, .version = "1.0.0", + .fingerprint = 0x8da9db57fa011a09, .dependencies = .{ .clap = .{ - .url = "https://github.com/Hejsil/zig-clap/archive/c0193e9247335a6c1688b946325060289405de2a.tar.gz", - .hash = "12207ee987ce045596cb992cfb15b0d6d9456e50d4721c3061c69dabc2962053644d", + .url = "https://github.com/Hejsil/zig-clap/archive/0.10.0.tar.gz", + .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", }, .themes = .{ - .url = "https://github.com/neurocyte/flow-themes/releases/download/master-618a7801d3383049adfe18cc09f5f5086c66995f/flow-themes.tar.gz", - .hash = "1220019ed92f48fb94d4ae82bba17b11d0ba06f17ed31cd66613b3c048b1d2382095", + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-ac2e3fe2df3419b71276f86fa9c45fd39d668f23/flow-themes.tar.gz", + .hash = "N-V-__8AAEtaFwAjAHCmWHRCrBxL7uSG4hQiIsSgS32Y67K6", }, .syntax = .{ - .url = "https://github.com/neurocyte/flow-syntax/archive/28bc77f4615488aaa269c25fc862864f4b3a7460.tar.gz", - .hash = "1220abddc10ca8f8b6b5477f8c007948c168504b9dd3516899fe37251890eeabf4ab", + .url = "https://github.com/neurocyte/flow-syntax/archive/fa6a411bc769882acc87cf0d961af3813abf2eac.tar.gz", + .hash = "flow_syntax-0.1.0-X8jOof39AADK25RT1Bst_x7aUIwHbh7y09PJXBghLu_b", }, - .thespian = .{ - .url = "https://github.com/neurocyte/thespian/archive/d7dd27116398b17c8ab68327c384885f161d0cc1.tar.gz", - .hash = "1220ace715c2ee9087fe375996b8e9180bfc722b2d1acdcbf00e8b507b31dd1cdd94", - }, - .@"ansi-term" = .{ - .url = "https://github.com/ziglibs/ansi-term/archive/0bb62115db6749044765fdb37c9791388e7970f2.tar.gz", - .hash = "12200719196e0abd325efa248fb03882c8fa2c7130e3ae1d57dbff72afc846b28495", + .ansi_term = .{ + .url = "https://github.com/ziglibs/ansi-term/archive/c0e6ad093d4f6a9ed4e65d962d1e53b97888f989.tar.gz", + .hash = "ansi_term-0.1.0-_baAywpoAABEqsPmS5Jz_CddDCrG8qdIyRIESH8D2fzd", }, }, .paths = .{ diff --git a/src/config_loader.zig b/src/config_loader.zig index 491970b..040e39b 100644 --- a/src/config_loader.zig +++ b/src/config_loader.zig @@ -31,7 +31,7 @@ fn read_json_config_file(a: std.mem.Allocator, file_name: []const u8, buf: *?[]c 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| { + 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)))) return error.InvalidConfig; diff --git a/src/main.zig b/src/main.zig index 6b354ff..9d4668a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,7 +3,7 @@ const clap = @import("clap"); const syntax = @import("syntax"); const Theme = @import("theme"); const themes = @import("themes"); -const term = @import("ansi-term"); +const term = @import("ansi_term"); const config_loader = @import("config_loader.zig"); const Writer = std.io.BufferedWriter(4096, std.fs.File.Writer).Writer; @@ -70,7 +70,7 @@ pub fn main() !void { return list_langs(writer); if (res.args.color == 0 and !stdout_file.supportsAnsiEscapeCodes()) - return plain_cat(res.positionals); + return plain_cat(res.positionals[0]); var conf_buf: ?[]const u8 = null; const conf = config_loader.read_config(a, &conf_buf); @@ -106,8 +106,8 @@ pub fn main() !void { if (res.args.html != 0) try write_html_preamble(writer, theme.editor); - if (res.positionals.len > 0) { - for (res.positionals) |arg| { + if (res.positionals[0].len > 0) { + for (res.positionals[0]) |arg| { const file = if (std.mem.eql(u8, arg, "-")) std.io.getStdIn() else @@ -158,11 +158,11 @@ pub fn main() !void { try write_html_postamble(writer); } -fn get_parser(a: std.mem.Allocator, content: []const u8, file_path: []const u8) *syntax { +fn get_parser(a: std.mem.Allocator, content: []const u8, file_path: []const u8, query_cache: *syntax.QueryCache) *syntax { return (if (lang_override) |name| - syntax.create_file_type(a, name) catch unknown_file_type(name) + syntax.create_file_type(a, name, query_cache) catch unknown_file_type(name) else - syntax.create_guess_file_type(a, content, file_path)) catch syntax.create_file_type(a, lang_default) catch unknown_file_type(lang_default); + syntax.create_guess_file_type(a, content, file_path, query_cache)) catch syntax.create_file_type(a, lang_default, query_cache) catch unknown_file_type(lang_default); } fn unknown_file_type(name: []const u8) noreturn { @@ -200,7 +200,8 @@ fn render_file( end_line = start_line + lines; } - const parser = get_parser(a, content, file_path); + 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) { try render_file_type(writer, parser.file_type, theme); @@ -399,16 +400,16 @@ fn list_themes(writer: Writer) !void { } fn set_ansi_style(writer: Writer, style: Theme.Style) Writer.Error!void { - const ansi_style = .{ + const ansi_style: term.style.Style = .{ .foreground = if (style.fg) |color| to_rgb_color(color.color) else .Default, .background = if (style.bg) |color| to_rgb_color(color.color) else .Default, .font_style = switch (style.fs orelse .normal) { .normal => term.style.FontStyle{}, - .bold => term.style.FontStyle.bold, - .italic => term.style.FontStyle.italic, - .underline => term.style.FontStyle.underline, - .undercurl => term.style.FontStyle.underline, - .strikethrough => term.style.FontStyle.crossedout, + .bold => term.style.FontStyle{ .bold = true }, + .italic => term.style.FontStyle{ .italic = true }, + .underline => term.style.FontStyle{ .underline = true }, + .undercurl => term.style.FontStyle{ .underline = true }, + .strikethrough => term.style.FontStyle{ .crossedout = true }, }, }; try term.format.updateStyle(writer, ansi_style, null); @@ -503,7 +504,7 @@ fn plain_cat_file(out_file: std.fs.File, in_file_name: []const u8) !void { try std.fs.cwd().openFile(in_file_name, .{}); defer in_file.close(); - var buf: [std.mem.page_size]u8 = undefined; + var buf: [std.heap.page_size_min]u8 = undefined; while (true) { const bytes_read = try in_file.read(&buf); if (bytes_read == 0) return; From 4afe32566957ed2c2970c3037ff7875361beb6af Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 25 Apr 2025 09:45:36 +0200 Subject: [PATCH 7/9] feat: update config loader to support current flow editor configuration files --- src/config.zig | 1 + src/config_loader.zig | 189 ++++++++++++++++++++++++++++++++---------- src/main.zig | 9 +- 3 files changed, 155 insertions(+), 44 deletions(-) diff --git a/src/config.zig b/src/config.zig index 46cbfe1..eef7cb8 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1 +1,2 @@ theme: []const u8 = "default", +include_files: []const u8 = "", diff --git a/src/config_loader.zig b/src/config_loader.zig index 040e39b..69f96e1 100644 --- a/src/config_loader.zig +++ b/src/config_loader.zig @@ -4,51 +4,153 @@ const builtin = @import("builtin"); const application_name = "flow"; -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 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 }; } -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, +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; }; + 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 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 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 cb = try cbor.fromJson(json, cbor_buf); var iter = cb; - 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)) { + _ = 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 value: field_info.type = undefined; - if (!(try cbor.matchValue(&iter, cbor.extract(&value)))) return error.InvalidConfig; - @field(data, field_info.name) = value; - found = true; - } + 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 (!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 { 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 local = struct { 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}); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return error.MakeHomeConfigDirFailed, }; break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname }); } 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 }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return error.MakeAppConfigDirFailed, }; break :ret dir; } else return error.AppConfigDirUnavailable; @@ -82,21 +184,24 @@ fn get_app_config_dir(appname: []const u8) ![]const u8 { local.config_dir = config_dir; std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) { 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; } -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 { var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined; - var config_file: ?[]const u8 = 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; + return std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name }); } diff --git a/src/main.zig b/src/main.zig index 9d4668a..32c819e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,6 +13,11 @@ 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. @@ -72,8 +77,8 @@ pub fn main() !void { if (res.args.color == 0 and !stdout_file.supportsAnsiEscapeCodes()) return plain_cat(res.positionals[0]); - var conf_buf: ?[]const u8 = null; - const conf = config_loader.read_config(a, &conf_buf); + const conf, const conf_bufs = config_loader.read_config(a); + defer config_loader.free_config(a, conf_bufs); const theme_name = if (res.args.theme) |theme| theme else conf.theme; const limit_lines = res.args.limit; From 8f66e855ed21fcfe4f15c33137cd9904ac2e153f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 25 Apr 2025 09:46:10 +0200 Subject: [PATCH 8/9] feat: add cli option to show used theme --- src/main.zig | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index 32c819e..efc82eb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -25,6 +25,7 @@ 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. @@ -77,7 +78,7 @@ 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(a); + const conf, const conf_bufs = config_loader.read_config(@import("config.zig"), a); defer config_loader.free_config(a, conf_bufs); const theme_name = if (res.args.theme) |theme| theme else conf.theme; const limit_lines = res.args.limit; @@ -127,6 +128,7 @@ pub fn main() !void { arg, &theme, res.args.@"show-language" != 0, + res.args.@"show-theme" != 0, set_style, unset_style, highlight_line_start, @@ -148,6 +150,7 @@ pub fn main() !void { "-", &theme, res.args.@"show-language" != 0, + res.args.@"show-theme" != 0, set_style, unset_style, highlight_line_start, @@ -183,7 +186,8 @@ fn render_file( content: []const u8, file_path: []const u8, theme: *const Theme, - show: bool, + show_file_type: bool, + show_theme: bool, set_style: StyleFn, unset_style: StyleFn, highlight_line_start: usize, @@ -208,10 +212,14 @@ 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) { + if (show_file_type) { 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), @@ -493,6 +501,20 @@ 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) { From e8dbff76a1c8d74d81e3e1109f65e6435bdc1643 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 25 Apr 2025 10:02:01 +0200 Subject: [PATCH 9/9] feat: add support for loading customized flow themes --- src/config_loader.zig | 61 +++++++++++++++++++++++++++++++++++++++++-- src/main.zig | 17 ++++-------- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/config_loader.zig b/src/config_loader.zig index 69f96e1..cb47ede 100644 --- a/src/config_loader.zig +++ b/src/config_loader.zig @@ -1,5 +1,7 @@ const std = @import("std"); const cbor = @import("cbor"); +const Theme = @import("theme"); +const themes = @import("themes"); const builtin = @import("builtin"); const application_name = "flow"; @@ -193,8 +195,6 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { return config_dir; } -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"); } @@ -205,3 +205,60 @@ fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: }; 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; +} diff --git a/src/main.zig b/src/main.zig index efc82eb..72bca06 100644 --- a/src/main.zig +++ b/src/main.zig @@ -94,14 +94,15 @@ pub fn main() !void { } if (highlight_line_end < highlight_line_start) { - try std.io.getStdErr().writer().print("invalid range\n", .{}); + std.log.err("invalid range", .{}); std.process.exit(1); } - const theme = get_theme_by_name(theme_name) orelse { - try std.io.getStdErr().writer().print("theme \"{s}\" not found\n", .{theme_name}); + const theme, const parsed_theme = config_loader.get_theme_by_name(a, theme_name) orelse { + std.log.err("theme \"{s}\" not found", .{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; @@ -174,7 +175,7 @@ fn get_parser(a: std.mem.Allocator, content: []const u8, file_path: []const u8, } fn unknown_file_type(name: []const u8) noreturn { - std.io.getStdErr().writer().print("unknown file type \'{s}\'\n", .{name}) catch {}; + std.log.err("unknown file type \'{s}\'\n", .{name}); std.process.exit(1); } @@ -391,14 +392,6 @@ 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|