lens

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

commit ba1a6566f871342e3f1f43076b0763bbe6643e1e
parent f41ccd41067e344da56cda541608f59fe753c30e
Author: brookjeynes <me@brookjeynes.dev>
Date:   Fri, 15 May 2026 08:13:06 +1000

feat: upgrade to zig 0.16.0

Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
D.github/workflows/create-draft-release.yml | 67-------------------------------------------------------------------
M.gitignore | 1+
AREADME | 20++++++++++++++++++++
DREADME.md | 31-------------------------------
Mbuild.zig | 1-
Mbuild.zig.zon | 6+++---
Msrc/environment.zig | 17+++++++++--------
Msrc/main.zig | 107+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/process.zig | 53++++++++++++++++++++++++++---------------------------
Msrc/stats.zig | 308++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/sys_info.zig | 216++++++++++++++++++++++++++++++++++++++-----------------------------------------
11 files changed, 372 insertions(+), 455 deletions(-)

diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml @@ -1,67 +0,0 @@ -name: Create draft release - -on: - workflow_dispatch: - -env: - # https://github.com/cli/cli/issues/9514#issuecomment-2311517523 - GH_TOKEN: ${{ secrets.TOKEN }} - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Zig - uses: korandoru/setup-zig@v1 - with: - zig-version: "master" - - - name: Build application - run: | - zig build -Dall-targets - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: builds - path: zig-out/ - - release: - needs: build - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Create tarballs - run: | - mkdir -p tarballs - for dir in artifacts/builds/*; do - if [ -d "$dir" ] && [ $(basename "$dir") != "bin" ]; then - tar -czf "tarballs/$(basename "$dir").tar.gz" -C "$dir" . - fi - done - - - name: gh log - run: | - gh --version - gh auth status - - - name: Create release - run: | - gh release create ${{ github.ref_name }} tarballs/* \ - --title "Release ${{ github.ref_name }}" \ - --notes "Automated release with build artifacts." \ - --draft - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ zig-out/ .zig-cache/ +zig-pkg/ diff --git a/README b/README @@ -0,0 +1,20 @@ +lens +==== + +a minimal posix system fetch tool + +user@fedora +-------------- +distro Fedora Linux 40 (Workstation Edition) x86_64 +uptime 6d 13h 57m +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%) + +lens will look for a configuration file at either: +- $HOME/.lens/config.ziggy +- $XDG_CONFIG_HOME/lens/config.ziggy diff --git a/README.md b/README.md @@ -1,31 +0,0 @@ -lens ------ - -A simple unix system fetch tool. - -``` -user@fedora --------------- -distro Fedora Linux 40 (Workstation Edition) x86_64 -uptime 6d 13h 57m -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%) -``` - -Configure `lens` by editing the external configuration file located at either: -- `$HOME/.lens/config.ziggy` -- `$XDG_CONFIG_HOME/lens/config.ziggy`. - -lens will look for these env variables specifically. If they are not set, lens -will not be able to find the config file. - -An example config file can be found [here](./example-config.ziggy). -The config schema can be found [here](./config.ziggy-schema). - -Contributions, issues, and feature requests are always welcome! This project -is built using Zig `v0.14.0-dev.2628+5b5c60f43`. diff --git a/build.zig b/build.zig @@ -28,7 +28,6 @@ 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); diff --git a/build.zig.zon b/build.zig.zon @@ -2,12 +2,12 @@ .fingerprint = 0x2cdaf8c397088e72, .name = .lens, .version = "0.2.0", - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0", .dependencies = .{ .ziggy = .{ - .url = "https://github.com/kristoff-it/ziggy/archive/4353b20ef2ac750e35c6d68e4eb2a07c2d7cf901.tar.gz", - .hash = "ziggy-0.1.0-kTg8v5pABgDztlefWHceH-Sh8tVveguFC61QkmLkIRaA", + .url = "git+https://github.com/kristoff-it/ziggy#0b61581980d6905dd4108a618d602f5778593122", + .hash = "ziggy-0.1.0-kTg8v0NWBgAyFtS5awtOlKnC7ddvefR_I0Xh3XQyi7p1", }, }, diff --git a/src/environment.zig b/src/environment.zig @@ -1,23 +1,24 @@ const std = @import("std"); -pub fn getHomeDir() !?std.fs.Dir { - return try std.fs.openDirAbsolute(std.posix.getenv("HOME") orelse { +pub fn getHomeDir(io: std.Io, env_map: *std.process.Environ.Map) !?std.Io.Dir { + return try std.Io.Dir.openDirAbsolute(io, env_map.get("HOME") orelse { return null; }, .{ .iterate = true }); } -pub fn getXdgConfigHomeDir() !?std.fs.Dir { - return try std.fs.openDirAbsolute(std.posix.getenv("XDG_CONFIG_HOME") orelse { +pub fn getXdgConfigHomeDir(io: std.Io, env_map: *std.process.Environ.Map) !?std.Io.Dir { + return try std.Io.Dir.openDirAbsolute(io, env_map.get("XDG_CONFIG_HOME") orelse { return null; }, .{ .iterate = true }); } -pub fn fileExists(dir: std.fs.Dir, path: []const u8) bool { +pub fn fileExists(dir: std.Io.Dir, io: std.Io, path: []const u8) bool { const result = blk: { - _ = dir.openFile(path, .{}) catch |err| { + _ = dir.openFile(io, path, .{}) catch |err| { switch (err) { error.FileNotFound => break :blk false, else => { + std.log.info("{}", .{err}); break :blk true; }, } @@ -27,9 +28,9 @@ pub fn fileExists(dir: std.fs.Dir, path: []const u8) bool { return result; } -pub fn dirExists(dir: std.fs.Dir, path: []const u8) bool { +pub fn dirExists(dir: std.Io.Dir, io: std.Io, path: []const u8) bool { const result = blk: { - _ = dir.openDir(path, .{}) catch |err| { + _ = dir.openDir(io, path, .{}) catch |err| { switch (err) { error.FileNotFound => break :blk false, else => { diff --git a/src/main.zig b/src/main.zig @@ -43,50 +43,51 @@ const CONFIG_NAME = "config.ziggy"; const HOME_DIR_NAME = ".lens"; const XDG_CONFIG_HOME_DIR_NAME = "lens"; -pub fn main() !void { +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const io = init.io; + var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); - const stdout = &stdout_writer.interface; + var stdout_file_writer: std.Io.File.Writer = .init(.stdout(), io, &stdout_buffer); + const stdout_writer = &stdout_file_writer.interface; - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - var arena = std.heap.ArenaAllocator.init(gpa.allocator()); + var arena = std.heap.ArenaAllocator.init(gpa); defer _ = arena.deinit(); - var alloc = arena.allocator(); + var allocator = arena.allocator(); // Read config const config = lbl: { var config_dir = dir: { - if (try environment.getXdgConfigHomeDir()) |home_dir| { + if (try environment.getXdgConfigHomeDir(io, init.environ_map)) |home_dir| { defer { var dir = home_dir; - dir.close(); + dir.close(io); } - if (!environment.dirExists(home_dir, XDG_CONFIG_HOME_DIR_NAME)) { + if (!environment.dirExists(home_dir, io, XDG_CONFIG_HOME_DIR_NAME)) { break :lbl default_config; } - const dir = try home_dir.openDir(XDG_CONFIG_HOME_DIR_NAME, .{ .iterate = true }); - if (!environment.fileExists(dir, CONFIG_NAME)) { + const dir = try home_dir.openDir(io, XDG_CONFIG_HOME_DIR_NAME, .{ .iterate = true }); + if (!environment.fileExists(dir, io, CONFIG_NAME)) { break :lbl default_config; } break :dir dir; } - if (try environment.getHomeDir()) |home_dir| { + if (try environment.getHomeDir(io, init.environ_map)) |home_dir| { defer { var dir = home_dir; - dir.close(); + dir.close(io); } - if (!environment.dirExists(home_dir, HOME_DIR_NAME)) { + if (!environment.dirExists(home_dir, io, HOME_DIR_NAME)) { break :lbl default_config; } - const dir = try home_dir.openDir(HOME_DIR_NAME, .{ .iterate = true }); - if (!environment.fileExists(dir, CONFIG_NAME)) { + const dir = try home_dir.openDir(io, HOME_DIR_NAME, .{ .iterate = true }); + if (!environment.fileExists(dir, io, CONFIG_NAME)) { break :lbl default_config; } @@ -96,33 +97,35 @@ pub fn main() !void { break :lbl default_config; }; - const contents = config_dir.readFileAlloc(alloc, CONFIG_NAME, 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; + const contents = config_dir.readFileAlloc(io, CONFIG_NAME, allocator, .limited(4096)) catch break :lbl default_config; + defer allocator.free(contents); + const contentsZ = try allocator.dupeZ(u8, contents); + defer allocator.free(contentsZ); + + var meta: ziggy.Deserializer.Meta = undefined; + break :lbl ziggy.deserializeLeaky(Config, allocator, contentsZ, &meta, .{}) 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); + const user_env = try sys_info.getUser(allocator, init.environ_map); + defer allocator.free(user_env); + const distro_id = try sys_info.getDistroId(io, allocator); + defer allocator.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("{[str]s:-<[count]}\n", .{ .str = "-", .count = header.len + 1 }); + try stdout_writer.writeAll(header); + try stdout_writer.print("{[str]s:-<[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); + const distro = sys_info.getDistro(io, allocator) catch continue; + defer allocator.free(distro); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "distro", .stat = distro, .padding = padding }, ); @@ -130,7 +133,7 @@ pub fn main() !void { .uptime => { const uptime = stats.getUptime() catch continue; - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[days]d}d {[hours]d}h {[minutes]d}m\n", .{ .header = "uptime", @@ -145,61 +148,61 @@ pub fn main() !void { const utsname = std.posix.uname(); const release = std.mem.sliceTo(&utsname.release, 0); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "kernel", .stat = release, .padding = padding }, ); }, .desktop => { - const desktop = sys_info.getDesktop(alloc) catch continue; - defer alloc.free(desktop); + const desktop = try sys_info.getDesktop(allocator, init.environ_map); + defer allocator.free(desktop); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "desktop", .stat = desktop, .padding = padding }, ); }, .shell => { - const shell = sys_info.getShell(alloc) catch continue; - defer alloc.free(shell); + const shell = sys_info.getShell(io, allocator, init.environ_map) catch continue; + defer allocator.free(shell); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "shell", .stat = shell, .padding = padding }, ); }, .memory => { - const mem = stats.getMemory(alloc, .{ .mb = true }) catch continue; - defer alloc.free(mem); + const mem = stats.getMemory(io, allocator, .{ .mb = true }) catch continue; + defer allocator.free(mem); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "memory", .stat = mem, .padding = padding }, ); }, .battery => { - const battery = stats.getBattery(alloc) catch continue; - defer alloc.free(battery); + const battery = stats.getBattery(io, allocator) catch continue; + defer allocator.free(battery); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "battery", .stat = battery, .padding = padding }, ); }, .cpu => { - const cpu = sys_info.getCpu(alloc) catch continue; - defer alloc.free(cpu); + const cpu = sys_info.getCpu(io, allocator) catch continue; + defer allocator.free(cpu); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "cpu", .stat = cpu, .padding = padding }, ); }, .disk => { - const disk = sys_info.getDisk(alloc) catch continue; - defer alloc.free(disk); + const disk = sys_info.getDisk(io, allocator) catch continue; + defer allocator.free(disk); - try stdout.print( + try stdout_writer.print( "{[header]s: <[padding]}{[stat]s}\n", .{ .header = "disk", .stat = disk, .padding = padding }, ); @@ -207,5 +210,5 @@ pub fn main() !void { } } - try stdout.flush(); + try stdout_writer.flush(); } diff --git a/src/process.zig b/src/process.zig @@ -6,40 +6,39 @@ pub const ProcessInfo = struct { ppid: std.c.pid_t, }; -pub fn getProcessInfo(alloc: std.mem.Allocator, pid: std.c.pid_t) !ProcessInfo { - switch (builtin.os.tag) { - .linux, .openbsd => { - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const path = try std.fmt.bufPrint(&path_buf, "/proc/{d}/stat", .{pid}); +pub fn getProcessInfo(io: std.Io, allocator: std.mem.Allocator, pid: std.c.pid_t) !ProcessInfo { + if (builtin.os.tag != .linux and builtin.os.tag != .openbsd) return error.UnsupportedOs; - const file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only }); - defer file.close(); + var path_buf: [std.Io.Dir.max_path_bytes]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, "/proc/{d}/stat", .{pid}); - var file_buffer: [4096]u8 = undefined; - const bytes_read = try file.readAll(&file_buffer); - const content = file_buffer[0..bytes_read]; + const file = try std.Io.Dir.openFileAbsolute(io, path, .{ .mode = .read_only }); + defer file.close(io); - const name_start = std.mem.indexOf(u8, content, "(") orelse return error.InvalidStatFormat; - const name_end = std.mem.lastIndexOf(u8, content, ")") orelse return error.InvalidStatFormat; + var file_buffer: [4096]u8 = undefined; + var file_reader = file.reader(io, &.{}); - if (name_end <= name_start + 1) return error.InvalidStatFormat; + const bytes_read = try file_reader.interface.readSliceShort(&file_buffer); + const content = file_buffer[0..bytes_read]; - const name = try alloc.dupe(u8, content[name_start + 1 .. name_end]); + const name_start = std.mem.indexOf(u8, content, "(") orelse return error.InvalidStatFormat; + const name_end = std.mem.lastIndexOf(u8, content, ")") orelse return error.InvalidStatFormat; - const after_name = content[name_end + 1 ..]; - var field_it = std.mem.tokenizeAny(u8, after_name, " \t\n"); + if (name_end <= name_start + 1) return error.InvalidStatFormat; - // Skip state field - _ = field_it.next() orelse return error.InvalidStatFormat; + const name = try allocator.dupe(u8, content[name_start + 1 .. name_end]); - const ppid_str = field_it.next() orelse return error.InvalidStatFormat; - const ppid = try std.fmt.parseInt(std.c.pid_t, ppid_str, 10); + const after_name = content[name_end + 1 ..]; + var field_it = std.mem.tokenizeAny(u8, after_name, " \t\n"); - return ProcessInfo{ - .name = name, - .ppid = ppid, - }; - }, - else => return error.UnsupportedOs, - } + // Skip state field + _ = field_it.next() orelse return error.InvalidStatFormat; + + const ppid_str = field_it.next() orelse return error.InvalidStatFormat; + const ppid = try std.fmt.parseInt(std.c.pid_t, ppid_str, 10); + + return ProcessInfo{ + .name = name, + .ppid = ppid, + }; } diff --git a/src/stats.zig b/src/stats.zig @@ -1,143 +1,144 @@ const std = @import("std"); const builtin = @import("builtin"); -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; - } - } +pub fn getBattery(io: std.Io, allocator: std.mem.Allocator) ![]const u8 { + if (builtin.os.tag != .linux and builtin.os.tag != .openbsd) return error.UnsupportedOs; + + var dir = try std.Io.Dir.openDirAbsolute(io, "/sys/class/power_supply/", .{ .iterate = true }); + defer dir.close(io); + + var smallest_battery: ?usize = null; + + // Walk dir and find the smallest battery. + var it = try dir.walk(allocator); + defer it.deinit(); + while (try it.next(io)) |entry| { + if (std.mem.startsWith(u8, entry.basename, "BAT")) { + const battery_no = try std.fmt.parseInt(usize, std.mem.trimStart(u8, entry.basename, "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_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&file_buffer); - - 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_reader.interface.takeDelimiter('\n') orelse return error.MemAvailableIsNotSpecified; - 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_reader.interface.takeDelimiter('\n'); // Skip "MemFree: x" - - const mem_available = lbl: { - const line = try file_reader.interface.takeDelimiter('\n') 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; - }; + if (smallest_battery == null) return error.NoBatteryDetected; + + // Read battery capacity. + var battery_capacity_path_buf: [std.Io.Dir.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.Io.Dir.openFileAbsolute( + io, + battery_capacity_path, + .{ .mode = .read_only }, + ); + defer battery_capacity_file.close(io); + + var battery_capacity_buf: [1024]u8 = undefined; + var battery_capacity_file_reader = battery_capacity_file.reader(io, &.{}); + + const battery_capacity_bytes = try battery_capacity_file_reader.interface.readSliceShort(&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.Io.Dir.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.Io.Dir.openFileAbsolute( + io, + battery_status_path, + .{ .mode = .read_only }, + ); + defer battery_status_file.close(io); + + var battery_status_buf: [1024]u8 = undefined; + var battery_status_file_reader = battery_status_file.reader(io, &.{}); + + const battery_status_bytes = try battery_status_file_reader.interface.readSliceShort(&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 allocator.dupe(u8, output); +} - 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 getMemory(io: std.Io, allocator: std.mem.Allocator, options: struct { mb: bool }) ![]const u8 { + if (builtin.os.tag != .linux and builtin.os.tag != .openbsd) return error.UnsupportedOs; + + const file = try std.Io.Dir.openFileAbsolute(io, "/proc/meminfo", .{ .mode = .read_only }); + defer file.close(io); + + var file_buffer: [1024]u8 = undefined; + var file_reader = file.reader(io, &file_buffer); + + 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_reader.interface.takeDelimiter('\n') orelse return error.MemAvailableIsNotSpecified; + 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_reader.interface.takeDelimiter('\n'); // Skip "MemFree: x" + + const mem_available = lbl: { + const line = try file_reader.interface.takeDelimiter('\n') 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 allocator.dupe(u8, mem_str); } pub fn getUptime() !struct { days: isize, minutes: isize, hours: isize } { switch (builtin.os.tag) { .linux, .openbsd => { var info: std.os.linux.Sysinfo = undefined; - const result: usize = std.os.linux.sysinfo(&info); - if (std.os.linux.E.init(result) != .SUCCESS) { + const result: std.os.linux.E = @enumFromInt(std.os.linux.sysinfo(&info)); + if (result != .SUCCESS) { return error.UnknownUptime; } const uptime = info.uptime; @@ -152,32 +153,33 @@ pub fn getUptime() !struct { days: isize, minutes: isize, hours: isize } { .hours = uptime_hours, }; }, - .macos => { - var boot_timeval: std.posix.timeval = undefined; - var size: usize = @sizeOf(@TypeOf(boot_timeval)); - const kern_boottime = [_]c_int{ 1, 21 }; // CTL_KERN, KERN_BOOTTIME return void on macos - std.posix.sysctl( - &kern_boottime, - @ptrCast(&boot_timeval), - &size, - null, - 0, - ) catch return error.SysctlFailed; - - const now = std.time.timestamp(); - const boot_time = boot_timeval.sec; - const uptime = now - boot_time; - - const uptime_days = @divTrunc(uptime, 86400); - const uptime_hours = @divTrunc(@rem(uptime, 86400), 3600); - const uptime_minutes = @divTrunc(@rem(uptime, 3600), 60); - - return .{ - .days = uptime_days, - .minutes = uptime_minutes, - .hours = uptime_hours, - }; - }, + // TODO: upgrade to 0.16.0 + // .macos => { + // var boot_timeval: std.posix.timeval = undefined; + // var size: usize = @sizeOf(@TypeOf(boot_timeval)); + // const kern_boottime = [_]c_int{ 1, 21 }; // CTL_KERN, KERN_BOOTTIME return void on macos + // std.posix.sysctl( + // &kern_boottime, + // @ptrCast(&boot_timeval), + // &size, + // null, + // 0, + // ) catch return error.SysctlFailed; + // + // const now = std.time.timestamp(); + // const boot_time = boot_timeval.sec; + // const uptime = now - boot_time; + // + // const uptime_days = @divTrunc(uptime, 86400); + // const uptime_hours = @divTrunc(@rem(uptime, 86400), 3600); + // const uptime_minutes = @divTrunc(@rem(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 @@ -3,17 +3,18 @@ const builtin = @import("builtin"); const process = @import("process.zig"); -pub fn getCpu(alloc: std.mem.Allocator) ![]const u8 { +pub fn getCpu(io: std.Io, allocator: std.mem.Allocator) ![]const u8 { switch (builtin.os.tag) { .linux, .openbsd => { - const file = try std.fs.openFileAbsolute( + const file = try std.Io.Dir.openFileAbsolute( + io, "/proc/cpuinfo", .{ .mode = .read_only }, ); - defer file.close(); + defer file.close(io); var file_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&file_buffer); + var file_reader = file.reader(io, &file_buffer); while (try file_reader.interface.takeDelimiter('\n')) |line| { if (std.mem.startsWith(u8, line, "model name")) { @@ -21,32 +22,32 @@ pub fn getCpu(alloc: std.mem.Allocator) ![]const u8 { _ = 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 try allocator.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; - }, + // TODO: upgrade to 0.16.0 + // .macos => { + // const child = try std.process.Child.run(.{ + // .allocator = allocator, + // .argv = &[_][]const u8{ "sysctl", "-n", "machdep.cpu.brand_string" }, + // }); + // defer allocator.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(); +pub fn getShell(io: std.Io, allocator: std.mem.Allocator, env_map: *std.process.Environ.Map) ![]const u8 { + if (builtin.os.tag != .linux and builtin.os.tag != .openbsd) return error.UnsupportedOs; if (env_map.get("SHELL")) |shell_path| { var shell_it = std.mem.tokenizeScalar(u8, shell_path, '/'); @@ -56,61 +57,56 @@ pub fn getShell(alloc: std.mem.Allocator) ![]const u8 { shell = split; } - return try alloc.dupe(u8, shell); + return try allocator.dupe(u8, shell); } // If $SHELL is not set, try find the shell via pid. // Walk up the process tree: current -> parent -> grandparent - const parent_info = try process.getProcessInfo(alloc, std.os.linux.getppid()); - defer alloc.free(parent_info.name); + const parent_info = try process.getProcessInfo(io, allocator, std.os.linux.getppid()); + defer allocator.free(parent_info.name); - const grandparent_info = try process.getProcessInfo(alloc, parent_info.ppid); + const grandparent_info = try process.getProcessInfo(io, allocator, parent_info.ppid); return grandparent_info.name; } -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); - } +pub fn getDesktop(allocator: std.mem.Allocator, env_map: *std.process.Environ.Map) ![]const u8 { + if (builtin.os.tag != .linux and builtin.os.tag != .openbsd) return error.UnsupportedOs; - if (env_map.get("XDG_SESSION_DESKTOP")) |desktop| { - return try alloc.dupe(u8, desktop); - } + if (env_map.get("XDG_CURRENT_DESKTOP")) |desktop| { + return try allocator.dupe(u8, desktop); + } - if (env_map.get("DESKTOP_SESSION")) |desktop| { - return try alloc.dupe(u8, desktop); - } + if (env_map.get("XDG_SESSION_DESKTOP")) |desktop| { + return try allocator.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"); - } - } + if (env_map.get("DESKTOP_SESSION")) |desktop| { + return try allocator.dupe(u8, desktop); + } - return try alloc.dupe(u8, "Unknown"); - }, - else => return error.UnsupportedOs, + if (env_map.get("XDG_SESSION_TYPE")) |session_type| { + if (std.mem.eql(u8, session_type, "tty")) { + return try allocator.dupe(u8, "Headless"); + } } + + return try allocator.dupe(u8, "Unknown"); } -pub fn getDistro(alloc: std.mem.Allocator) ![]const u8 { +pub fn getDistro(io: std.Io, allocator: std.mem.Allocator) ![]const u8 { switch (builtin.os.tag) { .linux, .openbsd => { const utsname = std.posix.uname(); - const file = try std.fs.openFileAbsolute( + const file = try std.Io.Dir.openFileAbsolute( + io, "/etc/os-release", .{ .mode = .read_only }, ); - defer file.close(); + defer file.close(io); var file_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&file_buffer); + var file_reader = file.reader(io, &file_buffer); while (try file_reader.interface.takeDelimiter('\n')) |line| { if (std.mem.startsWith(u8, line, "PRETTY_NAME")) { @@ -124,7 +120,7 @@ pub fn getDistro(alloc: std.mem.Allocator) ![]const u8 { "{s} {s}", .{ std.mem.trim(u8, name, "\""), std.mem.sliceTo(&utsname.machine, 0) }, ); - return try alloc.dupe(u8, name_machine); + return try allocator.dupe(u8, name_machine); } } @@ -139,30 +135,31 @@ pub fn getDistro(alloc: std.mem.Allocator) ![]const u8 { "MacOS X {s}", .{std.mem.sliceTo(&utsname.machine, 0)}, ); - return try alloc.dupe(u8, name_machine); + return try allocator.dupe(u8, name_machine); }, else => return error.UnsupportedOs, } } -pub fn getDistroId(alloc: std.mem.Allocator) ![]const u8 { +pub fn getDistroId(io: std.Io, allocator: std.mem.Allocator) ![]const u8 { switch (builtin.os.tag) { .linux, .openbsd => { - const file = try std.fs.openFileAbsolute( + const file = try std.Io.Dir.openFileAbsolute( + io, "/etc/os-release", .{ .mode = .read_only }, ); - defer file.close(); + defer file.close(io); var file_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&file_buffer); + var file_reader = file.reader(io, &file_buffer); while (try file_reader.interface.takeDelimiter('\n')) |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 try allocator.dupe(u8, id); } } @@ -170,69 +167,62 @@ pub fn getDistroId(alloc: std.mem.Allocator) ![]const u8 { }, .macos => { const utsname = std.posix.uname(); - return try alloc.dupe(u8, std.mem.sliceTo(&utsname.nodename, 0)); + return try allocator.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(); - +pub fn getUser(allocator: std.mem.Allocator, env_map: *std.process.Environ.Map) ![]const u8 { const user_env = env_map.get("USER") orelse return error.USEREnvNotSet; - return try alloc.dupe(u8, user_env); + return try allocator.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, - " ", - ); +pub fn getDisk(io: std.Io, allocator: std.mem.Allocator) ![]const u8 { + if (builtin.os.tag != .linux and builtin.os.tag != .openbsd) return error.UnsupportedOs; - var line_it = std.mem.tokenizeScalar(u8, line, ' '); + const child = try std.process.run(allocator, io, .{ + .argv = &[_][]const u8{ "df", "-h", "--output=size,used,pcent", "/" }, + }); + defer allocator.free(child.stdout); + defer allocator.free(child.stderr); - 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, + 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 allocator.dupe(u8, output); }