// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! grebe: a history browser for git. //! //! `grebe [revision arguments] [-- paths]` takes what `gitk` takes, and //! hands it to git as it is. const std = @import("std"); const builtin = @import("builtin"); const build_options = @import("build_options"); const aerie = @import("aerie"); const App = @import("App.zig"); const l10n = @import("l10n.zig"); const load_config = @import("config/load.zig"); /// Where grebe looks for the repository, in place of the current /// directory: what a file manager opens a folder with. const repository_option = "--repository="; pub fn main(init: std.process.Init) !u8 { const gpa = init.gpa; const io = init.io; const arena = init.arena.allocator(); const args = try init.minimal.args.toSlice(arena); // What goes wrong before the window opens, said when grebe stops. var failure: std.Io.Writer.Allocating = .init(arena); const stderr = &failure.writer; defer report(io, arena, failure.written()); try l10n.init(gpa, init.environ_map, null); defer l10n.deinit(); // `--repository=DIR` is grebe's, and goes no further; everything else // is git's. var git_args: std.ArrayList([]const u8) = .empty; var repository: ?[]const u8 = null; var past_options = false; for (args[1..]) |a| { if (!past_options and std.mem.startsWith(u8, a, repository_option)) { repository = a[repository_option.len..]; continue; } if (std.mem.eql(u8, a, "--")) past_options = true; try git_args.append(arena, a); } for (git_args.items) |a| { if (std.mem.eql(u8, a, "--")) break; if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { var stdout_buf: [1024]u8 = undefined; var stdout_file = std.Io.File.stdout().writer(io, &stdout_buf); try stdout_file.interface.print("{s}\n", .{l10n.text("usage")}); try stdout_file.interface.flush(); return 0; } if (std.mem.eql(u8, a, "--version")) { var stdout_buf: [64]u8 = undefined; var stdout_file = std.Io.File.stdout().writer(io, &stdout_buf); try stdout_file.interface.print("grebe {s}\n", .{build_options.version}); try stdout_file.interface.flush(); return 0; } } // Before anything else runs: git, and every thread, look from there. if (repository) |dir| std.process.setCurrentPath(io, dir) catch |err| { try l10n.write(stderr, "cli-repository-failed", &.{ l10n.str("path", dir), l10n.str("error", @errorName(err)) }); try stderr.writeByte('\n'); return 1; }; // A repository, first: said on the terminal it was started from, as // git says it, rather than in a window with nothing to show. const top = std.process.run(arena, io, .{ .argv = &.{ "git", "rev-parse", "--show-toplevel" }, }) catch |err| { try l10n.write(stderr, "cli-git-failed", &.{l10n.str("error", @errorName(err))}); try stderr.writeByte('\n'); return 1; }; const ok = switch (top.term) { .exited => |code| code == 0, else => false, }; if (!ok) { try stderr.writeAll(top.stderr); return 1; } const top_path = std.mem.trimEnd(u8, top.stdout, "\n"); const name = std.fs.path.basename(top_path); // The desktop's settings, for the configuration to see, and then the // configuration: a mistake in it is said on the terminal, as git's // are. const desktop = aerie.desktop.Watcher.start(gpa, io, init.environ_map); var desktop_owned = desktop != null; defer if (desktop_owned) desktop.?.destroy(); var settings: aerie.desktop.Settings = .init(gpa); var seen: u64 = 0; if (desktop) |d| if (try d.takeIfNewer(gpa, 0)) |latest| { settings.deinit(); settings = latest.settings; seen = latest.generation; }; var problem: load_config.Problem = .{}; var loaded = load_config.load(gpa, io, init.environ_map, top_path, settings, &problem) catch |err| switch (err) { error.OutOfMemory => return err, else => { try l10n.write(stderr, "cli-config-failed", &.{l10n.str("problem", try std.fmt.allocPrint(arena, "{f}", .{&problem}))}); try stderr.writeByte('\n'); return 1; }, }; var loaded_owned = true; defer if (loaded_owned) loaded.deinit(gpa); if (loaded.config().language) |language| try l10n.init(gpa, init.environ_map, language); const title = try l10n.format(arena, "window-title", &.{l10n.str("repository", name)}); // The app takes the configuration and the settings, whatever happens. loaded_owned = false; desktop_owned = false; const app = App.create(gpa, io, init.environ_map, .{ .args = git_args.items, .title = title, .snapshot = init.environ_map.get("GREBE_SNAPSHOT"), .config = loaded, .desktop = desktop, .desktop_seen = seen, }) catch |err| { try l10n.write(stderr, "cli-failed", &.{l10n.str("error", @errorName(err))}); try stderr.writeByte('\n'); return 1; }; defer app.destroy(); try app.run(); return 0; } /// Says why grebe stopped before its window opened: on the terminal, and /// on Windows in a message box as well, since one started from Explorer /// has no terminal to say it on. fn report(io: std.Io, arena: std.mem.Allocator, text: []const u8) void { if (text.len == 0) return; var buf: [1024]u8 = undefined; var w = std.Io.File.stderr().writer(io, &buf); w.interface.writeAll(text) catch {}; w.interface.flush() catch {}; if (builtin.os.tag == .windows and !builtin.is_test) { const win32 = @import("win32").everything; const message = std.unicode.utf8ToUtf16LeAllocZ(arena, std.mem.trimEnd(u8, text, "\r\n")) catch return; _ = win32.MessageBoxW(null, message, std.unicode.utf8ToUtf16LeStringLiteral("grebe"), .{ .ICONHAND = 1 }); } } test { _ = @import("App.zig"); _ = @import("l10n.zig"); _ = @import("Clock.zig"); _ = @import("history/History.zig"); _ = @import("history/Loader.zig"); _ = @import("history/git/LogParser.zig"); _ = @import("history/git/Relate.zig"); _ = @import("history/RelationsCache.zig"); _ = @import("history/git/Log.zig"); _ = @import("history/git/Show.zig"); _ = @import("history/git/ShowParser.zig"); _ = @import("history/git/Local.zig"); _ = @import("history/git/reference.zig"); _ = @import("history/git/Refs.zig"); _ = @import("ui/Dialog.zig"); _ = @import("ui/Menu.zig"); _ = @import("find/Query.zig"); _ = @import("history/git/Find.zig"); _ = @import("ui/TextField.zig"); _ = @import("ui/CommitList.zig"); _ = @import("graph/Layout.zig"); _ = @import("ui/Document.zig"); _ = @import("ui/Panes.zig"); _ = @import("ui/theme.zig"); _ = @import("ui/Scrollbar.zig"); _ = @import("testing/fixture.zig"); _ = @import("config/Config.zig"); _ = @import("config/load.zig"); _ = @import("input/bindings.zig"); }