commit 1cee38065a849b02d63726a84b7daa8f0fb5f9f6
Author: BrookJeynes <jeynesbrook@gmail.com>
Date: Mon, 4 Nov 2024 14:32:38 +1000
first commit
Diffstat:
12 files changed, 859 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+zig-out/
+.zig-cache/
diff --git a/README b/README
@@ -0,0 +1,27 @@
+zysys
+-----
+
+A simple unix system fetch tool.
+
+```
+bjeyn@fedora
+--------------
+distro Fedora Linux 40 (Workstation Edition) x86_64
+uptime 6 13 57
+kernel 6.10.12-200.fc40.x86_64
+desktop river
+shell zsh
+memory 17170 / 63572 MB (27%)
+battery 96% (Charging)
+cpu AMD Ryzen 7 7840U w/ Radeon 780M Graphics
+disk 30G / 1.9T (2%)
+```
+
+An example config can be found in `./example-config.ziggy` with its schema
+defined in `./config.ziggy-schema`.
+
+The config will first read from `$XDG_CONFIG_HOME/zysys/config.ziggy`, falling
+back to `$HOME/.config/zysys/config.ziggy` if not set.
+
+
+This project is built using Zig `v0.14.0-dev.2126+e27b4647d`.
diff --git a/build.zig b/build.zig
@@ -0,0 +1,75 @@
+const std = @import("std");
+
+const release_targets: []const std.Target.Query = &.{
+ .{ .cpu_arch = .aarch64, .os_tag = .macos },
+ .{ .cpu_arch = .aarch64, .os_tag = .linux },
+ .{ .cpu_arch = .x86_64, .os_tag = .linux },
+ .{ .cpu_arch = .x86_64, .os_tag = .macos },
+};
+
+pub fn build(b: *std.Build) !void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ // Build targets for release.
+ const build_all = b.option(bool, "all-targets", "Build all targets in ReleaseSafe mode.") orelse false;
+ if (build_all) {
+ try build_targets(b);
+ return;
+ }
+
+ const ziggy_dep = b.dependency("ziggy", .{
+ .target = target,
+ .optimize = optimize,
+ });
+ const ziggy = ziggy_dep.module("ziggy");
+
+ const exe = b.addExecutable(.{
+ .name = "zysys",
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ exe.root_module.addImport("ziggy", ziggy);
+ b.installArtifact(exe);
+
+ const run_cmd = b.addRunArtifact(exe);
+ run_cmd.step.dependOn(b.getInstallStep());
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
+}
+
+fn build_targets(b: *std.Build) !void {
+ for (release_targets) |t| {
+ const target = b.resolveTargetQuery(t);
+
+ const ziggy_dep = b.dependency("ziggy", .{
+ .target = target,
+ .optimize = .ReleaseSafe,
+ });
+ const ziggy = ziggy_dep.module("ziggy");
+
+ const exe = b.addExecutable(.{
+ .name = "zysys",
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = .ReleaseSafe,
+ });
+ exe.root_module.addImport("ziggy", ziggy);
+ b.installArtifact(exe);
+
+ const target_output = b.addInstallArtifact(exe, .{
+ .dest_dir = .{
+ .override = .{
+ .custom = try t.zigTriple(b.allocator),
+ },
+ },
+ });
+
+ b.getInstallStep().dependOn(&target_output.step);
+ }
+}
diff --git a/build.zig.zon b/build.zig.zon
@@ -0,0 +1,20 @@
+.{
+ .name = "zysys",
+ .version = "0.1.0",
+ .minimum_zig_version = "0.14.0-dev.2126+e27b4647d",
+
+ .dependencies = .{
+ // Move to https://github.com/kristoff-it/ziggy/tree/zig-0.14.0-dev
+ // once https://github.com/kristoff-it/ziggy/pull/43 has been merged.
+ .ziggy = .{
+ .url = "git+https://github.com/BrookJeynes/ziggy?ref=add-enum-support#fb224b22f0a30005e521bde9a874bc959463362b",
+ .hash = "122034c46e43d6d7bc031caad233eb2547352df32e2a2b00fa494742ef6bca52d161",
+ },
+ },
+
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ },
+}
diff --git a/config.ziggy-schema b/config.ziggy-schema
@@ -0,0 +1,17 @@
+root = Config
+
+@widgets = enum {
+ distro,
+ uptime,
+ kernel,
+ desktop,
+ shell,
+ memory,
+ battery,
+ cpu,
+ disk,
+},
+
+struct Config {
+ widgets: [@widgets],
+}
diff --git a/example-config.ziggy b/example-config.ziggy
@@ -0,0 +1,11 @@
+.widgets = [
+ @widgets("distro"),
+ @widgets("uptime"),
+ @widgets("kernel"),
+ @widgets("desktop"),
+ @widgets("shell"),
+ @widgets("memory"),
+ @widgets("battery"),
+ @widgets("cpu"),
+ @widgets("disk"),
+],
diff --git a/src/buffered_file_iter.zig b/src/buffered_file_iter.zig
@@ -0,0 +1,29 @@
+const std = @import("std");
+
+const BufferedFileIterator = @This();
+
+alloc: std.mem.Allocator,
+buf_reader: std.io.BufferedReader(4096, std.io.AnyReader),
+line: std.ArrayList(u8),
+
+pub fn init(alloc: std.mem.Allocator, reader: std.io.AnyReader) BufferedFileIterator {
+ return BufferedFileIterator{
+ .alloc = alloc,
+ .buf_reader = std.io.bufferedReader(reader),
+ .line = std.ArrayList(u8).init(alloc),
+ };
+}
+
+pub fn deinit(self: BufferedFileIterator) void {
+ self.line.deinit();
+}
+
+pub fn next(self: *BufferedFileIterator) !?[]const u8 {
+ self.line.clearRetainingCapacity();
+ const writer = self.line.writer();
+ self.buf_reader.reader().streamUntilDelimiter(writer, '\n', null) catch |err| switch (err) {
+ error.EndOfStream => return null,
+ else => return err,
+ };
+ return self.line.items;
+}
diff --git a/src/environment.zig b/src/environment.zig
@@ -0,0 +1,28 @@
+const std = @import("std");
+
+pub fn get_home_dir() !?std.fs.Dir {
+ return try std.fs.openDirAbsolute(std.posix.getenv("HOME") orelse {
+ return null;
+ }, .{ .iterate = true });
+}
+
+pub fn get_xdg_config_home_dir() !?std.fs.Dir {
+ return try std.fs.openDirAbsolute(std.posix.getenv("XDG_CONFIG_HOME") orelse {
+ return null;
+ }, .{ .iterate = true });
+}
+
+pub fn file_exists(dir: std.fs.Dir, path: []const u8) bool {
+ const result = blk: {
+ _ = dir.openFile(path, .{}) catch |err| {
+ switch (err) {
+ error.FileNotFound => break :blk false,
+ else => {
+ break :blk true;
+ },
+ }
+ };
+ break :blk true;
+ };
+ return result;
+}
diff --git a/src/main.zig b/src/main.zig
@@ -0,0 +1,180 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const stats = @import("stats.zig");
+const sys_info = @import("sys_info.zig");
+const environment = @import("environment.zig");
+const ziggy = @import("ziggy");
+
+const padding: u8 = 9;
+
+const Widgets = enum {
+ distro,
+ uptime,
+ kernel,
+ desktop,
+ shell,
+ memory,
+ battery,
+ cpu,
+ disk,
+};
+
+const Config = struct {
+ widgets: []const Widgets,
+};
+
+const default_config = Config{
+ .widgets = &[_]Widgets{
+ .distro,
+ .uptime,
+ .kernel,
+ .desktop,
+ .shell,
+ .memory,
+ .battery,
+ .cpu,
+ .disk,
+ },
+};
+
+pub fn main() !void {
+ const stdout = std.io.getStdOut().writer();
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ defer _ = gpa.deinit();
+ var arena = std.heap.ArenaAllocator.init(gpa.allocator());
+ defer _ = arena.deinit();
+ var alloc = arena.allocator();
+
+ // Read config
+ const config = lbl: {
+ var config_path: []u8 = undefined;
+ defer alloc.free(config_path);
+
+ var config_home: std.fs.Dir = undefined;
+ defer config_home.close();
+
+ if (try environment.get_xdg_config_home_dir()) |path| {
+ config_home = path;
+ config_path = try std.fs.path.join(alloc, &.{ "zysys", "config.ziggy" });
+ } else {
+ if (try environment.get_home_dir()) |path| {
+ config_home = path;
+ config_path = try std.fs.path.join(alloc, &.{ ".config", "zysys", "config.ziggy" });
+ }
+ }
+
+ if (!environment.file_exists(config_home, config_path)) {
+ break :lbl default_config;
+ }
+
+ const contents = config_home.readFileAlloc(alloc, config_path, 4096) catch break :lbl default_config;
+ defer alloc.free(contents);
+ const contentsZ = try alloc.dupeZ(u8, contents);
+ defer alloc.free(contentsZ);
+ break :lbl ziggy.parseLeaky(Config, alloc, contentsZ, .{}) catch default_config;
+ };
+
+ // Print header
+ const user_env = try sys_info.getUser(alloc);
+ defer alloc.free(user_env);
+ const distro_id = try sys_info.getDistroId(alloc);
+ defer alloc.free(distro_id);
+
+ var header_buf: [1024]u8 = undefined;
+ const header = try std.fmt.bufPrint(&header_buf, "{s}@{s}\n", .{ user_env, distro_id });
+
+ try stdout.writeAll(header);
+ try stdout.print("{s[str]:-<[count]}\n", .{ .str = "-", .count = header.len + 1 });
+
+ // Print widgets
+ for (config.widgets) |widget| {
+ switch (widget) {
+ .distro => {
+ const distro = sys_info.getDistro(alloc) catch continue;
+ defer alloc.free(distro);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "distro", .stat = distro, .padding = padding },
+ );
+ },
+ .uptime => {
+ const uptime = stats.getUptime() catch continue;
+
+ try stdout.print(
+ "{s[header]: <[padding]}{[days]} {[hours]} {[minutes]}\n",
+ .{
+ .header = "uptime",
+ .days = uptime.days,
+ .hours = uptime.hours,
+ .minutes = uptime.minutes,
+ .padding = padding,
+ },
+ );
+ },
+ .kernel => {
+ const utsname = std.posix.uname();
+ const release = std.mem.sliceTo(&utsname.release, 0);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "kernel", .stat = release, .padding = padding },
+ );
+ },
+ .desktop => {
+ const desktop = sys_info.getDesktop(alloc) catch continue;
+ defer alloc.free(desktop);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "desktop", .stat = desktop, .padding = padding },
+ );
+ },
+ .shell => {
+ const shell = sys_info.getShell(alloc) catch continue;
+ defer alloc.free(shell);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "shell", .stat = shell, .padding = padding },
+ );
+ },
+ .memory => {
+ const mem = stats.getMemory(alloc, .{ .mb = true }) catch continue;
+ defer alloc.free(mem);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "memory", .stat = mem, .padding = padding },
+ );
+ },
+ .battery => {
+ const battery = stats.getBattery(alloc) catch continue;
+ defer alloc.free(battery);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "battery", .stat = battery, .padding = padding },
+ );
+ },
+ .cpu => {
+ const cpu = sys_info.getCpu(alloc) catch continue;
+ defer alloc.free(cpu);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "cpu", .stat = cpu, .padding = padding },
+ );
+ },
+ .disk => {
+ const disk = sys_info.getDisk(alloc) catch continue;
+ defer alloc.free(disk);
+
+ try stdout.print(
+ "{s[header]: <[padding]}{s[stat]}\n",
+ .{ .header = "disk", .stat = disk, .padding = padding },
+ );
+ },
+ }
+ }
+}
diff --git a/src/process.zig b/src/process.zig
@@ -0,0 +1,66 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const BufferedFileIterator = @import("buffered_file_iter.zig");
+
+pub fn getParentPid(alloc: std.mem.Allocator, pid: std.c.pid_t) !std.c.pid_t {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try std.fmt.bufPrint(&buf, "/proc/{d}/status", .{pid});
+
+ const file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only });
+ defer file.close();
+
+ var file_it = BufferedFileIterator.init(alloc, file.reader().any());
+ defer file_it.deinit();
+
+ while (try file_it.next()) |line| {
+ if (std.mem.startsWith(u8, line, "PPid")) {
+ var ppid_it = std.mem.splitScalar(u8, line, ':');
+ _ = ppid_it.next(); // Skip "PPid:"
+ const ppid_str = ppid_it.next() orelse return error.PpidIsNull;
+ const ppid = try std.fmt.parseInt(
+ std.c.pid_t,
+ std.mem.trim(u8, ppid_str, " \t"),
+ 10,
+ );
+ return ppid;
+ }
+ }
+
+ return error.PpidIsNotSpecified;
+ },
+ else => return error.UnsupportedOs,
+ }
+}
+
+pub fn getProcessName(alloc: std.mem.Allocator, pid: std.c.pid_t) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try std.fmt.bufPrint(&buf, "/proc/{d}/status", .{pid});
+
+ const file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only });
+ defer file.close();
+
+ var file_it = BufferedFileIterator.init(alloc, file.reader().any());
+ defer file_it.deinit();
+
+ while (try file_it.next()) |line| {
+ if (std.mem.startsWith(u8, line, "Name")) {
+ var name_it = std.mem.splitScalar(u8, line, ':');
+ _ = name_it.next(); // Skip "Name:"
+ const process_name = std.mem.trim(
+ u8,
+ (name_it.next() orelse return error.NameIsNull),
+ " \t",
+ );
+ return try alloc.dupe(u8, process_name);
+ }
+ }
+
+ return error.NameIsNotSpecified;
+ },
+ else => return error.UnsupportedOs,
+ }
+}
diff --git a/src/stats.zig b/src/stats.zig
@@ -0,0 +1,169 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const BufferedFileIterator = @import("buffered_file_iter.zig");
+
+pub fn getBattery(alloc: std.mem.Allocator) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ var dir = try std.fs.openDirAbsolute("/sys/class/power_supply/", .{ .iterate = true });
+ defer dir.close();
+
+ var smallest_battery: ?usize = null;
+
+ // Walk dir and find the smallest battery.
+ var it = dir.iterate();
+ while (try it.next()) |entry| {
+ if (std.mem.startsWith(u8, entry.name, "BAT")) {
+ const battery_no = try std.fmt.parseInt(usize, std.mem.trimLeft(u8, entry.name, "BAT"), 10);
+ if (smallest_battery) |smallest| {
+ if (battery_no < smallest) smallest_battery = battery_no;
+ } else {
+ smallest_battery = battery_no;
+ }
+ }
+ }
+
+ if (smallest_battery == null) return error.NoBatteryDetected;
+
+ // Read battery capacity.
+ var battery_capacity_path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const battery_capacity_path = try std.fmt.bufPrint(
+ &battery_capacity_path_buf,
+ "/sys/class/power_supply/BAT{d}/capacity",
+ .{smallest_battery.?},
+ );
+ const battery_capacity_file = try std.fs.openFileAbsolute(
+ battery_capacity_path,
+ .{ .mode = .read_only },
+ );
+ defer battery_capacity_file.close();
+
+ var battery_capacity_buf: [1024]u8 = undefined;
+ const battery_capacity_bytes = try battery_capacity_file.readAll(&battery_capacity_buf);
+ const battery_capacity = try std.fmt.parseInt(
+ usize,
+ std.mem.trim(u8, battery_capacity_buf[0..battery_capacity_bytes], "\n"),
+ 10,
+ );
+
+ // Read battery status.
+ var battery_status_path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const battery_status_path = try std.fmt.bufPrint(
+ &battery_status_path_buf,
+ "/sys/class/power_supply/BAT{d}/status",
+ .{smallest_battery.?},
+ );
+ const battery_status_file = try std.fs.openFileAbsolute(
+ battery_status_path,
+ .{ .mode = .read_only },
+ );
+ defer battery_status_file.close();
+
+ var battery_status_buf: [1024]u8 = undefined;
+ const battery_status_bytes = try battery_status_file.readAll(&battery_status_buf);
+ const battery_status = std.mem.trim(
+ u8,
+ battery_status_buf[0..battery_status_bytes],
+ "\n",
+ );
+
+ var output_buf: [1024]u8 = undefined;
+ const output = try std.fmt.bufPrint(
+ &output_buf,
+ "{d}% ({s})",
+ .{ battery_capacity, battery_status },
+ );
+ return try alloc.dupe(u8, output);
+ },
+ else => return error.UnsupportedOs,
+ }
+}
+
+pub fn getMemory(alloc: std.mem.Allocator, options: struct { mb: bool }) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ const file = try std.fs.openFileAbsolute("/proc/meminfo", .{ .mode = .read_only });
+ defer file.close();
+
+ var file_it = BufferedFileIterator.init(alloc, file.reader().any());
+ defer file_it.deinit();
+
+ const dividend: usize = if (options.mb) 1000 else 1024;
+ const suffix = if (options.mb) "MB" else "MiB";
+
+ const mem_total = lbl: {
+ const line = try file_it.next() orelse return error.MemTotalIsNotSpecified;
+ var line_it = std.mem.splitScalar(u8, line, ':');
+ _ = line_it.next(); // Skip "MemTotal:"
+ const mem_total_with_suffix = line_it.next() orelse return error.MemTotalIsNull;
+ // TODO: Is it safe to assume the suffix will only ever be "kB"?
+ const mem_total_str = std.mem.trim(u8, mem_total_with_suffix, " \tkB");
+ const mem_total_int = (try std.fmt.parseInt(usize, mem_total_str, 10)) / dividend;
+ break :lbl mem_total_int;
+ };
+
+ _ = try file_it.next(); // Skip "MemFree: x"
+
+ const mem_available = lbl: {
+ const line = try file_it.next() orelse return error.MemAvailableIsNotSpecified;
+ var line_it = std.mem.splitScalar(u8, line, ':');
+ _ = line_it.next(); // Skip "MemAvailable:"
+ const mem_available_with_suffix = line_it.next() orelse return error.MemAvailableIsNull;
+ // TODO: Is it safe to assume it'll only ever be "kB"?
+ const mem_available_str = std.mem.trim(u8, mem_available_with_suffix, " \tkB");
+ const mem_available_int = (try std.fmt.parseInt(usize, mem_available_str, 10)) / dividend;
+ break :lbl mem_available_int;
+ };
+
+ const mem_used = mem_total - mem_available;
+ const mem_percentage: usize = @intFromFloat(
+ (@as(f32, @floatFromInt(mem_used)) / @as(f32, @floatFromInt(mem_total))) * 100,
+ );
+
+ var buf: [1024]u8 = undefined;
+ const mem_str = try std.fmt.bufPrint(&buf, "{d} / {d} {s} ({d}%)", .{
+ mem_used,
+ mem_total,
+ suffix,
+ mem_percentage,
+ });
+ return try alloc.dupe(u8, mem_str);
+ },
+ else => return error.UnsupportedOs,
+ }
+}
+
+pub fn getUptime() !struct { days: usize, minutes: usize, hours: usize } {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ const file = try std.fs.openFileAbsolute(
+ "/proc/uptime",
+ .{ .mode = .read_only },
+ );
+ defer file.close();
+
+ var uptime_buf: [1024]u8 = undefined;
+ const bytes = try file.readAll(&uptime_buf);
+ const uptime_str = uptime_buf[0..bytes];
+
+ var uptime_it = std.mem.splitScalar(u8, uptime_str, ' ');
+ const uptime: usize = @intFromFloat(
+ try std.fmt.parseFloat(
+ f32,
+ uptime_it.next() orelse return error.UptimeIsNull,
+ ),
+ );
+
+ const uptime_days = uptime / 86400;
+ const uptime_hours = (uptime % 86400) / 3600;
+ const uptime_minutes = (uptime % 3600) / 60;
+
+ return .{
+ .days = uptime_days,
+ .minutes = uptime_minutes,
+ .hours = uptime_hours,
+ };
+ },
+ else => return error.UnsupportedOs,
+ }
+}
diff --git a/src/sys_info.zig b/src/sys_info.zig
@@ -0,0 +1,235 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const BufferedFileIterator = @import("buffered_file_iter.zig");
+const process = @import("process.zig");
+
+pub fn getCpu(alloc: std.mem.Allocator) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ const file = try std.fs.openFileAbsolute(
+ "/proc/cpuinfo",
+ .{ .mode = .read_only },
+ );
+ defer file.close();
+
+ var file_it = BufferedFileIterator.init(alloc, file.reader().any());
+ defer file_it.deinit();
+
+ while (try file_it.next()) |line| {
+ if (std.mem.startsWith(u8, line, "model name")) {
+ var name_it = std.mem.tokenizeScalar(u8, line, ':');
+ _ = name_it.next(); // Skip "model name :"
+ const name = name_it.next() orelse return error.ModelNameIsNull;
+
+ return try alloc.dupe(u8, std.mem.trim(u8, name, " \t"));
+ }
+ }
+
+ return error.ModelNameIsNotSpecified;
+ },
+ .macos => {
+ const child = try std.process.Child.run(.{
+ .allocator = alloc,
+ .argv = &[_][]const u8{ "sysctl", "-n", "machdep.cpu.brand_string" },
+ });
+ defer alloc.free(child.stderr);
+
+ if (child.term.Exited != 0) {
+ return error.FailedToReadMacCPU;
+ }
+
+ return child.stdout;
+ },
+ else => return error.UnsupportedOs,
+ }
+}
+
+pub fn getShell(alloc: std.mem.Allocator) ![]const u8 {
+ var env_map = try std.process.getEnvMap(alloc);
+ defer env_map.deinit();
+
+ if (env_map.get("SHELL")) |shell_path| {
+ var shell_it = std.mem.tokenizeScalar(u8, shell_path, '/');
+ var shell: []const u8 = undefined;
+ // Get the last element
+ while (shell_it.next()) |split| {
+ shell = split;
+ }
+
+ return try alloc.dupe(u8, shell);
+ }
+
+ // If $SHELL is not set, try find the shell via pid.
+ const pid = try process.getParentPid(alloc, std.os.linux.getppid());
+ const ppid = try process.getParentPid(alloc, pid);
+ return try process.getProcessName(alloc, ppid);
+}
+
+pub fn getDesktop(alloc: std.mem.Allocator) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ var env_map = try std.process.getEnvMap(alloc);
+ defer env_map.deinit();
+
+ if (env_map.get("XDG_CURRENT_DESKTOP")) |desktop| {
+ return try alloc.dupe(u8, desktop);
+ }
+
+ if (env_map.get("XDG_SESSION_DESKTOP")) |desktop| {
+ return try alloc.dupe(u8, desktop);
+ }
+
+ if (env_map.get("DESKTOP_SESSION")) |desktop| {
+ return try alloc.dupe(u8, desktop);
+ }
+
+ if (env_map.get("XDG_SESSION_TYPE")) |session_type| {
+ if (std.mem.eql(u8, session_type, "tty")) {
+ return try alloc.dupe(u8, "Headless");
+ }
+ }
+
+ return try alloc.dupe(u8, "Unknown");
+ },
+ else => return error.UnsupportedOs,
+ }
+}
+
+pub fn getDistro(alloc: std.mem.Allocator) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ const utsname = std.posix.uname();
+
+ const file = try std.fs.openFileAbsolute(
+ "/etc/os-release",
+ .{ .mode = .read_only },
+ );
+ defer file.close();
+
+ var file_it = BufferedFileIterator.init(alloc, file.reader().any());
+ defer file_it.deinit();
+
+ while (try file_it.next()) |line| {
+ if (std.mem.startsWith(u8, line, "PRETTY_NAME")) {
+ var name_it = std.mem.tokenizeScalar(u8, line, '=');
+ _ = name_it.next(); // Skip "NAME="
+ const name = name_it.next() orelse return error.OsIdIsNull;
+
+ var name_machine_buf: [1024]u8 = undefined;
+ const name_machine = try std.fmt.bufPrint(
+ &name_machine_buf,
+ "{s} {s}",
+ .{ std.mem.trim(u8, name, "\""), std.mem.sliceTo(&utsname.machine, 0) },
+ );
+ return try alloc.dupe(u8, name_machine);
+ }
+ }
+
+ return error.OsIdNotSpecified;
+ },
+ .macos => {
+ const utsname = std.posix.uname();
+
+ var name_machine_buf: [1024]u8 = undefined;
+ const name_machine = try std.fmt.bufPrint(
+ &name_machine_buf,
+ "MacOS X {s}",
+ .{std.mem.sliceTo(&utsname.machine, 0)},
+ );
+ return try alloc.dupe(u8, name_machine);
+ },
+ else => return error.UnsupportedOs,
+ }
+}
+
+pub fn getDistroId(alloc: std.mem.Allocator) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ const file = try std.fs.openFileAbsolute(
+ "/etc/os-release",
+ .{ .mode = .read_only },
+ );
+ defer file.close();
+
+ var file_it = BufferedFileIterator.init(alloc, file.reader().any());
+ defer file_it.deinit();
+
+ while (try file_it.next()) |line| {
+ if (std.mem.startsWith(u8, line, "ID")) {
+ var id_it = std.mem.tokenizeScalar(u8, line, '=');
+ _ = id_it.next(); // Skip "ID="
+ const id = id_it.next() orelse return error.OsIdIsNull;
+ return try alloc.dupe(u8, id);
+ }
+ }
+
+ return error.OsIdNotSpecified;
+ },
+ .macos => {
+ const utsname = std.posix.uname();
+ return try alloc.dupe(u8, std.mem.sliceTo(&utsname.nodename, 0));
+ },
+ else => return error.UnsupportedOs,
+ }
+}
+
+pub fn getUser(alloc: std.mem.Allocator) ![]const u8 {
+ var env_map = try std.process.getEnvMap(alloc);
+ defer env_map.deinit();
+
+ const user_env = env_map.get("USER") orelse return error.USEREnvNotSet;
+ return try alloc.dupe(u8, user_env);
+}
+
+pub fn getDisk(alloc: std.mem.Allocator) ![]const u8 {
+ switch (builtin.os.tag) {
+ .linux, .openbsd => {
+ const child = try std.process.Child.run(.{
+ .allocator = alloc,
+ .argv = &[_][]const u8{ "df", "-h", "--output=size,used,pcent", "/" },
+ });
+ defer alloc.free(child.stdout);
+ defer alloc.free(child.stderr);
+
+ if (child.term.Exited != 0) {
+ return error.FailedToReadMacCPU;
+ }
+
+ var output_it = std.mem.tokenizeScalar(u8, child.stdout, '\n');
+ _ = output_it.next(); // Skip headers.
+
+ const line = std.mem.trim(
+ u8,
+ output_it.next() orelse return error.DiskUsageIsNull,
+ " ",
+ );
+
+ var line_it = std.mem.tokenizeScalar(u8, line, ' ');
+
+ const total = std.mem.trim(
+ u8,
+ line_it.next() orelse return error.DiskTotalIsNull,
+ " ",
+ );
+ const used = std.mem.trim(
+ u8,
+ line_it.next() orelse return error.DiskUsedIsNull,
+ " ",
+ );
+ const percentage = std.mem.trim(
+ u8,
+ line_it.next() orelse return error.DiskPercentageIsNull,
+ " ",
+ );
+
+ var output_buf: [1024]u8 = undefined;
+ const output = try std.fmt.bufPrint(
+ &output_buf,
+ "{s} / {s} ({s})",
+ .{ used, total, percentage },
+ );
+ return try alloc.dupe(u8, output);
+ },
+ else => return error.UnsupportedOs,
+ }
+}