lens

posix system fetch tool
git clone git://brookjeynes.dev/bjeynes/lens.git
Log | Files | Refs | README

commit 1cee38065a849b02d63726a84b7daa8f0fb5f9f6
Author: BrookJeynes <jeynesbrook@gmail.com>
Date:   Mon,  4 Nov 2024 14:32:38 +1000

first commit

Diffstat:
A.gitignore | 2++
AREADME | 27+++++++++++++++++++++++++++
Abuild.zig | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild.zig.zon | 20++++++++++++++++++++
Aconfig.ziggy-schema | 17+++++++++++++++++
Aexample-config.ziggy | 11+++++++++++
Asrc/buffered_file_iter.zig | 29+++++++++++++++++++++++++++++
Asrc/environment.zig | 28++++++++++++++++++++++++++++
Asrc/main.zig | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/process.zig | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/stats.zig | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sys_info.zig | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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, + } +}