├── .gitignore ├── src ├── config.zig ├── config_loader.zig └── main.zig ├── scripts ├── fzf-grep └── make_release ├── README.md ├── LICENSE └── zig /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache/ 2 | /.zig-cache/ 3 | /zig-out/ 4 | -------------------------------------------------------------------------------- /src/config.zig: -------------------------------------------------------------------------------- 1 | theme: []const u8 = "default", 2 | include_files: []const u8 = "", 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) struct { syntax.FileType, *syntax } { 171 | return if (lang_override) |name| blk: { 172 | const file_type = syntax.FileType.get_by_name_static(name) orelse unknown_file_type(name); 173 | break :blk .{ file_type, syntax.create(file_type, a, query_cache) catch unknown_file_type(name) }; 174 | } else blk: { 175 | const file_type = syntax.FileType.guess_static(file_path, content) orelse 176 | syntax.FileType.get_by_name_static(lang_default) orelse 177 | unknown_file_type(lang_default); 178 | break :blk .{ file_type, syntax.create(file_type, a, query_cache) catch unknown_file_type(lang_default) }; 179 | }; 180 | } 181 | 182 | fn unknown_file_type(name: []const u8) noreturn { 183 | std.log.err("unknown file type \'{s}\'\n", .{name}); 184 | std.process.exit(1); 185 | } 186 | 187 | const StyleFn = *const fn (writer: Writer, style: Theme.Style) Writer.Error!void; 188 | 189 | fn render_file( 190 | a: std.mem.Allocator, 191 | writer: Writer, 192 | content: []const u8, 193 | file_path: []const u8, 194 | theme: *const Theme, 195 | show_file_type: bool, 196 | show_theme: bool, 197 | set_style: StyleFn, 198 | unset_style: StyleFn, 199 | highlight_line_start: usize, 200 | highlight_line_end: usize, 201 | limit_lines: ?usize, 202 | ) !void { 203 | var start_line: usize = 1; 204 | var end_line: usize = std.math.maxInt(usize); 205 | 206 | if (limit_lines) |lines| { 207 | const center = (lines - 1) / 2; 208 | if (highlight_line_start != no_highlight) { 209 | const range_size = highlight_line_end - highlight_line_start; 210 | const top = center - @min(center, range_size / 2); 211 | if (highlight_line_start > top) { 212 | start_line = highlight_line_start - top; 213 | } 214 | } 215 | end_line = start_line + lines; 216 | } 217 | 218 | const query_cache = try syntax.QueryCache.create(a, .{}); 219 | const file_type, const parser = get_parser(a, content, file_path, query_cache); 220 | try parser.refresh_full(content); 221 | if (show_file_type) { 222 | try render_file_type(writer, &file_type, theme); 223 | end_line -= 1; 224 | } 225 | if (show_theme) { 226 | try render_theme_indicator(writer, theme); 227 | end_line -= 1; 228 | } 229 | 230 | const Ctx = struct { 231 | writer: @TypeOf(writer), 232 | content: []const u8, 233 | theme: *const Theme, 234 | last_pos: usize = 0, 235 | set_style: StyleFn, 236 | unset_style: StyleFn, 237 | start_line: usize, 238 | end_line: usize, 239 | highlight_line_start: usize, 240 | highlight_line_end: usize, 241 | current_line: usize = 1, 242 | 243 | fn write_styled(ctx: *@This(), text: []const u8, style: Theme.Style) !void { 244 | if (!(ctx.start_line <= ctx.current_line and ctx.current_line <= ctx.end_line)) return; 245 | 246 | const style_: Theme.Style = if (ctx.highlight_line_start <= ctx.current_line and ctx.current_line <= ctx.highlight_line_end) 247 | .{ .fg = style.fg, .bg = ctx.theme.editor_selection.bg } 248 | else 249 | .{ .fg = style.fg }; 250 | 251 | try ctx.set_style(ctx.writer, style_); 252 | try ctx.writer.writeAll(text); 253 | try ctx.unset_style(ctx.writer, .{ .fg = ctx.theme.editor.fg }); 254 | } 255 | 256 | fn write_lines_styled(ctx: *@This(), text_: []const u8, style: Theme.Style) !void { 257 | var text = text_; 258 | while (std.mem.indexOf(u8, text, "\n")) |pos| { 259 | try ctx.write_styled(text[0 .. pos + 1], style); 260 | ctx.current_line += 1; 261 | text = text[pos + 1 ..]; 262 | } 263 | try ctx.write_styled(text, style); 264 | } 265 | 266 | fn cb(ctx: *@This(), range: syntax.Range, scope: []const u8, id: u32, idx: usize, _: *const syntax.Node) error{Stop}!void { 267 | if (idx > 0) return; 268 | 269 | if (ctx.last_pos < range.start_byte) { 270 | const before_segment = ctx.content[ctx.last_pos..range.start_byte]; 271 | ctx.write_lines_styled(before_segment, ctx.theme.editor) catch return error.Stop; 272 | ctx.last_pos = range.start_byte; 273 | } 274 | 275 | if (range.start_byte < ctx.last_pos) return; 276 | 277 | const scope_segment = ctx.content[range.start_byte..range.end_byte]; 278 | if (style_cache_lookup(ctx.theme, scope, id)) |token| { 279 | ctx.write_lines_styled(scope_segment, token.style) catch return error.Stop; 280 | } else { 281 | ctx.write_lines_styled(scope_segment, ctx.theme.editor) catch return error.Stop; 282 | } 283 | ctx.last_pos = range.end_byte; 284 | if (ctx.current_line >= ctx.end_line) 285 | return error.Stop; 286 | } 287 | }; 288 | var ctx: Ctx = .{ 289 | .writer = writer, 290 | .content = content, 291 | .theme = theme, 292 | .set_style = set_style, 293 | .unset_style = unset_style, 294 | .start_line = start_line, 295 | .end_line = end_line, 296 | .highlight_line_start = highlight_line_start, 297 | .highlight_line_end = highlight_line_end, 298 | }; 299 | const range: ?syntax.Range = ret: { 300 | if (limit_lines) |_| break :ret .{ 301 | .start_point = .{ .row = @intCast(start_line - 1), .column = 0 }, 302 | .end_point = .{ .row = @intCast(end_line - 1), .column = 0 }, 303 | .start_byte = 0, 304 | .end_byte = 0, 305 | }; 306 | break :ret null; 307 | }; 308 | try parser.render(&ctx, Ctx.cb, range); 309 | while (ctx.current_line < end_line) { 310 | if (std.mem.indexOfPos(u8, content, ctx.last_pos, "\n")) |pos| { 311 | try ctx.writer.writeAll(content[ctx.last_pos .. pos + 1]); 312 | ctx.current_line += 1; 313 | ctx.last_pos = pos + 1; 314 | } else { 315 | try ctx.writer.writeAll(content[ctx.last_pos..]); 316 | break; 317 | } 318 | } 319 | } 320 | 321 | fn style_cache_lookup(theme: *const Theme, scope: []const u8, id: u32) ?Theme.Token { 322 | return if (style_cache.get(id)) |sty| ret: { 323 | break :ret sty; 324 | } else ret: { 325 | const sty = find_scope_style(theme, scope) orelse null; 326 | style_cache.put(id, sty) catch {}; 327 | break :ret sty; 328 | }; 329 | } 330 | 331 | fn find_scope_style(theme: *const Theme, scope: []const u8) ?Theme.Token { 332 | return if (find_scope_fallback(scope)) |tm_scope| 333 | find_scope_style_nofallback(theme, tm_scope) orelse find_scope_style_nofallback(theme, scope) 334 | else 335 | find_scope_style_nofallback(theme, scope); 336 | } 337 | 338 | fn find_scope_style_nofallback(theme: *const Theme, scope: []const u8) ?Theme.Token { 339 | var idx = theme.tokens.len - 1; 340 | var done = false; 341 | while (!done) : (if (idx == 0) { 342 | done = true; 343 | } else { 344 | idx -= 1; 345 | }) { 346 | const token = theme.tokens[idx]; 347 | const name = themes.scopes[token.id]; 348 | if (name.len > scope.len) 349 | continue; 350 | if (std.mem.eql(u8, name, scope[0..name.len])) 351 | return token; 352 | } 353 | return null; 354 | } 355 | 356 | fn find_scope_fallback(scope: []const u8) ?[]const u8 { 357 | for (fallbacks) |fallback| { 358 | if (fallback.ts.len > scope.len) 359 | continue; 360 | if (std.mem.eql(u8, fallback.ts, scope[0..fallback.ts.len])) 361 | return fallback.tm; 362 | } 363 | return null; 364 | } 365 | 366 | pub const FallBack = struct { ts: []const u8, tm: []const u8 }; 367 | pub const fallbacks: []const FallBack = &[_]FallBack{ 368 | .{ .ts = "namespace", .tm = "entity.name.namespace" }, 369 | .{ .ts = "type", .tm = "entity.name.type" }, 370 | .{ .ts = "type.defaultLibrary", .tm = "support.type" }, 371 | .{ .ts = "struct", .tm = "storage.type.struct" }, 372 | .{ .ts = "class", .tm = "entity.name.type.class" }, 373 | .{ .ts = "class.defaultLibrary", .tm = "support.class" }, 374 | .{ .ts = "interface", .tm = "entity.name.type.interface" }, 375 | .{ .ts = "enum", .tm = "entity.name.type.enum" }, 376 | .{ .ts = "function", .tm = "entity.name.function" }, 377 | .{ .ts = "function.defaultLibrary", .tm = "support.function" }, 378 | .{ .ts = "method", .tm = "entity.name.function.member" }, 379 | .{ .ts = "macro", .tm = "entity.name.function.macro" }, 380 | .{ .ts = "variable", .tm = "variable.other.readwrite , entity.name.variable" }, 381 | .{ .ts = "variable.readonly", .tm = "variable.other.constant" }, 382 | .{ .ts = "variable.readonly.defaultLibrary", .tm = "support.constant" }, 383 | .{ .ts = "parameter", .tm = "variable.parameter" }, 384 | .{ .ts = "property", .tm = "variable.other.property" }, 385 | .{ .ts = "property.readonly", .tm = "variable.other.constant.property" }, 386 | .{ .ts = "enumMember", .tm = "variable.other.enummember" }, 387 | .{ .ts = "event", .tm = "variable.other.event" }, 388 | 389 | // zig 390 | .{ .ts = "attribute", .tm = "keyword" }, 391 | .{ .ts = "number", .tm = "constant.numeric" }, 392 | .{ .ts = "conditional", .tm = "keyword.control.conditional" }, 393 | .{ .ts = "operator", .tm = "keyword.operator" }, 394 | .{ .ts = "boolean", .tm = "keyword.constant.bool" }, 395 | .{ .ts = "string", .tm = "string.quoted" }, 396 | .{ .ts = "repeat", .tm = "keyword.control.flow" }, 397 | .{ .ts = "field", .tm = "variable" }, 398 | }; 399 | 400 | fn list_themes(writer: Writer) !void { 401 | var max_name_len: usize = 0; 402 | for (themes.themes) |theme| 403 | max_name_len = @max(max_name_len, theme.name.len); 404 | 405 | for (themes.themes) |theme| { 406 | try writer.writeAll(theme.name); 407 | try writer.writeByteNTimes(' ', max_name_len + 2 - theme.name.len); 408 | try writer.writeAll(theme.description); 409 | try writer.writeAll("\n"); 410 | } 411 | } 412 | 413 | fn set_ansi_style(writer: Writer, style: Theme.Style) Writer.Error!void { 414 | const ansi_style: term.style.Style = .{ 415 | .foreground = if (style.fg) |color| to_rgb_color(color.color) else .Default, 416 | .background = if (style.bg) |color| to_rgb_color(color.color) else .Default, 417 | .font_style = switch (style.fs orelse .normal) { 418 | .normal => term.style.FontStyle{}, 419 | .bold => term.style.FontStyle{ .bold = true }, 420 | .italic => term.style.FontStyle{ .italic = true }, 421 | .underline => term.style.FontStyle{ .underline = true }, 422 | .undercurl => term.style.FontStyle{ .underline = true }, 423 | .strikethrough => term.style.FontStyle{ .crossedout = true }, 424 | }, 425 | }; 426 | try term.format.updateStyle(writer, ansi_style, null); 427 | } 428 | 429 | const unset_ansi_style = set_ansi_style; 430 | 431 | fn write_html_preamble(writer: Writer, style: Theme.Style) !void { 432 | const color = if (style.fg) |color| color.color else 0; 433 | const background = if (style.bg) |background| background.color else 0xFFFFFF; 434 | try writer.writeAll("
");
439 | }
440 | 
441 | fn write_html_postamble(writer: Writer) !void {
442 |     try writer.writeAll("
"); 443 | } 444 | 445 | fn set_html_style(writer: Writer, style: Theme.Style) !void { 446 | const color = if (style.fg) |color| color.color else 0; 447 | try writer.writeAll(" {}, 451 | .bold => try writer.writeAll(";font-weight: bold"), 452 | .italic => try writer.writeAll(";font-style: italic"), 453 | .underline => try writer.writeAll(";text-decoration: underline"), 454 | .undercurl => try writer.writeAll(";text-decoration: underline wavy"), 455 | .strikethrough => try writer.writeAll(";text-decoration: line-through"), 456 | } 457 | try writer.writeAll(";\">"); 458 | } 459 | 460 | fn unset_html_style(writer: Writer, _: Theme.Style) !void { 461 | try writer.writeAll(""); 462 | } 463 | 464 | fn to_rgb_color(color: u24) term.style.Color { 465 | const r = @as(u8, @intCast(color >> 16 & 0xFF)); 466 | const g = @as(u8, @intCast(color >> 8 & 0xFF)); 467 | const b = @as(u8, @intCast(color & 0xFF)); 468 | return .{ .RGB = .{ .r = r, .g = g, .b = b } }; 469 | } 470 | 471 | fn write_hex_color(writer: Writer, color: u24) !void { 472 | try writer.print("#{x:0>6}", .{color}); 473 | } 474 | 475 | fn list_langs(writer: Writer) !void { 476 | for (syntax.FileType.get_all()) |file_type| { 477 | try writer.writeAll(file_type.name); 478 | try writer.writeAll("\n"); 479 | } 480 | } 481 | 482 | fn render_file_type(writer: Writer, file_type: *const syntax.FileType, theme: *const Theme) !void { 483 | const style = theme.editor_selection; 484 | const reversed = Theme.Style{ .fg = theme.editor_selection.bg }; 485 | const plain: Theme.Style = Theme.Style{ .fg = theme.editor.fg }; 486 | try set_ansi_style(writer, reversed); 487 | try writer.writeAll(""); 488 | try set_ansi_style(writer, .{ 489 | .fg = if (file_type.color == 0xFFFFFF or file_type.color == 0x000000) style.fg else .{ .color = file_type.color }, 490 | .bg = style.bg, 491 | }); 492 | try writer.writeAll(file_type.icon); 493 | try writer.writeAll(" "); 494 | try set_ansi_style(writer, style); 495 | try writer.writeAll(file_type.name); 496 | try set_ansi_style(writer, reversed); 497 | try writer.writeAll(""); 498 | try set_ansi_style(writer, plain); 499 | try writer.writeAll("\n"); 500 | } 501 | 502 | fn render_theme_indicator(writer: Writer, theme: *const Theme) !void { 503 | const style = Theme.Style{ .bg = theme.editor_selection.bg, .fg = theme.editor.fg }; 504 | const reversed = Theme.Style{ .fg = theme.editor_selection.bg }; 505 | const plain: Theme.Style = Theme.Style{ .fg = theme.editor.fg }; 506 | try set_ansi_style(writer, reversed); 507 | try writer.writeAll(""); 508 | try set_ansi_style(writer, style); 509 | try writer.writeAll(theme.name); 510 | try set_ansi_style(writer, reversed); 511 | try writer.writeAll(""); 512 | try set_ansi_style(writer, plain); 513 | try writer.writeAll("\n"); 514 | } 515 | 516 | fn plain_cat(files: []const []const u8) !void { 517 | const stdout = std.io.getStdOut(); 518 | if (files.len == 0) { 519 | try plain_cat_file(stdout, "-"); 520 | } else { 521 | for (files) |file| try plain_cat_file(stdout, file); 522 | } 523 | } 524 | 525 | fn plain_cat_file(out_file: std.fs.File, in_file_name: []const u8) !void { 526 | var in_file = if (std.mem.eql(u8, in_file_name, "-")) 527 | std.io.getStdIn() 528 | else 529 | try std.fs.cwd().openFile(in_file_name, .{}); 530 | defer in_file.close(); 531 | 532 | var buf: [std.heap.page_size_min]u8 = undefined; 533 | while (true) { 534 | const bytes_read = try in_file.read(&buf); 535 | if (bytes_read == 0) return; 536 | try out_file.writeAll(buf[0..bytes_read]); 537 | } 538 | } 539 | --------------------------------------------------------------------------------