Skip to content

Commit 4bf0904

Browse files
committed
init
0 parents  commit 4bf0904

9 files changed

+445
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.zig-cache
2+
zig-out
3+
**/*.*
4+
!**/*.zig
5+
!init.sh
6+
!.gitignore
7+
!LICENSE
8+
!README.md

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 David Bushell
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.

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ⚡ Zigbar
2+
3+
Zigbar is my personal ZSH prompt written in [Zig](https://ziglang.org).
4+
5+
Inspired by [Starship](https://github.com/starship/starship) and [Pure](https://github.com/sindresorhus/pure).
6+
7+
🚧 Under construction!
8+
9+
* * *
10+
11+
[MIT License](/LICENSE) | Copyright © 2025 [David Bushell](https://dbushell.com)

build.zig

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const std = @import("std");
2+
3+
// Although this function looks imperative, note that its job is to
4+
// declaratively construct a build graph that will be executed by an external
5+
// runner.
6+
pub fn build(b: *std.Build) void {
7+
// Standard target options allows the person running `zig build` to choose
8+
// what target to build for. Here we do not override the defaults, which
9+
// means any target is allowed, and the default is native. Other options
10+
// for restricting supported target set are available.
11+
const target = b.standardTargetOptions(.{});
12+
13+
// Standard optimization options allow the person running `zig build` to select
14+
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
15+
// set a preferred release mode, allowing the user to decide how to optimize.
16+
const optimize = b.standardOptimizeOption(.{});
17+
18+
// const lib = b.addStaticLibrary(.{
19+
// .name = "Zigbar",
20+
// // In this case the main source file is merely a path, however, in more
21+
// // complicated build scripts, this could be a generated file.
22+
// .root_source_file = b.path("src/root.zig"),
23+
// .target = target,
24+
// .optimize = optimize,
25+
// });
26+
27+
// // This declares intent for the library to be installed into the standard
28+
// // location when the user invokes the "install" step (the default step when
29+
// // running `zig build`).
30+
// b.installArtifact(lib);
31+
32+
const exe = b.addExecutable(.{
33+
.name = "Zigbar",
34+
.root_source_file = b.path("src/main.zig"),
35+
.target = target,
36+
.optimize = optimize,
37+
});
38+
39+
// This declares intent for the executable to be installed into the
40+
// standard location when the user invokes the "install" step (the default
41+
// step when running `zig build`).
42+
b.installArtifact(exe);
43+
44+
// This *creates* a Run step in the build graph, to be executed when another
45+
// step is evaluated that depends on it. The next line below will establish
46+
// such a dependency.
47+
const run_cmd = b.addRunArtifact(exe);
48+
49+
// By making the run step depend on the install step, it will be run from the
50+
// installation directory rather than directly from within the cache directory.
51+
// This is not necessary, however, if the application depends on other installed
52+
// files, this ensures they will be present and in the expected location.
53+
run_cmd.step.dependOn(b.getInstallStep());
54+
55+
// This allows the user to pass arguments to the application in the build
56+
// command itself, like this: `zig build run -- arg1 arg2 etc`
57+
if (b.args) |args| {
58+
run_cmd.addArgs(args);
59+
}
60+
61+
// This creates a build step. It will be visible in the `zig build --help` menu,
62+
// and can be selected like this: `zig build run`
63+
// This will evaluate the `run` step rather than the default, which is "install".
64+
const run_step = b.step("run", "Run the app");
65+
run_step.dependOn(&run_cmd.step);
66+
67+
// Creates a step for unit testing. This only builds the test executable
68+
// but does not run it.
69+
// const lib_unit_tests = b.addTest(.{
70+
// .root_source_file = b.path("src/root.zig"),
71+
// .target = target,
72+
// .optimize = optimize,
73+
// });
74+
75+
// const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
76+
77+
const exe_unit_tests = b.addTest(.{
78+
.root_source_file = b.path("src/main.zig"),
79+
.target = target,
80+
.optimize = optimize,
81+
});
82+
83+
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
84+
85+
// Similar to creating the run step earlier, this exposes a `test` step to
86+
// the `zig build --help` menu, providing a way for the user to request
87+
// running the unit tests.
88+
const test_step = b.step("test", "Run unit tests");
89+
// test_step.dependOn(&run_lib_unit_tests.step);
90+
test_step.dependOn(&run_exe_unit_tests.step);
91+
}

init.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
setopt promptsubst
2+
3+
PROMPT='$('${0:a:h}'/zig-out/bin/Zigbar prompt)'

src/Context.zig

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
const std = @import("std");
2+
const TTY = @import("./TTY.zig");
3+
const Allocator = std.mem.Allocator;
4+
const AutoHashMap = std.AutoHashMap;
5+
const Dir = std.fs.Dir;
6+
const eql = std.mem.eql;
7+
const extension = std.fs.path.extension;
8+
const isDigit = std.ascii.isDigit;
9+
10+
/// Limit number of parent directories to scan (inclusive of current directory)
11+
const max_scan_depth = 10;
12+
13+
pub const Prop = enum {
14+
bun,
15+
deno,
16+
docker,
17+
node,
18+
rust,
19+
zig,
20+
21+
/// Nerd font unicode characters
22+
pub fn symbol(prop: Prop) []const u8 {
23+
return switch (prop) {
24+
.bun => "",
25+
.deno => "",
26+
.docker => "󰡨",
27+
.node => "󰎙",
28+
.rust => "󱘗",
29+
.zig => "",
30+
};
31+
}
32+
33+
/// Spawn child process to get the version string.
34+
/// If successful, caller owns result.
35+
pub fn version(prop: Prop, allocator: Allocator) !?[]const u8 {
36+
if (prop.versionArgv()) |argv| {
37+
const result = std.process.Child.run(.{
38+
.allocator = allocator,
39+
.argv = argv,
40+
}) catch return null;
41+
if (result.term == .Exited) {
42+
allocator.free(result.stderr);
43+
return result.stdout;
44+
}
45+
allocator.free(result.stderr);
46+
allocator.free(result.stdout);
47+
}
48+
return null;
49+
}
50+
51+
/// Get the version command arguments
52+
pub fn versionArgv(prop: Prop) ?[]const []const u8 {
53+
return switch (prop) {
54+
.bun => &.{ "bun", "-v" },
55+
.deno => &.{ "deno", "-v" },
56+
.docker => &.{ "docker", "-v" },
57+
.node => &.{ "node", "-v" },
58+
.rust => &.{ "rustc", "--version" },
59+
.zig => &.{ "zig", "version" },
60+
};
61+
}
62+
63+
/// Returns a version slice, e.g. "1.0.0", trimming additional text
64+
pub fn versionFormat(prop: Prop, string: []const u8) []const u8 {
65+
_ = prop;
66+
var start: usize = 0;
67+
var end: usize = 0;
68+
for (0..string.len) |i| if (isDigit(string[i])) {
69+
start = i;
70+
break;
71+
};
72+
for (start..string.len) |i| if (string[i] != '.' and !isDigit(string[i])) {
73+
end = i;
74+
break;
75+
};
76+
return if (start < end) string[start..end] else "";
77+
}
78+
};
79+
80+
const Context = @This();
81+
82+
allocator: Allocator,
83+
cwd: Dir,
84+
props: AutoHashMap(Prop, void),
85+
86+
pub fn init(allocator: Allocator, cwd: std.fs.Dir) Context {
87+
return Context{
88+
.allocator = allocator,
89+
.cwd = cwd,
90+
.props = AutoHashMap(Prop, void).init(allocator),
91+
};
92+
}
93+
94+
pub fn deinit(self: *Context) void {
95+
self.props.deinit();
96+
}
97+
98+
pub fn is(self: Context, prop: Prop) bool {
99+
return self.props.contains(prop);
100+
}
101+
102+
pub fn print(self: Context, tty: TTY) !void {
103+
inline for (std.meta.fields(Prop)) |field| {
104+
const prop: Prop = @enumFromInt(field.value);
105+
if (self.props.contains(prop)) {
106+
try tty.setColor(.white);
107+
try tty.write(" via ");
108+
try tty.setColor(.yellow);
109+
try tty.write(prop.symbol());
110+
const version = try prop.version(self.allocator);
111+
if (version) |string| {
112+
defer self.allocator.free(string);
113+
try tty.write(" ");
114+
try tty.write(prop.versionFormat(string));
115+
}
116+
}
117+
}
118+
try tty.setColor(.reset);
119+
}
120+
121+
pub fn scanAll(self: *Context) !void {
122+
var dir = try self.cwd.openDir("./", .{});
123+
defer dir.close();
124+
var depth: usize = 0;
125+
while (true) : (depth += 1) {
126+
if (depth == max_scan_depth) break;
127+
self.scanDirectory(&dir);
128+
const parent = try dir.openDir("../", .{});
129+
dir.close();
130+
dir = parent;
131+
// Exit once root is reached
132+
const path = try dir.realpathAlloc(self.allocator, ".");
133+
defer self.allocator.free(path);
134+
if (eql(u8, path, "/")) break;
135+
}
136+
}
137+
138+
/// Check all entries inside the open directory
139+
pub fn scanDirectory(self: *Context, open_dir: *Dir) void {
140+
var iter = open_dir.iterate();
141+
while (iter.next()) |next| {
142+
if (next) |entry| self.scanEntry(entry) else break;
143+
} else |_| return;
144+
}
145+
146+
/// Check an individual directory entry
147+
pub fn scanEntry(self: *Context, entry: Dir.Entry) void {
148+
const result: ?Prop = switch (entry.kind) {
149+
.directory => result: {
150+
if (eql(u8, entry.name, "node_modules")) {
151+
break :result .node;
152+
} else if (eql(u8, entry.name, "zig-out")) {
153+
break :result .zig;
154+
}
155+
break :result null;
156+
},
157+
.file, .sym_link => result: {
158+
const ext = extension(entry.name);
159+
if (eql(u8, entry.name, "bun.lock")) {
160+
break :result .bun;
161+
} else if (eql(u8, entry.name, "bun.lockb")) {
162+
break :result .bun;
163+
} else if (eql(u8, entry.name, "bunfig.toml")) {
164+
break :result .bun;
165+
} else if (eql(u8, entry.name, "Cargo.lock")) {
166+
break :result .rust;
167+
} else if (eql(u8, entry.name, "Cargo.toml")) {
168+
break :result .rust;
169+
} else if (eql(u8, entry.name, "deno.json")) {
170+
break :result .deno;
171+
} else if (eql(u8, entry.name, "deno.jsonc")) {
172+
break :result .deno;
173+
} else if (eql(u8, entry.name, "deno.lock")) {
174+
break :result .deno;
175+
} else if (eql(u8, entry.name, "docker-compose.yml")) {
176+
break :result .docker;
177+
} else if (eql(u8, entry.name, "package.json")) {
178+
break :result .node;
179+
} else if (eql(u8, ext, ".rs")) {
180+
break :result .rust;
181+
} else if (eql(u8, ext, ".zig")) {
182+
break :result .zig;
183+
}
184+
break :result null;
185+
},
186+
else => null,
187+
};
188+
if (result) |prop| {
189+
self.props.put(prop, {}) catch unreachable;
190+
}
191+
}

src/Host.zig

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const std = @import("std");
2+
const builtin = @import("builtin");
3+
const mem = std.mem;
4+
const posix = std.posix;
5+
6+
const Host = @This();
7+
8+
hostname_buffer: [std.posix.HOST_NAME_MAX]u8 = [_]u8{0} ** std.posix.HOST_NAME_MAX,
9+
10+
/// Returns the logged in username
11+
pub fn user(_: Host) []const u8 {
12+
const maybe = posix.getenv("USER");
13+
return if (maybe) |string| string else "user";
14+
}
15+
16+
/// Returns the system hostname
17+
pub fn name(self: *Host) ![]const u8 {
18+
const hostname = try posix.gethostname(&self.hostname_buffer);
19+
if (mem.indexOfScalar(u8, hostname, '.')) |i| {
20+
return hostname[0..i];
21+
}
22+
return hostname;
23+
}
24+
25+
/// Returns an OS emoji
26+
pub fn emoji(_: Host) []const u8 {
27+
return switch (builtin.os.tag) {
28+
.macos => "",
29+
.linux => emoji: {
30+
// @TODO Check uname for Raspberry Pi or Proxmox etc
31+
break :emoji "";
32+
},
33+
else => "",
34+
};
35+
}

src/TTY.zig

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const std = @import("std");
2+
3+
const TTY = @This();
4+
5+
writer: std.fs.File.Writer,
6+
config: std.io.tty.Config,
7+
8+
pub fn init() TTY {
9+
return .{
10+
.writer = std.io.getStdOut().writer(),
11+
.config = std.io.tty.detectConfig(std.io.getStdErr()),
12+
};
13+
}
14+
15+
pub fn write(self: TTY, bytes: []const u8) !void {
16+
_ = try self.writer.write(bytes);
17+
}
18+
19+
pub fn print(self: TTY, comptime format: []const u8, args: anytype) !void {
20+
try self.writer.print(format, args);
21+
}
22+
23+
pub fn setColor(self: TTY, color: std.io.tty.Color) !void {
24+
_ = try self.writer.write("%{");
25+
try self.config.setColor(self.writer, color);
26+
_ = try self.writer.write("%}");
27+
}

0 commit comments

Comments
 (0)