├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.version ├── build.zig.zon ├── scripts ├── fzf-grep └── make_release ├── src ├── config.zig ├── config_loader.zig └── main.zig └── zig /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache/ 2 | /.zig-cache/ 3 | /zig-out/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CJ van den Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zat 2 | zat is a syntax highlighting cat like utility. 3 | 4 | It uses tree-sitter and supports for vscode themes. 5 | 6 | Build with the provided zig wrapper: 7 | ```shell 8 | ./zig build -Doptimize=ReleaseSmall 9 | ``` 10 | 11 | The zig wrapper just fetches a known good version of zig nightly and places it 12 | in the .cache directory. Or use your own version of zig. 13 | 14 | Run with: 15 | ```shell 16 | zig-out/bin/zat 17 | ``` 18 | 19 | Place it in your path for convenient access. 20 | 21 | 22 | Supply files to highlight on the command line. Multiple files will be appended 23 | like with cat. If no files are on the command line zat will read from stdin. 24 | Override the language with --language and select a different theme with --theme. 25 | The default theme will be read from ~/.config/flow/config.json if found. 26 | 27 | See `scripts/fzf-grep` for an example of using zat to highlight fzf previews. 28 | 29 | See --help for full command line. 30 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const release = b.option(bool, "package_release", "Build all release targets") orelse false; 5 | const strip = b.option(bool, "strip", "Disable debug information (default: no)"); 6 | const pie = b.option(bool, "pie", "Produce an executable with position independent code (default: none)"); 7 | 8 | const run_step = b.step("run", "Run the app"); 9 | 10 | return (if (release) &build_release else &build_development)( 11 | b, 12 | run_step, 13 | strip, 14 | pie, 15 | ); 16 | } 17 | 18 | fn build_development( 19 | b: *std.Build, 20 | run_step: *std.Build.Step, 21 | strip: ?bool, 22 | pie: ?bool, 23 | ) void { 24 | const target = b.standardTargetOptions(.{}); 25 | const optimize = b.standardOptimizeOption(.{}); 26 | 27 | return build_exe( 28 | b, 29 | run_step, 30 | target, 31 | optimize, 32 | .{}, 33 | strip orelse false, 34 | pie, 35 | ); 36 | } 37 | 38 | fn build_release( 39 | b: *std.Build, 40 | run_step: *std.Build.Step, 41 | strip: ?bool, 42 | pie: ?bool, 43 | ) void { 44 | const targets: []const std.Target.Query = &.{ 45 | .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl }, 46 | .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl }, 47 | .{ .cpu_arch = .x86_64, .os_tag = .macos }, 48 | .{ .cpu_arch = .aarch64, .os_tag = .macos }, 49 | .{ .cpu_arch = .x86_64, .os_tag = .windows }, 50 | .{ .cpu_arch = .aarch64, .os_tag = .windows }, 51 | }; 52 | const optimize = .ReleaseFast; 53 | 54 | var version = std.ArrayList(u8).init(b.allocator); 55 | defer version.deinit(); 56 | gen_version(b, version.writer()) catch unreachable; 57 | const write_file_step = b.addWriteFiles(); 58 | const version_file = write_file_step.add("version", version.items); 59 | b.getInstallStep().dependOn(&b.addInstallFile(version_file, "version").step); 60 | 61 | for (targets) |t| { 62 | const target = b.resolveTargetQuery(t); 63 | var triple = std.mem.splitScalar(u8, t.zigTriple(b.allocator) catch unreachable, '-'); 64 | const arch = triple.next() orelse unreachable; 65 | const os = triple.next() orelse unreachable; 66 | const target_path = std.mem.join(b.allocator, "-", &[_][]const u8{ os, arch }) catch unreachable; 67 | 68 | build_exe( 69 | b, 70 | run_step, 71 | target, 72 | optimize, 73 | .{ .dest_dir = .{ .override = .{ .custom = target_path } } }, 74 | strip orelse true, 75 | pie, 76 | ); 77 | } 78 | } 79 | 80 | pub fn build_exe( 81 | b: *std.Build, 82 | run_step: *std.Build.Step, 83 | target: std.Build.ResolvedTarget, 84 | optimize: std.builtin.OptimizeMode, 85 | exe_install_options: std.Build.Step.InstallArtifact.Options, 86 | strip: bool, 87 | pie: ?bool, 88 | ) void { 89 | const clap_dep = b.dependency("clap", .{ .target = target, .optimize = optimize }); 90 | const ansi_term_dep = b.dependency("ansi_term", .{ .target = target, .optimize = optimize }); 91 | const themes_dep = b.dependency("themes", .{}); 92 | const syntax_dep = b.dependency("syntax", .{ .target = target, .optimize = optimize }); 93 | const cbor_dep = syntax_dep.builder.dependency("cbor", .{ 94 | .target = target, 95 | .optimize = optimize, 96 | }); 97 | 98 | const exe = b.addExecutable(.{ 99 | .name = "zat", 100 | .root_source_file = b.path("src/main.zig"), 101 | .target = target, 102 | .optimize = optimize, 103 | .strip = strip, 104 | }); 105 | if (pie) |value| exe.pie = value; 106 | exe.root_module.addImport("syntax", syntax_dep.module("syntax")); 107 | exe.root_module.addImport("theme", themes_dep.module("theme")); 108 | exe.root_module.addImport("themes", themes_dep.module("themes")); 109 | exe.root_module.addImport("clap", clap_dep.module("clap")); 110 | exe.root_module.addImport("ansi_term", ansi_term_dep.module("ansi_term")); 111 | exe.root_module.addImport("cbor", cbor_dep.module("cbor")); 112 | const exe_install = b.addInstallArtifact(exe, exe_install_options); 113 | b.getInstallStep().dependOn(&exe_install.step); 114 | 115 | const run_cmd = b.addRunArtifact(exe); 116 | run_cmd.step.dependOn(b.getInstallStep()); 117 | if (b.args) |args| run_cmd.addArgs(args); 118 | run_step.dependOn(&run_cmd.step); 119 | } 120 | 121 | fn gen_version(b: *std.Build, writer: anytype) !void { 122 | var code: u8 = 0; 123 | 124 | const describe = try b.runAllowFail(&[_][]const u8{ "git", "describe", "--always", "--tags" }, &code, .Ignore); 125 | const diff_ = try b.runAllowFail(&[_][]const u8{ "git", "diff", "--stat", "--patch", "HEAD" }, &code, .Ignore); 126 | const diff = std.mem.trimRight(u8, diff_, "\r\n "); 127 | const version = std.mem.trimRight(u8, describe, "\r\n "); 128 | 129 | try writer.print("{s}{s}", .{ version, if (diff.len > 0) "-dirty" else "" }); 130 | } 131 | -------------------------------------------------------------------------------- /build.zig.version: -------------------------------------------------------------------------------- 1 | 0.14.0 2 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zat, 3 | .version = "1.0.0", 4 | .fingerprint = 0x8da9db57fa011a09, 5 | .dependencies = .{ 6 | .clap = .{ 7 | .url = "https://github.com/Hejsil/zig-clap/archive/0.10.0.tar.gz", 8 | .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", 9 | }, 10 | .themes = .{ 11 | .url = "https://github.com/neurocyte/flow-themes/releases/download/master-ac2e3fe2df3419b71276f86fa9c45fd39d668f23/flow-themes.tar.gz", 12 | .hash = "N-V-__8AAEtaFwAjAHCmWHRCrBxL7uSG4hQiIsSgS32Y67K6", 13 | }, 14 | .syntax = .{ 15 | .url = "https://github.com/neurocyte/flow-syntax/archive/fa6a411bc769882acc87cf0d961af3813abf2eac.tar.gz", 16 | .hash = "flow_syntax-0.1.0-X8jOof39AADK25RT1Bst_x7aUIwHbh7y09PJXBghLu_b", 17 | }, 18 | .ansi_term = .{ 19 | .url = "https://github.com/ziglibs/ansi-term/archive/c0e6ad093d4f6a9ed4e65d962d1e53b97888f989.tar.gz", 20 | .hash = "ansi_term-0.1.0-_baAywpoAABEqsPmS5Jz_CddDCrG8qdIyRIESH8D2fzd", 21 | }, 22 | }, 23 | .paths = .{ 24 | "build.zig", 25 | "build.zig.zon", 26 | "src", 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /scripts/fzf-grep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eEuCo pipefail 4 | 5 | rg \ 6 | --no-heading \ 7 | --color always \ 8 | -n \ 9 | "$@" \ 10 | | fzf \ 11 | --marker=\* \ 12 | --delimiter=: \ 13 | --border \ 14 | --cycle \ 15 | --layout=reverse \ 16 | --ansi \ 17 | --tiebreak=index \ 18 | --preview 'zat --color --highlight {2} --limit $FZF_PREVIEW_LINES {1}' 19 | -------------------------------------------------------------------------------- /scripts/make_release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | BASEDIR="$(cd "$(dirname "$0")/.." && pwd)" 5 | APPNAME="$(basename "$BASEDIR")" 6 | 7 | cd "$BASEDIR" 8 | 9 | if [ -e "release" ]; then 10 | echo directory \"release\" already exists 11 | exit 1 12 | fi 13 | 14 | echo building... 15 | 16 | ./zig build -Dpackage_release --prefix release/build 17 | 18 | cd release/build 19 | 20 | VERSION=$(/bin/cat version) 21 | TARGETS=$(/bin/ls) 22 | 23 | for target in $TARGETS; do 24 | if [ -d "$target" ]; then 25 | cd "$target" 26 | echo packing "$target"... 27 | tar -czf "../../${APPNAME}-${VERSION}-${target}.tar.gz" -- * 28 | cd .. 29 | fi 30 | done 31 | 32 | cd .. 33 | rm -r build 34 | 35 | TARFILES=$(/bin/ls) 36 | 37 | for tarfile in $TARFILES; do 38 | echo signing "$tarfile"... 39 | gpg --local-user 4E6CF7234FFC4E14531074F98EB1E1BB660E3FB9 --detach-sig "$tarfile" 40 | done 41 | 42 | echo "done making release $VERSION" 43 | echo 44 | 45 | /bin/ls -lah 46 | -------------------------------------------------------------------------------- /src/config.zig: -------------------------------------------------------------------------------- 1 | theme: []const u8 = "default", 2 | include_files: []const u8 = "", 3 | -------------------------------------------------------------------------------- /src/config_loader.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const Theme = @import("theme"); 4 | const themes = @import("themes"); 5 | const builtin = @import("builtin"); 6 | 7 | const application_name = "flow"; 8 | 9 | pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const u8 } { 10 | var bufs: [][]const u8 = &[_][]const u8{}; 11 | const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ .{}, bufs }; 12 | const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; 13 | var conf: T = .{}; 14 | if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) { 15 | _ = read_config_file(T, allocator, &conf, &bufs, json_file_name); 16 | } 17 | read_nested_include_files(T, allocator, &conf, &bufs); 18 | return .{ conf, bufs }; 19 | } 20 | 21 | pub fn free_config(allocator: std.mem.Allocator, bufs: [][]const u8) void { 22 | for (bufs) |buf| allocator.free(buf); 23 | } 24 | 25 | // returns true if the file was found 26 | fn read_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8, file_name: []const u8) bool { 27 | std.log.info("loading {s}", .{file_name}); 28 | const err: anyerror = blk: { 29 | 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; 30 | if (read_text_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e; 31 | }; 32 | switch (err) { 33 | error.FileNotFound => return false, 34 | else => |e| std.log.err("error reading config file '{s}': {s}", .{ file_name, @errorName(e) }), 35 | } 36 | return true; 37 | } 38 | 39 | fn read_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { 40 | var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); 41 | defer file.close(); 42 | const text = try file.readToEndAlloc(allocator, 64 * 1024); 43 | defer allocator.free(text); 44 | var cbor_buf = std.ArrayList(u8).init(allocator); 45 | defer cbor_buf.deinit(); 46 | const writer = cbor_buf.writer(); 47 | var it = std.mem.splitScalar(u8, text, '\n'); 48 | var lineno: u32 = 0; 49 | while (it.next()) |line| { 50 | lineno += 1; 51 | if (line.len == 0 or line[0] == '#') 52 | continue; 53 | const sep = std.mem.indexOfScalar(u8, line, ' ') orelse { 54 | std.log.err("{s}:{}: {s} missing value", .{ file_name, lineno, line }); 55 | continue; 56 | }; 57 | const name = line[0..sep]; 58 | const value_str = line[sep + 1 ..]; 59 | const cb = cbor.fromJsonAlloc(allocator, value_str) catch { 60 | std.log.err("{s}:{}: {s} has bad value: {s}", .{ file_name, lineno, name, value_str }); 61 | continue; 62 | }; 63 | defer allocator.free(cb); 64 | try cbor.writeValue(writer, name); 65 | try cbor_buf.appendSlice(cb); 66 | } 67 | const cb = try cbor_buf.toOwnedSlice(); 68 | var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*); 69 | bufs.append(allocator, cb) catch @panic("OOM:read_text_config_file"); 70 | bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_text_config_file"); 71 | return read_cbor_config(T, conf, file_name, cb); 72 | } 73 | 74 | fn read_json_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { 75 | var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); 76 | defer file.close(); 77 | const json = try file.readToEndAlloc(allocator, 64 * 1024); 78 | defer allocator.free(json); 79 | const cbor_buf: []u8 = try allocator.alloc(u8, json.len); 80 | var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*); 81 | bufs.append(allocator, cbor_buf) catch @panic("OOM:read_json_config_file"); 82 | bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_json_config_file"); 83 | const cb = try cbor.fromJson(json, cbor_buf); 84 | var iter = cb; 85 | _ = try cbor.decodeMapHeader(&iter); 86 | return read_cbor_config(T, conf, file_name, iter); 87 | } 88 | 89 | fn read_cbor_config( 90 | T: type, 91 | conf: *T, 92 | file_name: []const u8, 93 | cb: []const u8, 94 | ) !void { 95 | var iter = cb; 96 | var field_name: []const u8 = undefined; 97 | while (cbor.matchString(&iter, &field_name) catch |e| switch (e) { 98 | error.TooShort => return, 99 | else => return e, 100 | }) { 101 | var known = false; 102 | inline for (@typeInfo(T).@"struct".fields) |field_info| 103 | if (comptime std.mem.eql(u8, "include_files", field_info.name)) { 104 | if (std.mem.eql(u8, field_name, field_info.name)) { 105 | known = true; 106 | var value: field_info.type = undefined; 107 | if (try cbor.matchValue(&iter, cbor.extract(&value))) { 108 | if (conf.include_files.len > 0) { 109 | std.log.warn("{s}: ignoring nested 'include_files' value '{s}'", .{ file_name, value }); 110 | } else { 111 | @field(conf, field_info.name) = value; 112 | } 113 | } else { 114 | try cbor.skipValue(&iter); 115 | std.log.err("invalid value for key '{s}'", .{field_name}); 116 | } 117 | } 118 | } else if (std.mem.eql(u8, field_name, field_info.name)) { 119 | known = true; 120 | var value: field_info.type = undefined; 121 | if (try cbor.matchValue(&iter, cbor.extract(&value))) { 122 | @field(conf, field_info.name) = value; 123 | } else { 124 | try cbor.skipValue(&iter); 125 | std.log.err("invalid value for key '{s}'", .{field_name}); 126 | } 127 | }; 128 | if (!known) { 129 | try cbor.skipValue(&iter); 130 | std.log.warn("unknown config value '{s}' ignored", .{field_name}); 131 | } 132 | } 133 | } 134 | 135 | fn read_nested_include_files(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8) void { 136 | if (conf.include_files.len == 0) return; 137 | var it = std.mem.splitScalar(u8, conf.include_files, std.fs.path.delimiter); 138 | while (it.next()) |path| if (!read_config_file(T, allocator, conf, bufs, path)) { 139 | std.log.warn("config include file '{s}' is not found", .{path}); 140 | }; 141 | } 142 | 143 | pub fn get_config_dir() ![]const u8 { 144 | return get_app_config_dir(application_name); 145 | } 146 | 147 | pub const ConfigDirError = error{ 148 | NoSpaceLeft, 149 | MakeConfigDirFailed, 150 | MakeHomeConfigDirFailed, 151 | MakeAppConfigDirFailed, 152 | AppConfigDirUnavailable, 153 | }; 154 | 155 | fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { 156 | const a = std.heap.c_allocator; 157 | const local = struct { 158 | var config_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; 159 | var config_dir: ?[]const u8 = null; 160 | }; 161 | const config_dir = if (local.config_dir) |dir| 162 | dir 163 | else if (std.process.getEnvVarOwned(a, "XDG_CONFIG_HOME") catch null) |xdg| ret: { 164 | defer a.free(xdg); 165 | break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ xdg, appname }); 166 | } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { 167 | defer a.free(home); 168 | const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config", .{home}); 169 | std.fs.makeDirAbsolute(dir) catch |e| switch (e) { 170 | error.PathAlreadyExists => {}, 171 | else => return error.MakeHomeConfigDirFailed, 172 | }; 173 | break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname }); 174 | } else if (builtin.os.tag == .windows) ret: { 175 | if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { 176 | defer a.free(appdata); 177 | const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ appdata, appname }); 178 | std.fs.makeDirAbsolute(dir) catch |e| switch (e) { 179 | error.PathAlreadyExists => {}, 180 | else => return error.MakeAppConfigDirFailed, 181 | }; 182 | break :ret dir; 183 | } else return error.AppConfigDirUnavailable; 184 | } else return error.AppConfigDirUnavailable; 185 | 186 | local.config_dir = config_dir; 187 | std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) { 188 | error.PathAlreadyExists => {}, 189 | else => return error.MakeConfigDirFailed, 190 | }; 191 | 192 | var theme_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; 193 | std.fs.makeDirAbsolute(try std.fmt.bufPrint(&theme_dir_buffer, "{s}/{s}", .{ config_dir, theme_dir })) catch {}; 194 | 195 | return config_dir; 196 | } 197 | 198 | fn get_app_config_file_name(appname: []const u8, comptime base_name: []const u8) ConfigDirError![]const u8 { 199 | return get_app_config_dir_file_name(appname, base_name ++ ".json"); 200 | } 201 | 202 | fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: []const u8) ConfigDirError![]const u8 { 203 | const local = struct { 204 | var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined; 205 | }; 206 | return std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name }); 207 | } 208 | 209 | const theme_dir = "themes"; 210 | 211 | fn get_theme_directory() ![]const u8 { 212 | const local = struct { 213 | var dir_buffer: [std.posix.PATH_MAX]u8 = undefined; 214 | }; 215 | const a = std.heap.c_allocator; 216 | if (std.process.getEnvVarOwned(a, "FLOW_THEMES_DIR") catch null) |dir| { 217 | defer a.free(dir); 218 | return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); 219 | } 220 | return try std.fmt.bufPrint(&local.dir_buffer, "{s}/{s}", .{ try get_app_config_dir(application_name), theme_dir }); 221 | } 222 | 223 | pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { 224 | const dir = try get_theme_directory(); 225 | const local = struct { 226 | var file_buffer: [std.posix.PATH_MAX]u8 = undefined; 227 | }; 228 | return try std.fmt.bufPrint(&local.file_buffer, "{s}/{s}.json", .{ dir, theme_name }); 229 | } 230 | 231 | fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const u8 { 232 | const file_name = get_theme_file_name(theme_name) catch return null; 233 | var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; 234 | defer file.close(); 235 | return file.readToEndAlloc(allocator, 64 * 1024) catch null; 236 | } 237 | 238 | fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { 239 | return load_theme_file_internal(allocator, theme_name) catch |e| { 240 | std.log.err("loaded theme from file failed: {}", .{e}); 241 | return e; 242 | }; 243 | } 244 | 245 | fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { 246 | _ = std.json.Scanner; 247 | const json_str = read_theme(allocator, theme_name) orelse return null; 248 | defer allocator.free(json_str); 249 | return try std.json.parseFromSlice(Theme, allocator, json_str, .{ .allocate = .alloc_always }); 250 | } 251 | 252 | pub fn get_theme_by_name(allocator: std.mem.Allocator, name: []const u8) ?struct { Theme, ?std.json.Parsed(Theme) } { 253 | if (load_theme_file(allocator, name) catch null) |parsed_theme| { 254 | std.log.info("loaded theme from file: {s}", .{name}); 255 | return .{ parsed_theme.value, parsed_theme }; 256 | } 257 | 258 | std.log.info("loading theme: {s}", .{name}); 259 | for (themes.themes) |theme_| { 260 | if (std.mem.eql(u8, theme_.name, name)) 261 | return .{ theme_, null }; 262 | } 263 | return null; 264 | } 265 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const clap = @import("clap"); 3 | const syntax = @import("syntax"); 4 | const Theme = @import("theme"); 5 | const themes = @import("themes"); 6 | const term = @import("ansi_term"); 7 | const config_loader = @import("config_loader.zig"); 8 | 9 | const Writer = std.io.BufferedWriter(4096, std.fs.File.Writer).Writer; 10 | const StyleCache = std.AutoHashMap(u32, ?Theme.Token); 11 | var style_cache: StyleCache = undefined; 12 | var lang_override: ?[]const u8 = null; 13 | var lang_default: []const u8 = "conf"; 14 | const no_highlight = std.math.maxInt(usize); 15 | 16 | const builtin = @import("builtin"); 17 | pub const std_options: std.Options = .{ 18 | .log_level = if (builtin.mode == .Debug) .info else .err, 19 | }; 20 | 21 | pub fn main() !void { 22 | const params = comptime clap.parseParamsComptime( 23 | \\-h, --help Display this help and exit. 24 | \\-l, --language Override the language. 25 | \\-t, --theme Select theme to use. 26 | \\-d, --default Set the language to use if guessing failed (default: conf). 27 | \\-s, --show-language Show detected language in output. 28 | \\-T, --show-theme Show selected theme in output. 29 | \\-C, --color Always produce color output, even if stdout is not a tty. 30 | \\--html Output HTML instead of ansi escape codes. 31 | \\--list-themes Show available themes. 32 | \\--list-languages Show available language parsers. 33 | \\-H, --highlight Highlight a line or a line range: 34 | \\ * LINE highlight just a single whole line 35 | \\ * LINE,LINE highlight a line range 36 | \\-L, --limit Limit output to around or from the beginning. 37 | \\... File to open. 38 | \\ 39 | ); 40 | 41 | var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; 42 | const a = gpa.allocator(); 43 | style_cache = StyleCache.init(a); 44 | 45 | const parsers = comptime .{ 46 | .name = clap.parsers.string, 47 | .file = clap.parsers.string, 48 | .range = clap.parsers.string, 49 | .lines = clap.parsers.int(usize, 10), 50 | }; 51 | var diag = clap.Diagnostic{}; 52 | var res = clap.parse(clap.Help, ¶ms, parsers, .{ 53 | .diagnostic = &diag, 54 | .allocator = a, 55 | }) catch |err| { 56 | diag.report(std.io.getStdErr().writer(), err) catch {}; 57 | clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}) catch {}; 58 | std.process.exit(1); 59 | return err; 60 | }; 61 | defer res.deinit(); 62 | 63 | const stdout_file = std.io.getStdOut(); 64 | const stdout_writer = stdout_file.writer(); 65 | var bw = std.io.bufferedWriter(stdout_writer); 66 | const writer = bw.writer(); 67 | defer bw.flush() catch {}; 68 | 69 | if (res.args.help != 0) 70 | return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); 71 | 72 | if (res.args.@"list-themes" != 0) 73 | return list_themes(writer); 74 | 75 | if (res.args.@"list-languages" != 0) 76 | return list_langs(writer); 77 | 78 | if (res.args.color == 0 and !stdout_file.supportsAnsiEscapeCodes()) 79 | return plain_cat(res.positionals[0]); 80 | 81 | const conf, const conf_bufs = config_loader.read_config(@import("config.zig"), a); 82 | defer config_loader.free_config(a, conf_bufs); 83 | const theme_name = if (res.args.theme) |theme| theme else conf.theme; 84 | const limit_lines = res.args.limit; 85 | 86 | var highlight_line_start: usize = no_highlight; 87 | var highlight_line_end: usize = no_highlight; 88 | if (res.args.highlight) |parm| { 89 | var it = std.mem.splitScalar(u8, parm, ','); 90 | highlight_line_start = std.fmt.parseInt(usize, it.first(), 10) catch no_highlight; 91 | highlight_line_end = highlight_line_start; 92 | if (it.next()) |end| 93 | highlight_line_end = std.fmt.parseInt(usize, end, 10) catch highlight_line_start; 94 | } 95 | 96 | if (highlight_line_end < highlight_line_start) { 97 | std.log.err("invalid range", .{}); 98 | std.process.exit(1); 99 | } 100 | 101 | const theme, const parsed_theme = config_loader.get_theme_by_name(a, theme_name) orelse { 102 | std.log.err("theme \"{s}\" not found", .{theme_name}); 103 | std.process.exit(1); 104 | }; 105 | _ = parsed_theme; 106 | 107 | const set_style: StyleFn = if (res.args.html != 0) set_html_style else set_ansi_style; 108 | const unset_style: StyleFn = if (res.args.html != 0) unset_html_style else unset_ansi_style; 109 | 110 | lang_override = res.args.language; 111 | if (res.args.default) |default| lang_default = default; 112 | 113 | if (res.args.html != 0) 114 | try write_html_preamble(writer, theme.editor); 115 | 116 | if (res.positionals[0].len > 0) { 117 | for (res.positionals[0]) |arg| { 118 | const file = if (std.mem.eql(u8, arg, "-")) 119 | std.io.getStdIn() 120 | else 121 | try std.fs.cwd().openFile(arg, .{ .mode = .read_only }); 122 | defer file.close(); 123 | const content = try file.readToEndAlloc(a, std.math.maxInt(u32)); 124 | defer a.free(content); 125 | render_file( 126 | a, 127 | writer, 128 | content, 129 | arg, 130 | &theme, 131 | res.args.@"show-language" != 0, 132 | res.args.@"show-theme" != 0, 133 | set_style, 134 | unset_style, 135 | highlight_line_start, 136 | highlight_line_end, 137 | limit_lines, 138 | ) catch |e| switch (e) { 139 | error.Stop => return, 140 | else => return e, 141 | }; 142 | try bw.flush(); 143 | } 144 | } else { 145 | const content = try std.io.getStdIn().readToEndAlloc(a, std.math.maxInt(u32)); 146 | defer a.free(content); 147 | render_file( 148 | a, 149 | writer, 150 | content, 151 | "-", 152 | &theme, 153 | res.args.@"show-language" != 0, 154 | res.args.@"show-theme" != 0, 155 | set_style, 156 | unset_style, 157 | highlight_line_start, 158 | highlight_line_end, 159 | limit_lines, 160 | ) catch |e| switch (e) { 161 | error.Stop => return, 162 | else => return e, 163 | }; 164 | } 165 | 166 | if (res.args.html != 0) 167 | try write_html_postamble(writer); 168 | } 169 | 170 | fn get_parser(a: std.mem.Allocator, content: []const u8, file_path: []const u8, query_cache: *syntax.QueryCache) *syntax { 171 | return (if (lang_override) |name| 172 | syntax.create_file_type(a, name, query_cache) catch unknown_file_type(name) 173 | else 174 | 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); 175 | } 176 | 177 | fn unknown_file_type(name: []const u8) noreturn { 178 | std.log.err("unknown file type \'{s}\'\n", .{name}); 179 | std.process.exit(1); 180 | } 181 | 182 | const StyleFn = *const fn (writer: Writer, style: Theme.Style) Writer.Error!void; 183 | 184 | fn render_file( 185 | a: std.mem.Allocator, 186 | writer: Writer, 187 | content: []const u8, 188 | file_path: []const u8, 189 | theme: *const Theme, 190 | show_file_type: bool, 191 | show_theme: bool, 192 | set_style: StyleFn, 193 | unset_style: StyleFn, 194 | highlight_line_start: usize, 195 | highlight_line_end: usize, 196 | limit_lines: ?usize, 197 | ) !void { 198 | var start_line: usize = 1; 199 | var end_line: usize = std.math.maxInt(usize); 200 | 201 | if (limit_lines) |lines| { 202 | const center = (lines - 1) / 2; 203 | if (highlight_line_start != no_highlight) { 204 | const range_size = highlight_line_end - highlight_line_start; 205 | const top = center - @min(center, range_size / 2); 206 | if (highlight_line_start > top) { 207 | start_line = highlight_line_start - top; 208 | } 209 | } 210 | end_line = start_line + lines; 211 | } 212 | 213 | const query_cache = try syntax.QueryCache.create(a, .{}); 214 | const parser = get_parser(a, content, file_path, query_cache); 215 | try parser.refresh_full(content); 216 | if (show_file_type) { 217 | try render_file_type(writer, parser.file_type, theme); 218 | end_line -= 1; 219 | } 220 | if (show_theme) { 221 | try render_theme_indicator(writer, theme); 222 | end_line -= 1; 223 | } 224 | 225 | const Ctx = struct { 226 | writer: @TypeOf(writer), 227 | content: []const u8, 228 | theme: *const Theme, 229 | last_pos: usize = 0, 230 | set_style: StyleFn, 231 | unset_style: StyleFn, 232 | start_line: usize, 233 | end_line: usize, 234 | highlight_line_start: usize, 235 | highlight_line_end: usize, 236 | current_line: usize = 1, 237 | 238 | fn write_styled(ctx: *@This(), text: []const u8, style: Theme.Style) !void { 239 | if (!(ctx.start_line <= ctx.current_line and ctx.current_line <= ctx.end_line)) return; 240 | 241 | const style_: Theme.Style = if (ctx.highlight_line_start <= ctx.current_line and ctx.current_line <= ctx.highlight_line_end) 242 | .{ .fg = style.fg, .bg = ctx.theme.editor_selection.bg } 243 | else 244 | .{ .fg = style.fg }; 245 | 246 | try ctx.set_style(ctx.writer, style_); 247 | try ctx.writer.writeAll(text); 248 | try ctx.unset_style(ctx.writer, .{ .fg = ctx.theme.editor.fg }); 249 | } 250 | 251 | fn write_lines_styled(ctx: *@This(), text_: []const u8, style: Theme.Style) !void { 252 | var text = text_; 253 | while (std.mem.indexOf(u8, text, "\n")) |pos| { 254 | try ctx.write_styled(text[0 .. pos + 1], style); 255 | ctx.current_line += 1; 256 | text = text[pos + 1 ..]; 257 | } 258 | try ctx.write_styled(text, style); 259 | } 260 | 261 | fn cb(ctx: *@This(), range: syntax.Range, scope: []const u8, id: u32, idx: usize, _: *const syntax.Node) error{Stop}!void { 262 | if (idx > 0) return; 263 | 264 | if (ctx.last_pos < range.start_byte) { 265 | const before_segment = ctx.content[ctx.last_pos..range.start_byte]; 266 | ctx.write_lines_styled(before_segment, ctx.theme.editor) catch return error.Stop; 267 | ctx.last_pos = range.start_byte; 268 | } 269 | 270 | if (range.start_byte < ctx.last_pos) return; 271 | 272 | const scope_segment = ctx.content[range.start_byte..range.end_byte]; 273 | if (style_cache_lookup(ctx.theme, scope, id)) |token| { 274 | ctx.write_lines_styled(scope_segment, token.style) catch return error.Stop; 275 | } else { 276 | ctx.write_lines_styled(scope_segment, ctx.theme.editor) catch return error.Stop; 277 | } 278 | ctx.last_pos = range.end_byte; 279 | if (ctx.current_line >= ctx.end_line) 280 | return error.Stop; 281 | } 282 | }; 283 | var ctx: Ctx = .{ 284 | .writer = writer, 285 | .content = content, 286 | .theme = theme, 287 | .set_style = set_style, 288 | .unset_style = unset_style, 289 | .start_line = start_line, 290 | .end_line = end_line, 291 | .highlight_line_start = highlight_line_start, 292 | .highlight_line_end = highlight_line_end, 293 | }; 294 | const range: ?syntax.Range = ret: { 295 | if (limit_lines) |_| break :ret .{ 296 | .start_point = .{ .row = @intCast(start_line - 1), .column = 0 }, 297 | .end_point = .{ .row = @intCast(end_line - 1), .column = 0 }, 298 | .start_byte = 0, 299 | .end_byte = 0, 300 | }; 301 | break :ret null; 302 | }; 303 | try parser.render(&ctx, Ctx.cb, range); 304 | while (ctx.current_line < end_line) { 305 | if (std.mem.indexOfPos(u8, content, ctx.last_pos, "\n")) |pos| { 306 | try ctx.writer.writeAll(content[ctx.last_pos .. pos + 1]); 307 | ctx.current_line += 1; 308 | ctx.last_pos = pos + 1; 309 | } else { 310 | try ctx.writer.writeAll(content[ctx.last_pos..]); 311 | break; 312 | } 313 | } 314 | } 315 | 316 | fn style_cache_lookup(theme: *const Theme, scope: []const u8, id: u32) ?Theme.Token { 317 | return if (style_cache.get(id)) |sty| ret: { 318 | break :ret sty; 319 | } else ret: { 320 | const sty = find_scope_style(theme, scope) orelse null; 321 | style_cache.put(id, sty) catch {}; 322 | break :ret sty; 323 | }; 324 | } 325 | 326 | fn find_scope_style(theme: *const Theme, scope: []const u8) ?Theme.Token { 327 | return if (find_scope_fallback(scope)) |tm_scope| 328 | find_scope_style_nofallback(theme, tm_scope) orelse find_scope_style_nofallback(theme, scope) 329 | else 330 | find_scope_style_nofallback(theme, scope); 331 | } 332 | 333 | fn find_scope_style_nofallback(theme: *const Theme, scope: []const u8) ?Theme.Token { 334 | var idx = theme.tokens.len - 1; 335 | var done = false; 336 | while (!done) : (if (idx == 0) { 337 | done = true; 338 | } else { 339 | idx -= 1; 340 | }) { 341 | const token = theme.tokens[idx]; 342 | const name = themes.scopes[token.id]; 343 | if (name.len > scope.len) 344 | continue; 345 | if (std.mem.eql(u8, name, scope[0..name.len])) 346 | return token; 347 | } 348 | return null; 349 | } 350 | 351 | fn find_scope_fallback(scope: []const u8) ?[]const u8 { 352 | for (fallbacks) |fallback| { 353 | if (fallback.ts.len > scope.len) 354 | continue; 355 | if (std.mem.eql(u8, fallback.ts, scope[0..fallback.ts.len])) 356 | return fallback.tm; 357 | } 358 | return null; 359 | } 360 | 361 | pub const FallBack = struct { ts: []const u8, tm: []const u8 }; 362 | pub const fallbacks: []const FallBack = &[_]FallBack{ 363 | .{ .ts = "namespace", .tm = "entity.name.namespace" }, 364 | .{ .ts = "type", .tm = "entity.name.type" }, 365 | .{ .ts = "type.defaultLibrary", .tm = "support.type" }, 366 | .{ .ts = "struct", .tm = "storage.type.struct" }, 367 | .{ .ts = "class", .tm = "entity.name.type.class" }, 368 | .{ .ts = "class.defaultLibrary", .tm = "support.class" }, 369 | .{ .ts = "interface", .tm = "entity.name.type.interface" }, 370 | .{ .ts = "enum", .tm = "entity.name.type.enum" }, 371 | .{ .ts = "function", .tm = "entity.name.function" }, 372 | .{ .ts = "function.defaultLibrary", .tm = "support.function" }, 373 | .{ .ts = "method", .tm = "entity.name.function.member" }, 374 | .{ .ts = "macro", .tm = "entity.name.function.macro" }, 375 | .{ .ts = "variable", .tm = "variable.other.readwrite , entity.name.variable" }, 376 | .{ .ts = "variable.readonly", .tm = "variable.other.constant" }, 377 | .{ .ts = "variable.readonly.defaultLibrary", .tm = "support.constant" }, 378 | .{ .ts = "parameter", .tm = "variable.parameter" }, 379 | .{ .ts = "property", .tm = "variable.other.property" }, 380 | .{ .ts = "property.readonly", .tm = "variable.other.constant.property" }, 381 | .{ .ts = "enumMember", .tm = "variable.other.enummember" }, 382 | .{ .ts = "event", .tm = "variable.other.event" }, 383 | 384 | // zig 385 | .{ .ts = "attribute", .tm = "keyword" }, 386 | .{ .ts = "number", .tm = "constant.numeric" }, 387 | .{ .ts = "conditional", .tm = "keyword.control.conditional" }, 388 | .{ .ts = "operator", .tm = "keyword.operator" }, 389 | .{ .ts = "boolean", .tm = "keyword.constant.bool" }, 390 | .{ .ts = "string", .tm = "string.quoted" }, 391 | .{ .ts = "repeat", .tm = "keyword.control.flow" }, 392 | .{ .ts = "field", .tm = "variable" }, 393 | }; 394 | 395 | fn list_themes(writer: Writer) !void { 396 | var max_name_len: usize = 0; 397 | for (themes.themes) |theme| 398 | max_name_len = @max(max_name_len, theme.name.len); 399 | 400 | for (themes.themes) |theme| { 401 | try writer.writeAll(theme.name); 402 | try writer.writeByteNTimes(' ', max_name_len + 2 - theme.name.len); 403 | try writer.writeAll(theme.description); 404 | try writer.writeAll("\n"); 405 | } 406 | } 407 | 408 | fn set_ansi_style(writer: Writer, style: Theme.Style) Writer.Error!void { 409 | const ansi_style: term.style.Style = .{ 410 | .foreground = if (style.fg) |color| to_rgb_color(color.color) else .Default, 411 | .background = if (style.bg) |color| to_rgb_color(color.color) else .Default, 412 | .font_style = switch (style.fs orelse .normal) { 413 | .normal => term.style.FontStyle{}, 414 | .bold => term.style.FontStyle{ .bold = true }, 415 | .italic => term.style.FontStyle{ .italic = true }, 416 | .underline => term.style.FontStyle{ .underline = true }, 417 | .undercurl => term.style.FontStyle{ .underline = true }, 418 | .strikethrough => term.style.FontStyle{ .crossedout = true }, 419 | }, 420 | }; 421 | try term.format.updateStyle(writer, ansi_style, null); 422 | } 423 | 424 | const unset_ansi_style = set_ansi_style; 425 | 426 | fn write_html_preamble(writer: Writer, style: Theme.Style) !void { 427 | const color = if (style.fg) |color| color.color else 0; 428 | const background = if (style.bg) |background| background.color else 0xFFFFFF; 429 | try writer.writeAll("
");
434 | }
435 | 
436 | fn write_html_postamble(writer: Writer) !void {
437 |     try writer.writeAll("
"); 438 | } 439 | 440 | fn set_html_style(writer: Writer, style: Theme.Style) !void { 441 | const color = if (style.fg) |color| color.color else 0; 442 | try writer.writeAll(" {}, 446 | .bold => try writer.writeAll(";font-weight: bold"), 447 | .italic => try writer.writeAll(";font-style: italic"), 448 | .underline => try writer.writeAll(";text-decoration: underline"), 449 | .undercurl => try writer.writeAll(";text-decoration: underline wavy"), 450 | .strikethrough => try writer.writeAll(";text-decoration: line-through"), 451 | } 452 | try writer.writeAll(";\">"); 453 | } 454 | 455 | fn unset_html_style(writer: Writer, _: Theme.Style) !void { 456 | try writer.writeAll(""); 457 | } 458 | 459 | fn to_rgb_color(color: u24) term.style.Color { 460 | const r = @as(u8, @intCast(color >> 16 & 0xFF)); 461 | const g = @as(u8, @intCast(color >> 8 & 0xFF)); 462 | const b = @as(u8, @intCast(color & 0xFF)); 463 | return .{ .RGB = .{ .r = r, .g = g, .b = b } }; 464 | } 465 | 466 | fn write_hex_color(writer: Writer, color: u24) !void { 467 | try writer.print("#{x:0>6}", .{color}); 468 | } 469 | 470 | fn list_langs(writer: Writer) !void { 471 | for (syntax.FileType.file_types) |file_type| { 472 | try writer.writeAll(file_type.name); 473 | try writer.writeAll("\n"); 474 | } 475 | } 476 | 477 | fn render_file_type(writer: Writer, file_type: *const syntax.FileType, theme: *const Theme) !void { 478 | const style = theme.editor_selection; 479 | const reversed = Theme.Style{ .fg = theme.editor_selection.bg }; 480 | const plain: Theme.Style = Theme.Style{ .fg = theme.editor.fg }; 481 | try set_ansi_style(writer, reversed); 482 | try writer.writeAll(""); 483 | try set_ansi_style(writer, .{ 484 | .fg = if (file_type.color == 0xFFFFFF or file_type.color == 0x000000) style.fg else .{ .color = file_type.color }, 485 | .bg = style.bg, 486 | }); 487 | try writer.writeAll(file_type.icon); 488 | try writer.writeAll(" "); 489 | try set_ansi_style(writer, style); 490 | try writer.writeAll(file_type.name); 491 | try set_ansi_style(writer, reversed); 492 | try writer.writeAll(""); 493 | try set_ansi_style(writer, plain); 494 | try writer.writeAll("\n"); 495 | } 496 | 497 | fn render_theme_indicator(writer: Writer, theme: *const Theme) !void { 498 | const style = Theme.Style{ .bg = theme.editor_selection.bg, .fg = theme.editor.fg }; 499 | const reversed = Theme.Style{ .fg = theme.editor_selection.bg }; 500 | const plain: Theme.Style = Theme.Style{ .fg = theme.editor.fg }; 501 | try set_ansi_style(writer, reversed); 502 | try writer.writeAll(""); 503 | try set_ansi_style(writer, style); 504 | try writer.writeAll(theme.name); 505 | try set_ansi_style(writer, reversed); 506 | try writer.writeAll(""); 507 | try set_ansi_style(writer, plain); 508 | try writer.writeAll("\n"); 509 | } 510 | 511 | fn plain_cat(files: []const []const u8) !void { 512 | const stdout = std.io.getStdOut(); 513 | if (files.len == 0) { 514 | try plain_cat_file(stdout, "-"); 515 | } else { 516 | for (files) |file| try plain_cat_file(stdout, file); 517 | } 518 | } 519 | 520 | fn plain_cat_file(out_file: std.fs.File, in_file_name: []const u8) !void { 521 | var in_file = if (std.mem.eql(u8, in_file_name, "-")) 522 | std.io.getStdIn() 523 | else 524 | try std.fs.cwd().openFile(in_file_name, .{}); 525 | defer in_file.close(); 526 | 527 | var buf: [std.heap.page_size_min]u8 = undefined; 528 | while (true) { 529 | const bytes_read = try in_file.read(&buf); 530 | if (bytes_read == 0) return; 531 | try out_file.writeAll(buf[0..bytes_read]); 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /zig: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ARCH=$(uname -m) 5 | 6 | BASEDIR="$(cd "$(dirname "$0")" && pwd)" 7 | ZIGDIR=$BASEDIR/.cache/zig 8 | VERSION=$(build.zig.version 32 | NEWVERSION=$(compile_commands.json 68 | exit 0 69 | fi 70 | 71 | exec $ZIG "$@" 72 | --------------------------------------------------------------------------------