diff --git a/.github/release-nest-v3 b/.github/release-nest-v3 index d8263ee..56a6051 100644 --- a/.github/release-nest-v3 +++ b/.github/release-nest-v3 @@ -1 +1 @@ -2 \ No newline at end of file +1 \ No newline at end of file diff --git a/pikabar/debian/changelog b/pikabar/debian/changelog index 0eea005..d0af9c2 100644 --- a/pikabar/debian/changelog +++ b/pikabar/debian/changelog @@ -1,3 +1,9 @@ +pikabar (1.0.0-101pika18) pika; urgency=medium + + * gostat replaced with zigstat + + -- ferrreo Sat, 01 Oct 2022 14:50:00 +0300 + pikabar (1.0.0-101pika17) pika; urgency=medium * Less of a chonker diff --git a/pikabar/usr/share/pikabar/programs/gostat b/pikabar/usr/share/pikabar/programs/gostat deleted file mode 100755 index 6eba7c5..0000000 Binary files a/pikabar/usr/share/pikabar/programs/gostat and /dev/null differ diff --git a/pikabar/usr/share/pikabar/programs/zigstat b/pikabar/usr/share/pikabar/programs/zigstat new file mode 100755 index 0000000..417a154 Binary files /dev/null and b/pikabar/usr/share/pikabar/programs/zigstat differ diff --git a/pikabar/usr/share/pikabar/widgets/Stats.qml b/pikabar/usr/share/pikabar/widgets/Stats.qml index 4931ec0..68cf9ac 100644 --- a/pikabar/usr/share/pikabar/widgets/Stats.qml +++ b/pikabar/usr/share/pikabar/widgets/Stats.qml @@ -17,9 +17,9 @@ Item { property string memoryUsage: "" Process { - id: gostatProcess + id: zigstatProcess running: true - command: [Quickshell.shellRoot + "/programs/gostat", root.updateInterval] + command: [Quickshell.shellRoot + "/programs/zigstat", root.updateInterval] stdout: SplitParser { onRead: function (line) { try { @@ -28,7 +28,7 @@ Item { root.cpuTemp = data.cputemp + "°C"; root.memoryUsage = data.mem + "G"; } catch (e) { - console.error("Failed to parse gostat output:", e); + console.error("Failed to parse zigstat output:", e); } } } diff --git a/src/zigstat/build.zig b/src/zigstat/build.zig new file mode 100644 index 0000000..e6587b8 --- /dev/null +++ b/src/zigstat/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "zigstat", + .root_source_file = .{ .cwd_relative = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(exe); +} diff --git a/src/zigstat/build.zig.zon b/src/zigstat/build.zig.zon new file mode 100644 index 0000000..a8f41dc --- /dev/null +++ b/src/zigstat/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "zigstat", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.1.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/zigstat/src/main.zig b/src/zigstat/src/main.zig new file mode 100644 index 0000000..f901448 --- /dev/null +++ b/src/zigstat/src/main.zig @@ -0,0 +1,260 @@ +const std = @import("std"); +const fs = std.fs; +const mem = std.mem; +const time = std.time; +const json = std.json; +const math = std.math; + +// Shared static buffers +const SharedBuffers = struct { + path_buf: [256]u8 = undefined, + read_buf: [256]u8 = undefined, +}; + +// Cache for temperature sensor paths +const TempSensorCache = struct { + initialized: bool = false, + sensor_type: enum { none, coretemp, k10temp } = .none, + path: [256]u8 = undefined, + path_len: usize = 0, + + fn init(self: *TempSensorCache, allocator: std.mem.Allocator, bufs: *SharedBuffers) !void { + if (self.initialized) return; + + var hwmon_dir = try fs.openDirAbsolute("/sys/class/hwmon", .{ .iterate = true }); + defer hwmon_dir.close(); + + var iter = hwmon_dir.iterate(); + while (try iter.next()) |entry| { + const name_path = try std.fmt.bufPrint(&bufs.path_buf, "/sys/class/hwmon/{s}/name", .{entry.name}); + const name_content = fs.cwd().readFileAlloc(allocator, name_path, 64) catch continue; + defer allocator.free(name_content); + + const trimmed_name = mem.trim(u8, name_content, "\n"); + if (mem.eql(u8, trimmed_name, "coretemp")) { + self.sensor_type = .coretemp; + const path_slice = try std.fmt.bufPrint(&self.path, "/sys/class/hwmon/{s}/", .{entry.name}); + self.path_len = path_slice.len; + break; + } else if (mem.eql(u8, trimmed_name, "k10temp")) { + self.sensor_type = .k10temp; + const path_slice = try std.fmt.bufPrint(&self.path, "/sys/class/hwmon/{s}/", .{entry.name}); + self.path_len = path_slice.len; + break; + } + } + + self.initialized = true; + } +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var shared_bufs = SharedBuffers{}; + var temp_cache = TempSensorCache{}; + + // Default sleep duration of 3 seconds + var sleep_duration: u64 = 3 * time.ns_per_s; + + // Parse command line args for custom duration + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + if (args.len > 1) { + const duration_str = args[1]; + if (parseDuration(duration_str)) |duration| { + sleep_duration = duration; + } else |_| { + std.debug.print("Invalid duration format. Using default of 3s.\n", .{}); + } + } + + const stdout = std.io.getStdOut().writer(); + + while (true) { + const mem_info = try getMemoryUsage(&shared_bufs); + const cpu_usage = try getCpuUsage(&shared_bufs); + const cpu_temp = try getCpuTemp(allocator, &shared_bufs, &temp_cache); + + try stdout.print("{{ \"mem\":\"{d:.1}\", \"cpu\": \"{d:.1}\", \"cputemp\": \"{d}\" }}\n", .{ mem_info, cpu_usage, cpu_temp }); + + time.sleep(sleep_duration); + } +} + +fn getMemoryUsage(bufs: *SharedBuffers) !f64 { + const file = try fs.openFileAbsolute("/proc/meminfo", .{}); + defer file.close(); + + var total: u64 = 0; + var available: u64 = 0; + var found_count: u8 = 0; + + // Read the file in one go + const bytes_read = try file.read(&bufs.read_buf); + var start: usize = 0; + var i: usize = 0; + + // Manual line parsing to avoid buffered reader overhead + while (i < bytes_read and found_count < 2) { + if (bufs.read_buf[i] == '\n' or i == bytes_read - 1) { + const line = bufs.read_buf[start..i]; + + if (mem.indexOf(u8, line, "MemTotal:") != null) { + var it = mem.tokenize(u8, line, " kB"); + _ = it.next(); + total = try std.fmt.parseInt(u64, it.next() orelse "0", 10); + found_count += 1; + } else if (mem.indexOf(u8, line, "MemAvailable:") != null) { + var it = mem.tokenize(u8, line, " kB"); + _ = it.next(); + available = try std.fmt.parseInt(u64, it.next() orelse "0", 10); + found_count += 1; + } + + start = i + 1; + } + i += 1; + } + + const byte_to_giga = 1000000.0; + return @as(f64, @floatFromInt(total - available)) / byte_to_giga; +} + +fn getCpuUsage(bufs: *SharedBuffers) !f64 { + const first = try getCpuTimes(bufs); + time.sleep(50 * time.ns_per_ms); + const second = try getCpuTimes(bufs); + + const idle_diff = second.idle - first.idle; + const total_diff = second.total - first.total; + + if (total_diff == 0) return 0; + return (1.0 - @as(f64, @floatFromInt(idle_diff)) / @as(f64, @floatFromInt(total_diff))) * 100.0; +} + +const CpuTimes = struct { + idle: u64, + total: u64, +}; + +fn getCpuTimes(bufs: *SharedBuffers) !CpuTimes { + const file = try fs.openFileAbsolute("/proc/stat", .{}); + defer file.close(); + + const bytes_read = try file.read(&bufs.read_buf); + const first_line = mem.sliceTo(bufs.read_buf[0..bytes_read], '\n'); + + if (mem.startsWith(u8, first_line, "cpu ")) { + var it = mem.tokenize(u8, first_line, " "); + _ = it.next(); + + var total: u64 = 0; + var idle: u64 = 0; + var i: u32 = 0; + + while (it.next()) |val| { + const num = try std.fmt.parseInt(u64, val, 10); + if (i == 3) idle = num; + if (i == 4) idle += num; + total += num; + i += 1; + if (i >= 5) break; + } + + return CpuTimes{ .idle = idle, .total = total }; + } + + return error.NoCpuLine; +} + +fn getCpuTemp(allocator: std.mem.Allocator, bufs: *SharedBuffers, cache: *TempSensorCache) !i32 { + try cache.init(allocator, bufs); + + if (cache.sensor_type == .none) return 0; + + const path = cache.path[0..cache.path_len]; + return switch (cache.sensor_type) { + .coretemp => try getCoreTemp(allocator, path, bufs), + .k10temp => try getK10Temp(allocator, path, bufs), + .none => 0, + }; +} + +// Update getK10Temp and getCoreTemp to use the shared buffers +fn getK10Temp(allocator: std.mem.Allocator, hwmon_path: []const u8, bufs: *SharedBuffers) !i32 { + var dir = try fs.openDirAbsolute(hwmon_path, .{ .iterate = true }); + defer dir.close(); + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (mem.startsWith(u8, entry.name, "temp") and mem.endsWith(u8, entry.name, "_label")) { + const label_path = try std.fmt.bufPrint(&bufs.path_buf, "{s}{s}", .{ hwmon_path, entry.name }); + const label_content = fs.cwd().readFileAlloc(allocator, label_path, 64) catch continue; + defer allocator.free(label_content); + + if (mem.eql(u8, mem.trim(u8, label_content, "\n"), "Tctl")) { + const label_index = mem.indexOf(u8, entry.name, "_label").?; + const input_path = try std.fmt.bufPrint(&bufs.path_buf, "{s}{s}_input", .{ hwmon_path, entry.name[0..label_index] }); + + const temp_content = fs.cwd().readFileAlloc(allocator, input_path, 64) catch continue; + defer allocator.free(temp_content); + + const temp = try std.fmt.parseInt(i32, mem.trim(u8, temp_content, "\n"), 10); + return @divTrunc(temp, 1000); + } + } + } + + return 0; +} + +fn getCoreTemp(allocator: std.mem.Allocator, hwmon_path: []const u8, bufs: *SharedBuffers) !i32 { + var dir = try fs.openDirAbsolute(hwmon_path, .{ .iterate = true }); + defer dir.close(); + + var total_temp: i32 = 0; + var count: f64 = 0; + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (mem.startsWith(u8, entry.name, "temp") and mem.endsWith(u8, entry.name, "_input")) { + const temp_path = try std.fmt.bufPrint(&bufs.path_buf, "{s}{s}", .{ hwmon_path, entry.name }); + const temp_content = fs.cwd().readFileAlloc(allocator, temp_path, 64) catch continue; + defer allocator.free(temp_content); + + const temp = try std.fmt.parseInt(i32, mem.trim(u8, temp_content, "\n"), 10); + total_temp += temp; + count += 1; + } + } + + if (count > 0) { + return @divTrunc(@as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(total_temp)) / count))), 1000); + } + + return 0; +} + +/// Parses duration strings like "1s", "500ms", "1m", etc. +/// Returns the duration in nanoseconds +fn parseDuration(str: []const u8) !u64 { + if (str.len < 2) return error.InvalidFormat; + + const value_str = str[0 .. str.len - 1]; + const unit = str[str.len - 1]; + + const value = try std.fmt.parseInt(u64, value_str, 10); + + return switch (unit) { + 's' => value * time.ns_per_s, + 'm' => value * time.ns_per_s * 60, + 'h' => value * time.ns_per_s * 60 * 60, + else => if (mem.eql(u8, str[str.len - 2 ..], "ms")) + value * time.ns_per_ms + else + error.InvalidUnit, + }; +}