Something went wrong. Try again.
zig langref cli nate.tngl.io/zigman
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452const std = @import("std");const builtin = @import("builtin");const Io = std.Io;const Environ = std.process.Environ;const zigman = @import("root.zig");const render = zigman.render;
const usage = \\zigman — the zig language reference in your terminal \\ \\usage: \\ zigman <query> show the matching section (paged on a terminal) \\ zigman -l [query] list section names — all, or matching a query \\ \\a query that names one section shows it; an ambiguous query lists the \\candidates. type any part of a section's name. \\ \\examples: \\ zigman comptime show the comptime section \\ zigman error several match → lists them \\ zigman -o comptime open the comptime section in your browser \\ zigman -l the whole table of contents \\ zigman -V 0.15.1 comptime show it from a specific langref version \\ \\options: \\ -l, --list list section names instead of rendering \\ -o, --open open the section in your browser instead \\ -V, --version <ver> langref version to read (default: master) \\ -r, --refresh bypass the cache and refetch \\ --no-pager don't page output on a terminal \\ --completions <shell> print a completion script (zsh|bash|fish) \\ -h, --help show this help \\ \\the reference is cached under $XDG_CACHE_HOME or ~/.cache/zigman. \\;
const Args = struct { ver: []const u8 = "master", refresh: bool = false, no_pager: bool = false, list: bool = false, open: bool = false, help: bool = false, completions: ?[]const u8 = null, query: ?[]const u8 = null,};
pub fn main(init: std.process.Init) !void { run(init) catch |e| switch (e) { // stdout/pager closed early (piping into `head`, quitting `less`) error.WriteFailed => {}, else => return e, };}
fn run(init: std.process.Init) !void { const a = init.gpa; const io = init.io; const env = init.environ_map.*;
var out_buf: [16 * 1024]u8 = undefined; var fw = std.Io.File.stdout().writer(io, &out_buf); const out = &fw.interface; var ew = std.Io.File.stderr().writer(io, &.{}); const err = &ew.interface;
const args = parseArgs(init.minimal.args) catch { try err.writeAll(usage); try err.flush(); std.process.exit(2); };
if (args.help) { try out.writeAll(usage); try out.flush(); return; } if (args.completions) |shell| { const script = completionScript(shell) orelse { try err.print("zigman: unknown shell '{s}' (want zsh, bash, or fish)\n", .{shell}); try err.flush(); std.process.exit(2); }; try out.writeAll(script); try out.flush(); return; } if (!zigman.validVersion(args.ver)) { try err.print("zigman: invalid version '{s}'\n", .{args.ver}); try err.flush(); std.process.exit(2); }
// `--open` with no query opens the reference index — no fetch needed if (args.open and args.query == null) { try openInBrowser(a, io, env, err, args.ver, null); return; }
// nothing to do without a query unless listing if (args.query == null and !args.list) { try out.writeAll(usage); try out.flush(); return; }
const dir = cacheDir(a, env) catch { try err.writeAll("zigman: neither $XDG_CACHE_HOME nor $HOME is set\n"); try err.flush(); std.process.exit(1); }; defer a.free(dir); const html = loadLangref(a, io, dir, args.ver, args.refresh) catch |e| { try err.print("zigman: could not load langref ({t})\n", .{e}); try err.flush(); std.process.exit(1); }; defer a.free(html);
const want_pager = !args.no_pager and (std.Io.File.stdout().isTty(io) catch false);
if (args.list and !args.open) { const text = try listText(a, html, args.query); defer a.free(text); try emit(io, out, env, text, want_pager); return; }
// query mode: resolve to one section (then show or open it), or surface // the candidates. `--open` only changes what we do with a unique match. const query = args.query.?; if (exactId(html, query)) |id| { try act(a, io, out, env, err, html, id, args, want_pager); return; } // resolve a fuzzy query to its obvious primary section before listing if (zigman.primaryMatch(html, query)) |id| { try act(a, io, out, env, err, html, id, args, want_pager); return; } // a 1-2 char query that wasn't an exact section just substring-matches noise // (e.g. "Io" -> "Introduction"); treat as no-match so the scope hint fires. if (query.len < 3) try noMatch(err, query); // never dead-end a multi-word query: retry on individual words var matches = try collect(a, html, query); if (matches.len == 0) { a.free(matches); matches = try collectWords(a, html, query); } defer a.free(matches); // freed once, after matches settles on its final slice switch (matches.len) { 0 => try noMatch(err, query), 1 => try act(a, io, out, env, err, html, matches[0].id, args, want_pager), else => { const text = try formatMatches(a, matches); defer a.free(text); try emit(io, out, env, text, want_pager); try err.print("{d} sections match '{s}' — narrow it, or run `zigman <name>`\n", .{ matches.len, query }); try err.flush(); }, }}
/// No section matched. State what zigman *is* so a std-ish miss reads as out of/// scope rather than a broken tool — but don't instruct how to use std/the rest/// of the toolchain (that's the guide's job). Exits non-zero.fn noMatch(err: *Io.Writer, query: []const u8) !noreturn { try err.print("zigman: no section matching '{s}' (the langref covers the Zig language, not std)\n", .{query}); try err.flush(); std.process.exit(1);}
/// Apply the resolved section: open it in a browser, or render it.fn act(a: std.mem.Allocator, io: Io, out: *Io.Writer, env: Environ.Map, err: *Io.Writer, html: []const u8, id: []const u8, args: Args, want_pager: bool) !void { if (args.open) { try openInBrowser(a, io, env, err, args.ver, id); return; } const sec = zigman.sliceSection(html, id) orelse return; const md = try render(a, sec); defer a.free(md); try emit(io, out, env, md, want_pager);}
/// Launch the system browser at the langref, optionally at a section anchor./// Uses $BROWSER, else the platform opener (`open` / `xdg-open`).fn openInBrowser(a: std.mem.Allocator, io: Io, env: Environ.Map, err: *Io.Writer, ver: []const u8, anchor: ?[]const u8) !void { const link = try zigman.url(a, ver, anchor); defer a.free(link); const opener = browserCmd(env); var child = std.process.spawn(io, .{ .argv = &.{ opener, link }, .stdin = .ignore, .stdout = .inherit, .stderr = .inherit, }) catch { // no browser launcher available — print the URL so it can be copied try err.print("zigman: open {s}\n", .{link}); try err.flush(); return; }; _ = child.wait(io) catch {}; try err.print("opening {s}\n", .{link}); try err.flush();}
fn browserCmd(env: Environ.Map) []const u8 { if (env.get("BROWSER")) |b| { if (b.len > 0) return b; } return switch (builtin.target.os.tag) { .macos => "open", .windows => "explorer", else => "xdg-open", };}
// ----------------------------------------------------------------------------// section matching// ----------------------------------------------------------------------------
const Match = struct { level: u8, id: []const u8 };
fn exactId(html: []const u8, query: []const u8) ?[]const u8 { var it = zigman.Sections.init(html); while (it.next()) |s| { if (std.ascii.eqlIgnoreCase(s.id, query)) return s.id; } return null;}
/// Collect sections whose anchor/title contains `query`. Caller owns the slice.fn collect(a: std.mem.Allocator, html: []const u8, query: []const u8) ![]Match { var list: std.ArrayList(Match) = .empty; errdefer list.deinit(a); var it = zigman.Sections.init(html); while (it.next()) |s| { if (zigman.matchesQuery(s, query)) try list.append(a, .{ .level = s.level, .id = s.id }); } return list.toOwnedSlice(a);}
/// Fallback for a multi-word query that matched nothing as a phrase: collect/// sections matching ANY word (length >= 2), so `labeled block` surfaces/// Blocks + Labeled-switch/while/for instead of dead-ending. Caller owns slice.fn collectWords(a: std.mem.Allocator, html: []const u8, query: []const u8) ![]Match { var list: std.ArrayList(Match) = .empty; errdefer list.deinit(a); var it = zigman.Sections.init(html); while (it.next()) |s| { var words = std.mem.tokenizeScalar(u8, query, ' '); const hit = while (words.next()) |w| { if (w.len >= 2 and zigman.matchesQuery(s, w)) break true; } else false; if (hit) try list.append(a, .{ .level = s.level, .id = s.id }); } return list.toOwnedSlice(a);}
fn formatMatches(a: std.mem.Allocator, matches: []const Match) ![]u8 { var buf: Io.Writer.Allocating = .init(a); defer buf.deinit(); for (matches) |m| { try buf.writer.splatByteAll(' ', (m.level - 1) * 2); try buf.writer.print("{s}\n", .{m.id}); } return a.dupe(u8, buf.written());}
/// The full table of contents, or just the sections matching `query`.fn listText(a: std.mem.Allocator, html: []const u8, query: ?[]const u8) ![]u8 { var buf: Io.Writer.Allocating = .init(a); defer buf.deinit(); var it = zigman.Sections.init(html); while (it.next()) |s| { if (query) |q| { if (!zigman.matchesQuery(s, q)) continue; } try buf.writer.splatByteAll(' ', (s.level - 1) * 2); try buf.writer.print("{s}\n", .{s.id}); } return a.dupe(u8, buf.written());}
// ----------------------------------------------------------------------------// output: page on a terminal, raw when piped// ----------------------------------------------------------------------------
fn emit(io: Io, out: *Io.Writer, env: Environ.Map, text: []const u8, want_pager: bool) !void { if (want_pager and page(io, env, text)) return; try out.writeAll(text); try out.flush();}
/// Pipe `text` through $PAGER (default `less -FRX`). Returns false if no pager/// could be spawned, so the caller falls back to raw output. `less -F` means/// short output prints and exits without trapping you in the pager.fn page(io: Io, env: Environ.Map, text: []const u8) bool { const pager = blk: { if (env.get("PAGER")) |p| { if (p.len > 0) break :blk p; } break :blk "less -FRX"; }; var child = std.process.spawn(io, .{ .argv = &.{ "sh", "-c", pager }, .stdin = .pipe, .stdout = .inherit, .stderr = .inherit, }) catch return false; if (child.stdin) |sin| { var pbuf: [16 * 1024]u8 = undefined; var cw = sin.writer(io, &pbuf); cw.interface.writeAll(text) catch {}; // EPIPE if the user quits the pager early cw.interface.flush() catch {}; sin.close(io); child.stdin = null; } _ = child.wait(io) catch {}; return true;}
// ----------------------------------------------------------------------------// arg parsing// ----------------------------------------------------------------------------
fn parseArgs(args: std.process.Args) !Args { var it = args.iterate(); _ = it.skip(); // argv0 var r: Args = .{}; var have_query = false; while (it.next()) |arg| { if (eqAny(arg, "-h", "--help")) { r.help = true; } else if (eqAny(arg, "-V", "--version")) { r.ver = it.next() orelse return error.MissingValue; } else if (eqAny(arg, "-l", "--list")) { r.list = true; } else if (eqAny(arg, "-o", "--open")) { r.open = true; } else if (eqAny(arg, "-r", "--refresh")) { r.refresh = true; } else if (std.mem.eql(u8, arg, "--no-pager")) { r.no_pager = true; } else if (std.mem.eql(u8, arg, "--completions")) { r.completions = it.next() orelse return error.MissingValue; } else if (arg.len > 0 and arg[0] == '-') { return error.UnknownFlag; } else { if (have_query) return error.TooManyArgs; r.query = arg; have_query = true; } } return r;}
fn completionScript(shell: []const u8) ?[]const u8 { if (std.mem.eql(u8, shell, "zsh")) return zsh_completion; if (std.mem.eql(u8, shell, "bash")) return bash_completion; if (std.mem.eql(u8, shell, "fish")) return fish_completion; return null;}
// section names contain no spaces, so callers strip the list's indentation.// ends with `compdef` so the script works when sourced (source <(zigman ...)).const zsh_completion = \\#compdef zigman \\_zigman() { \\ local -a ids \\ ids=("${(@f)$(command zigman -l 2>/dev/null)}") \\ compadd -- ${(@)ids//[[:space:]]/} \\} \\compdef _zigman zigman \\;
const bash_completion = \\_zigman() { \\ local cur="${COMP_WORDS[COMP_CWORD]}" \\ COMPREPLY=($(compgen -W "$(command zigman -l 2>/dev/null)" -- "$cur")) \\} \\complete -F _zigman zigman \\;
const fish_completion = \\complete -c zigman -f -a "(command zigman -l 2>/dev/null | string trim)" \\;
// ----------------------------------------------------------------------------// langref loading: cache under $XDG_CACHE_HOME or ~/.cache/zigman// ----------------------------------------------------------------------------
const day_ns: i96 = 24 * 60 * 60 * std.time.ns_per_s;
/// Resolve the zigman cache directory per the XDG Base Directory spec:/// `$XDG_CACHE_HOME/zigman` when that variable is set to an absolute path,/// otherwise `$HOME/.cache/zigman`. Caller owns the returned path.fn cacheDir(a: std.mem.Allocator, env: Environ.Map) ![]u8 { if (env.get("XDG_CACHE_HOME")) |xdg| { if (xdg.len > 0 and xdg[0] == '/') return std.fmt.allocPrint(a, "{s}/zigman", .{xdg}); } const home = env.get("HOME") orelse return error.NoCacheHome; return std.fmt.allocPrint(a, "{s}/.cache/zigman", .{home});}
fn loadLangref(a: std.mem.Allocator, io: Io, dir: []const u8, ver: []const u8, refresh: bool) ![]u8 { const path = try std.fmt.allocPrint(a, "{s}/{s}.html", .{ dir, ver }); defer a.free(path);
const cwd = Io.Dir.cwd(); if (!refresh and cacheFresh(io, cwd, path, ver)) { if (cwd.readFileAlloc(io, path, a, .unlimited)) |cached| { return cached; } else |_| {} }
const url = try std.fmt.allocPrint(a, "https://ziglang.org/documentation/{s}/", .{ver}); defer a.free(url); const html = try fetch(a, io, url);
cwd.createDirPath(io, dir) catch {}; cwd.writeFile(io, .{ .sub_path = path, .data = html }) catch {}; return html;}
/// pinned versions are immutable, so any cache hit is fresh; `master` moves,/// so its cache is only fresh for a day.fn cacheFresh(io: Io, cwd: Io.Dir, path: []const u8, ver: []const u8) bool { const st = cwd.statFile(io, path, .{}) catch return false; if (!std.mem.eql(u8, ver, "master")) return true; const now = Io.Timestamp.now(io, .real).nanoseconds; return now - st.mtime.nanoseconds < day_ns;}
fn fetch(a: std.mem.Allocator, io: Io, url: []const u8) ![]u8 { var client: std.http.Client = .{ .allocator = a, .io = io }; defer client.deinit(); var body: Io.Writer.Allocating = .init(a); errdefer body.deinit(); const res = try client.fetch(.{ .location = .{ .url = url }, .response_writer = &body.writer }); if (res.status != .ok) return error.HttpStatus; return body.toOwnedSlice();}
fn eqAny(arg: []const u8, short: []const u8, long: []const u8) bool { return std.mem.eql(u8, arg, short) or std.mem.eql(u8, arg, long);}