dotfiles/scripts/prompt/src/prompt.zig
2024-04-14 22:06:02 +02:00

380 lines
12 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const std = @import("std");
const at = @import("ansi-term");
const known_folders = @import("known-folders");
const ffi = @import("ffi.zig");
const checkGitError = ffi.checkGitError;
const c = ffi.c;
const FishMode = @import("FishMode.zig");
const Style = at.style.Style;
const Color = at.style.Color;
const symbols = struct {
const left_separator = "";
const right_separator = "";
const top_left = "";
const bottom_left = "";
const path_separator = "";
const top_end = "";
const staged = "";
const unstaged = "";
const home = "";
const root = "";
const watch = "";
const jobs = "";
const nix = "󱄅";
};
pub const Options = struct {
status: i16,
mode: FishMode,
duration: u32,
jobs: u32,
nix_name: ?[]const u8,
};
pub fn render(writer: anytype, options: Options) !void {
var renderer = Renderer(@TypeOf(writer)){
.last_style = null,
.writer = writer,
.options = options,
};
try renderer.render();
}
fn Renderer(comptime Writer: type) type {
return struct {
last_style: ?Style,
writer: Writer,
options: Options,
const Self = @This();
pub fn render(self: *Self) !void {
const left_color: Color = if (self.options.status == 0) .Green else .Red;
try self.setStyle(.{ .foreground = left_color });
try self.writer.writeAll(symbols.top_left);
try self.setStyle(.{ .background = left_color });
try self.renderDuration();
try self.renderJobs();
try self.renderNix();
try self.renderCwd();
self.renderGit() catch |err| {
switch (err) {
error.GitError => {}, // git error will be printed
else => return err,
}
};
try self.writer.writeAll(" ");
try self.setStyle(.{ .foreground = self.last_style.?.background });
try self.writer.writeAll(symbols.top_end ++ "\n");
try self.setStyle(.{ .foreground = left_color });
try self.writer.writeAll(symbols.bottom_left);
try self.setStyle(.{ .foreground = left_color, .background = left_color });
try self.writer.writeAll(" ");
const mode_color = self.options.mode.getColor();
try self.setStyle(.{
.foreground = left_color,
.background = mode_color,
.font_style = .{ .bold = true },
});
try self.writer.writeAll(symbols.right_separator ++ " ");
try self.setStyle(.{
.foreground = .Black,
.background = mode_color,
.font_style = .{ .bold = true },
});
try self.writer.writeAll(self.options.mode.getText());
try self.writer.writeAll(" ");
try self.setStyle(.{ .foreground = mode_color });
try self.writer.writeAll(symbols.right_separator ++ " ");
try self.setStyle(.{});
}
fn renderDuration(self: *Self) !void {
if (self.options.duration < 2 * std.time.ms_per_s)
return;
try self.drawLeftSep(.Blue);
try self.setStyle(.{
.background = .Blue,
.foreground = .Black,
.font_style = .{ .bold = true },
});
try self.writer.writeAll(" ");
try self.writer.writeAll(symbols.watch);
try self.writer.writeAll(" ");
var total = self.options.duration;
const hours = total / std.time.ms_per_hour;
total -= hours * std.time.ms_per_hour;
const minutes = total / std.time.ms_per_min;
total -= minutes * std.time.ms_per_min;
const seconds = total / std.time.ms_per_s;
total -= seconds * std.time.ms_per_s;
const millis = total;
if (hours > 0) {
try self.writer.print("{}h ", .{hours});
}
if (minutes > 0 or hours > 0) {
try self.writer.print("{}min ", .{minutes});
}
if (seconds > 0 or minutes > 0 or hours > 0) {
try self.writer.print("{}s ", .{seconds});
}
if (millis > 0 or seconds > 0 or minutes > 0 or hours > 0) {
try self.writer.print("{}ms", .{millis});
}
}
fn renderJobs(self: *Self) !void {
if (self.options.jobs <= 0)
return;
try self.drawLeftSep(.Cyan);
try self.setStyle(.{ .background = .Cyan, .foreground = .Black });
try self.writer.print(" {s} {}", .{ symbols.jobs, self.options.jobs });
}
fn renderNix(self: *Self) !void {
if (self.options.nix_name) |name| {
try self.drawLeftSep(.Blue);
try self.setStyle(.{ .background = .Blue, .foreground = .Black});
try self.writer.print(" {s} {s}", .{symbols.nix, name });
}
}
fn renderCwd(self: *Self) !void {
var cwd_buf: [512]u8 = undefined;
const cwd = try std.posix.getcwd(&cwd_buf);
const home_path = (try known_folders.getPath(std.heap.c_allocator, .home));
try self.drawLeftSep(.{ .Yellow = {} });
var written_path = false;
if (home_path) |home| {
defer std.heap.c_allocator.free(home);
if (std.mem.startsWith(u8, cwd, home)) {
try self.setStyle(.{
.background = .Yellow,
.foreground = .Red,
});
try self.writer.writeAll(" " ++ symbols.home);
if (home.len != cwd.len) {
try self.renderPathSep();
try self.renderPath(cwd[(home.len + 1)..]);
}
written_path = true;
}
}
// write root-relative path
if (!written_path) {
try self.setStyle(.{
.background = .Yellow,
.foreground = .Red,
});
try self.writer.writeAll(" " ++ symbols.root);
// don't render separators when we're in /
if (cwd.len > 1) {
try self.renderPathSep();
try self.renderPath(cwd[1..]);
}
}
}
fn renderPath(self: *Self, path: []const u8) !void {
for (path) |byte|
if (byte == '/')
try self.renderPathSep()
else
try self.writer.writeByte(byte);
}
fn renderPathSep(self: *Self) !void {
try self.setStyle(.{
.background = self.last_style.?.background,
.foreground = .Blue,
});
try self.writer.writeAll(" " ++ symbols.path_separator ++ " ");
try self.setStyle(.{
.background = self.last_style.?.background,
.foreground = .Black,
});
}
fn renderGit(self: *Self) !void {
try checkGitError(c.git_libgit2_init());
defer _ = c.git_libgit2_shutdown();
var path_buf = std.mem.zeroes(c.git_buf);
defer c.git_buf_dispose(&path_buf);
if (c.git_repository_discover(&path_buf, ".", 1, null) < 0)
// no repo found
return;
var repo: ?*c.git_repository = null;
try checkGitError(c.git_repository_open(&repo, path_buf.ptr));
defer c.git_repository_free(repo);
var head: ?*c.git_reference = null;
const head_err = c.git_repository_head(&head, repo);
// branch with no commits
if (head_err == c.GIT_EUNBORNBRANCH) {
const bg = Color{ .Grey = 200 };
try self.drawLeftSep(bg);
try self.setStyle(.{
.background = bg,
.foreground = .Black,
.font_style = .{ .bold = true },
});
try self.writer.writeAll(" <new branch>");
return;
}
defer c.git_reference_free(head);
const name = c.git_reference_shorthand(head);
var status_options: c.git_status_options = undefined;
try checkGitError(c.git_status_options_init(
&status_options,
c.GIT_STATUS_OPTIONS_VERSION,
));
status_options.flags =
c.GIT_STATUS_OPT_INCLUDE_UNTRACKED |
c.GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
var status_list: ?*c.git_status_list = null;
try checkGitError(c.git_status_list_new(
&status_list,
repo,
&status_options,
));
var counts = GitStatusCounts{};
try checkGitError(c.git_status_foreach_ext(
repo,
&status_options,
gitStatusCb,
&counts,
));
// now render all that info!
const ref_bg = counts.getColor();
try self.drawLeftSep(ref_bg);
try self.setStyle(.{
.background = ref_bg,
.foreground = .Black,
.font_style = .{ .bold = true },
});
// using print here because name is a cstring
try self.writer.print(" {s}", .{name});
if (counts.staged > 0) {
try self.drawLeftSep(.Green);
try self.setStyle(.{
.background = .Green,
.foreground = .Black,
});
try self.writer.print(" {}{s}", .{ counts.staged, symbols.staged });
}
if (counts.unstaged > 0) {
try self.drawLeftSep(.Magenta);
try self.setStyle(.{
.background = .Magenta,
.foreground = .Black,
});
try self.writer.print(" {}{s}", .{ counts.unstaged, symbols.unstaged });
}
}
fn setStyle(self: *Self, style: Style) !void {
try at.format.updateStyle(self.writer, style, self.*.last_style);
self.last_style = style;
}
fn drawLeftSep(self: *Self, new_bg: Color) !void {
try self.writer.writeAll(" ");
try self.setStyle(.{
.background = self.last_style.?.background,
.foreground = new_bg,
});
try self.writer.writeAll(symbols.left_separator);
}
};
}
fn gitStatusCb(
_: [*c]const u8,
flags: c_uint,
counts_: ?*anyopaque,
) callconv(.C) c_int {
const staged_flags =
c.GIT_STATUS_INDEX_NEW |
c.GIT_STATUS_INDEX_MODIFIED |
c.GIT_STATUS_INDEX_DELETED |
c.GIT_STATUS_INDEX_RENAMED |
c.GIT_STATUS_INDEX_TYPECHANGE;
const unstaged_flags =
c.GIT_STATUS_WT_NEW |
c.GIT_STATUS_WT_MODIFIED |
c.GIT_STATUS_WT_DELETED |
c.GIT_STATUS_WT_RENAMED |
c.GIT_STATUS_WT_TYPECHANGE;
const counts: *GitStatusCounts = @ptrCast(@alignCast(counts_));
if (flags & staged_flags > 0)
counts.staged += 1;
if (flags & unstaged_flags > 0)
counts.unstaged += 1;
return 0;
}
const GitStatusCounts = struct {
staged: u32 = 0,
unstaged: u32 = 0,
pub fn getColor(self: *GitStatusCounts) Color {
const has_staged = self.staged > 0;
const has_unstaged = self.unstaged > 0;
return if (!has_staged and !has_unstaged)
.Blue
else if (has_staged and has_unstaged)
.Magenta
else if (has_staged)
.Green
else
.{ .Grey = 200 };
}
};