download patch
commit c8fbee744ae4e6e681807513dbd19061b3692f56
Author: tri <tri@thac.loan>
Date: Wed Oct 1 11:59:35 2025 +0700
split repo page to 3 tabs: source, readme, commits
Readme tab is only present if a readme file exists.
Maybe look into some templating system next.
diff --git a/src/assets/style.css b/src/assets/style.css
index 01ceec8..c7171bf 100644
--- a/src/assets/style.css
+++ b/src/assets/style.css
:root {
+ --bg: white;
+ --fg: black;
+
--table-odd-row-bg: #eee;
--table-border-color: #ddd;
--table-row-hover-bg: papayawhip;
ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
"DejaVu Sans Mono", monospace;
+ background-color: var(--bg);
+ color: var(--fg);
font-family: var(--serif-fonts);
font-size: 100%;
}
details:open > summary {
}
}
+.repo-nav {
+ ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+ li {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ border: 1px solid transparent;
+
+ &.active {
+ border: 1px solid var(--fg);
+ border-bottom: 1px solid var(--bg);
+ z-index: 1;
+ }
+ }
+}
+.repo-content {
+ border: 1px solid var(--fg);
+ margin-top: -1px; /* so nav item's bottom border overlaps its border */
+ padding: 0 1rem;
+ width: fit-content;
+}
+
@media (prefers-color-scheme: dark) {
:root {
- background-color: #222;
- color: #ddd;
- font-family: var(--sans-serif-fonts);
+ --bg: #222;
+ --fg: #ddd;
--table-odd-row-bg: #333;
--table-border-color: #444;
details:open > summary {
--table-border-color: #ddd;
--pre-bg: #444;
--pre-border-color: #555;
+
+ font-family: var(--sans-serif-fonts);
}
a {
diff --git a/src/git.zig b/src/git.zig
index 55ed1e3..a86f685 100644
--- a/src/git.zig
+++ b/src/git.zig
const std = @import("std");
const mem = std.mem;
const fs = std.fs;
+const t = std.testing;
pub fn findGitDir(arena: mem.Allocator, dir: fs.Dir) ![]const u8 {
var proc = try std.process.Child.run(.{
pub fn readFileAlloc(arena: mem.Allocator, dir: fs.Dir, file_name: []const u8) !
return proc.stdout;
}
+
+pub const File = struct {
+ hash: []const u8,
+ size: u64,
+ path: []const u8,
+};
+
+pub const Walker = struct {
+ remaining: []const u8,
+
+ pub fn next(self: *Walker) !?File {
+ if (self.remaining.len == 0) return null;
+
+ const hashEndIdx = mem.indexOfScalar(u8, self.remaining, ' ').?;
+ const hash = self.remaining[0..hashEndIdx];
+
+ const sizeEndIdx = mem.indexOfScalarPos(u8, self.remaining, hashEndIdx + 1, ' ').?;
+ const sizeStr = self.remaining[hashEndIdx + 1 .. sizeEndIdx];
+ const size =
+ if (mem.eql(u8, sizeStr, "-")) // is a symlink
+ 0
+ else
+ try std.fmt.parseUnsigned(u64, sizeStr, 10);
+
+ const pathEndIdx = mem.indexOfScalarPos(u8, self.remaining, sizeEndIdx + 1, '\x00').?;
+ const path = self.remaining[sizeEndIdx + 1 .. pathEndIdx];
+
+ self.remaining = self.remaining[pathEndIdx + 1 ..];
+ return File{ .hash = hash, .size = size, .path = path };
+ }
+};
+
+pub fn walkTree(arena: mem.Allocator, dir: fs.Dir) !Walker {
+ var proc = try std.process.Child.run(.{
+ .allocator = arena,
+ .cwd_dir = dir,
+ .max_output_bytes = 1024 * 1024 * 64,
+ .argv = &.{
+ "git",
+ "ls-tree",
+ "HEAD",
+ "-r",
+ "-z",
+ "--format",
+ "%(objectname) %(objectsize) %(path)",
+ },
+ });
+ return Walker{ .remaining = proc.stdout };
+}
+
+//test "walkTree" {
+// var arena_impl = std.heap.ArenaAllocator.init(t.allocator_instance.allocator());
+// defer arena_impl.deinit();
+// const arena = arena_impl.allocator();
+//
+// var dir = try fs.cwd().openDir("demo/khoe", .{});
+// defer dir.close();
+//
+// var walker = try walkTree(arena, dir);
+// try t.expectEqualDeep(
+// File{
+// .hash = "f216b65cf70beaeda50910cf5c778d466d297bad",
+// .size = 33,
+// .path = ".gitignore",
+// },
+// (try walker.next()).?,
+// );
+//}
diff --git a/src/main.zig b/src/main.zig
index 2354c4c..e310cc6 100644
--- a/src/main.zig
+++ b/src/main.zig
const html = @import("html.zig");
const markdown = @import("markdown.zig");
const web_path = "_";
-const assets_path = web_path ++ "/_khoe-hang";
+const web_assets_path = web_path ++ "/_khoe-hang";
+const web_readme_path = "readme.html";
+const web_commits_path = "commits.html";
const Mode = union(enum) {
all: void,
pub fn main() !u8 {
.last_commit_time = if (commits.len == 0) "" else try arena.dupe(u8, commits[0].time),
});
- try writeRepoPage(.{
+ try processRepo(&.{
.arena = repo_arena,
.site_url = site_url,
.repo_name = entry.name,
pub fn main() !u8 {
try writeHomePage(arena, target_dir, repo_summaries.items);
- var assets_dir = try target_dir.makeOpenPath(assets_path, .{});
+ var assets_dir = try target_dir.makeOpenPath(web_assets_path, .{});
inline for (.{ "style.css", "script.js" }) |asset_name| {
try assets_dir.writeFile(.{
.data = @embedFile("assets/" ++ asset_name),
const RepoArgs = struct {
git_dir: fs.Dir,
};
-pub fn writeRepoPage(args: RepoArgs) !void {
+pub fn processRepo(args: *const RepoArgs) !void {
const arena = args.arena;
- const site_url = args.site_url;
- const repo_name = args.repo_name;
- const target_dir = args.target_dir;
- const in_repo_dir = args.in_repo_dir;
- const commits = args.commits;
-
- var buf: [1024]u8 = undefined;
- var out_repo_dir = try target_dir.makeOpenPath(
- try std.fmt.bufPrint(&buf, "{s}/{s}", .{ web_path, repo_name }),
+
+ var out_repo_dir = try args.target_dir.makeOpenPath(
+ try std.fmt.allocPrint(arena, "{s}/{s}", .{ web_path, args.repo_name }),
.{},
);
defer out_repo_dir.close();
+ const maybe_readme_path = try git.findReadme(arena, args.in_repo_dir);
+
+ try writeRepoPage(args, out_repo_dir, maybe_readme_path);
+ try writeReadmePage(args, out_repo_dir, maybe_readme_path);
+ try writeCommitsPage(args, out_repo_dir, maybe_readme_path);
+}
+
+pub fn writeRepoPage(
+ args: *const RepoArgs,
+ out_repo_dir: fs.Dir,
+ maybe_readme_filename: ?[]const u8,
+) !void {
+ const arena = args.arena;
+
var file = try out_repo_dir.createFile("index.html", .{});
defer file.close();
var buf2: [1024 * 16]u8 = undefined;
- var writer = file.writer(&buf2);
- try writer.interface.print(
- \\<!doctype html>
- \\<html lang="en">
- \\ <head>
- \\ <meta charset="utf-8" />
- \\ <title>{0s} | Khoe</title>
- \\ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- \\ <link rel="stylesheet" href="/_/_khoe-hang/style.css">
- \\ <script src="/_/_khoe-hang/script.js"></script>
- \\ </head>
- \\ <body>
- \\ <header>
- \\ <div id="breadcrumbs">
- \\ /<a href="/">repos</a>/<h1>{0s}</h1>/
- \\ </div>
- \\ </header>
- \\ <p style="margin:0; font-style:italic;">{4s}</p>
- \\ <p style="margin:0">{5s} | {1d} commits</p>
+ var file_writer = file.writer(&buf2);
+ const writer = &file_writer.interface;
+
+ try writer.print(repo_html_start, .{
+ args.repo_name,
+ args.commits.len,
+ args.site_url,
+ if (mem.eql(u8, args.git_dir_path, "."))
+ args.repo_name
+ else
+ try std.fmt.allocPrint(arena, "{s}/{s}", .{ args.repo_name, args.git_dir_path }),
+ try html.escapeAlloc(arena, args.description),
+ try utils.humanReadableSize(arena, try utils.dirSize(arena, args.git_dir)),
+ });
+
+ try writer.print(
+ \\<nav class="repo-nav">
+ \\ <ul>
+ \\ <li class="active"><a href="./">source</a></li>
+ \\ {0s}
+ \\ <li><a href="{1s}">commits</a></li>
+ \\ </ul>
+ \\</nav>
+ \\
+ ,
+ .{
+ if (maybe_readme_filename) |readme_filename|
+ try std.fmt.allocPrint(
+ arena,
+ "<li><a href=\"{s}\">{s}</a></li>",
+ .{ web_readme_path, readme_filename },
+ )
+ else
+ "",
+ web_commits_path,
+ },
+ );
+
+ try writer.writeAll(
+ \\<div class="repo-content">
\\
- \\ <pre class="git-clone-command">git clone {2s}/{3s}</pre>
+ \\ <div style="overflow-x:auto; padding-bottom:1rem">
+ \\ <table style="margin-top: 1rem">
+ \\ <thead>
+ \\ <tr>
+ \\ <th>name</th>
+ \\ <th>size</th>
+ \\ </tr>
+ \\ </thead>
+ \\ <tbody>
+ );
+
+ var treeWalker = try git.walkTree(arena, args.in_repo_dir);
+ while (try treeWalker.next()) |src_file| {
+ try writer.print(
+ \\<tr>
+ \\ <td>{0s}</td>
+ \\ <td style="opacity:0.4">{1s}</td>
+ \\</tr>
+ \\
+ , .{
+ src_file.path,
+ try utils.humanReadableSize(arena, src_file.size),
+ });
+ }
+
+ try writer.writeAll(
+ \\ </tbody>
+ \\ </table>
+ \\ </div>
+ \\</div>
\\
- , .{
- repo_name,
- commits.len,
- site_url,
+ );
+ try writer.writeAll(repo_html_end);
+ try writer.flush();
+}
+
+pub fn writeReadmePage(
+ args: *const RepoArgs,
+ out_repo_dir: fs.Dir,
+ maybe_readme_filename: ?[]const u8,
+) !void {
+ if (maybe_readme_filename == null) return;
+ const readme_filename = maybe_readme_filename.?;
+
+ const arena = args.arena;
+
+ var readme_file = try out_repo_dir.createFile(web_readme_path, .{});
+ defer readme_file.close();
+
+ var writerBuf: [4096]u8 = undefined;
+ var file_writer = readme_file.writer(&writerBuf);
+ const writer = &file_writer.interface;
+
+ try writer.print(repo_html_start, .{
+ args.repo_name,
+ args.commits.len,
+ args.site_url,
if (mem.eql(u8, args.git_dir_path, "."))
- repo_name
+ args.repo_name
else
- try std.fmt.bufPrint(&buf, "{s}/{s}", .{ repo_name, args.git_dir_path }),
+ try std.fmt.allocPrint(arena, "{s}/{s}", .{ args.repo_name, args.git_dir_path }),
try html.escapeAlloc(arena, args.description),
try utils.humanReadableSize(arena, try utils.dirSize(arena, args.git_dir)),
});
- // If there's a readme file, include it in repo index:
- if (try git.findReadme(arena, in_repo_dir)) |readme_path| {
- //println(" {s}", .{readme_path});
- const readme_text = try git.readFileAlloc(arena, in_repo_dir, readme_path);
- const is_markdown = (ascii.endsWithIgnoreCase(readme_path, ".md") or
- ascii.endsWithIgnoreCase(readme_path, ".markdown"));
+ try writer.print(
+ \\<nav class="repo-nav">
+ \\ <ul>
+ \\ <li><a href="./">source</a></li>
+ \\ <li class="active"><a href="{0s}">{2s}</a></li>
+ \\ <li><a href="{1s}">commits</a></li>
+ \\ </ul>
+ \\</nav>
+ \\
+ ,
+ .{
+ web_readme_path,
+ web_commits_path,
+ readme_filename,
+ },
+ );
- try writer.interface.print(
- \\<details class="readme-container">
- \\ <summary>{0s}</summary>
- ,
- .{readme_path},
+ const readme_text = try git.readFileAlloc(arena, args.in_repo_dir, readme_filename);
+ const is_markdown = (ascii.endsWithIgnoreCase(readme_filename, ".md") or
+ ascii.endsWithIgnoreCase(readme_filename, ".markdown"));
+
+ if (is_markdown) {
+ const readme_html = try markdown.toHtml(arena, readme_text);
+ try writer.writeAll(
+ \\<div class="repo-content readme-content">
+ );
+ try writer.writeAll(readme_html);
+ try writer.writeAll(
+ \\</div>
);
+ } else {
+ try writer.writeAll(
+ \\<pre class="repo-content readme-content pre-wrap">
+ );
+ try html.escape(
+ writer,
+ mem.trimEnd(u8, readme_text, "\n"),
+ );
+ try writer.writeAll(
+ \\</pre>
+ );
+ }
- if (is_markdown) {
- const readme_html = try markdown.toHtml(arena, readme_text);
- try writer.interface.writeAll(
- \\<div class="readme-content">
- );
- try writer.interface.writeAll(readme_html);
- try writer.interface.writeAll(
- \\</div>
- );
- } else {
- try writer.interface.writeAll(
- \\<pre class="readme-content pre-wrap">
- );
- try html.escape(
- &writer.interface,
- mem.trimEnd(u8, readme_text, "\n"),
- );
- try writer.interface.writeAll(
- \\</pre>
- );
- }
+ try writer.flush();
+}
- try writer.interface.print(
- \\</details>
- , .{});
- }
+pub fn writeCommitsPage(
+ args: *const RepoArgs,
+ out_repo_dir: fs.Dir,
+ maybe_readme_filename: ?[]const u8,
+) !void {
+ const arena = args.arena;
- try writer.interface.print(
+ var commits_file = try out_repo_dir.createFile(web_commits_path, .{});
+ defer commits_file.close();
+
+ var writerBuf: [4096]u8 = undefined;
+ var file_writer = commits_file.writer(&writerBuf);
+ const writer = &file_writer.interface;
+
+ try writer.print(repo_html_start, .{
+ args.repo_name,
+ args.commits.len,
+ args.site_url,
+ if (mem.eql(u8, args.git_dir_path, "."))
+ args.repo_name
+ else
+ try std.fmt.allocPrint(arena, "{s}/{s}", .{ args.repo_name, args.git_dir_path }),
+ try html.escapeAlloc(arena, args.description),
+ try utils.humanReadableSize(arena, try utils.dirSize(arena, args.git_dir)),
+ });
+
+ try writer.print(
+ \\<nav class="repo-nav">
+ \\ <ul>
+ \\ <li><a href="./">source</a></li>
+ \\ {0s}
+ \\ <li class="active"><a href="{1s}">commits</a></li>
+ \\ </ul>
+ \\</nav>
+ \\
+ ,
+ .{
+ if (maybe_readme_filename) |readme_filename|
+ try std.fmt.allocPrint(
+ arena,
+ "<li><a href=\"{s}\">{s}</a></li>",
+ .{ web_readme_path, readme_filename },
+ )
+ else
+ "",
+ web_commits_path,
+ },
+ );
+
+ try writer.writeAll(
+ \\<div class="repo-content">
+ \\
+ );
+
+ try writer.print(
\\ <div style="overflow-x:auto; padding-bottom:1rem">
- \\ <table style="margin-top: 1.5rem">
+ \\ <table style="margin-top: 1rem">
\\ <thead>
\\ <tr>
\\ <th>hash</th>
pub fn writeRepoPage(args: RepoArgs) !void {
\\
, .{});
- for (commits) |cmt| {
+ for (args.commits) |cmt| {
const escaped_subject = try html.escapeAlloc(
arena,
cmt.subject[0..@min(cmt.subject.len, 80)],
);
const escaped_author = try html.escapeAlloc(arena, cmt.author_name);
- try writer.interface.print(
+ try writer.print(
\\<tr>
\\ <td class="monospace" title="commit diff coming Soon™">{0s}</td>
\\ <td>{1s}{2s}</td>
pub fn writeRepoPage(args: RepoArgs) !void {
);
}
- try writer.interface.writeAll(
- \\ </tbody>
- \\ </table>
- \\ </div>
- \\ </body>
- \\</html>
+ try writer.writeAll(
+ \\</div>
+ \\
);
-
- try writer.interface.flush();
+ try writer.writeAll(repo_html_end);
}
+const repo_html_start =
+ \\<!doctype html>
+ \\<html lang="en">
+ \\ <head>
+ \\ <meta charset="utf-8" />
+ \\ <title>{0s} | Khoe</title>
+ \\ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ \\ <link rel="stylesheet" href="/_/_khoe-hang/style.css">
+ \\ <script src="/_/_khoe-hang/script.js"></script>
+ \\ </head>
+ \\ <body>
+ \\ <header>
+ \\ <div id="breadcrumbs">
+ \\ /<a href="/">repos</a>/<h1>{0s}</h1>/
+ \\ </div>
+ \\ </header>
+ \\ <p style="margin:0; font-style:italic;">{4s}</p>
+ \\ <p style="margin:0">{5s} | {1d} commits</p>
+ \\
+ \\ <pre class="git-clone-command">git clone {2s}/{3s}</pre>
+ \\
+;
+
+const repo_html_end =
+ \\ </body>
+ \\</html>
+ \\
+;
+
test "all" {
_ = @import("html.zig");
_ = @import("markdown.zig");