Something went wrong. Try again.
htop for your network: a real-time IP traffic monitor for the terminal, ported to Zig from iptop
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145// SPDX-FileCopyrightText: © 2026 Francesc Muñoz// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The settings that survive a restart, in `$XDG_CONFIG_HOME/ziptop/config`//! (`~/.config/ziptop/config` when that is unset): plain `key = value` lines,//! so that it is easy to edit by hand, with `#` starting a comment. Reading//! and writing are best effort. A missing or unreadable file is the defaults,//! and a file that cannot be written is a setting that does not persist,//! never a reason not to start.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
/// The keys this program writes, in the order it writes them.const known_keys = [_][]const u8{"theme"};
pub const Config = struct { /// Every `key = value` in the file, including keys this version does not /// know, so that writing the file back does not lose them. values: std.StringArrayHashMapUnmanaged([]const u8) = .empty,
pub fn get(self: *const Config, key: []const u8) ?[]const u8 { const v = self.values.get(key) orelse return null; return if (v.len == 0) null else v; }
pub fn put(self: *Config, arena: Allocator, key: []const u8, value: []const u8) Allocator.Error!void { try self.values.put(arena, key, value); }};
pub fn parse(arena: Allocator, text: []const u8) Allocator.Error!Config { var config: Config = .{}; var lines = std.mem.splitScalar(u8, text, '\n'); while (lines.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len == 0 or line[0] == '#') continue; const eq = std.mem.findScalar(u8, line, '=') orelse continue; const key = std.mem.trim(u8, line[0..eq], " \t"); if (key.len == 0) continue; try config.values.put(arena, key, std.mem.trim(u8, line[eq + 1 ..], " \t")); } return config;}
fn isKnown(key: []const u8) bool { for (known_keys) |k| if (std.mem.eql(u8, k, key)) return true; return false;}
/// The known keys first, in a stable order, then whatever else the file had.pub fn serialize(config: *const Config, w: *Io.Writer) Io.Writer.Error!void { try w.writeAll("# ziptop config -- edit by hand, or with the theme picker (t)\n"); for (known_keys) |key| { const value = config.get(key) orelse continue; try w.print("{s} = {s}\n", .{ key, value }); } for (config.values.keys(), config.values.values()) |key, value| { if (isKnown(key) or value.len == 0) continue; try w.print("{s} = {s}\n", .{ key, value }); }}
/// The directory the file lives in.pub fn directory(arena: Allocator, environ: *const std.process.Environ.Map) Allocator.Error!?[]const u8 { if (environ.get("XDG_CONFIG_HOME")) |xdg| { if (xdg.len > 0) return try std.fs.path.join(arena, &.{ xdg, "ziptop" }); } const home = environ.get("HOME") orelse return null; return try std.fs.path.join(arena, &.{ home, ".config", "ziptop" });}
pub fn load(arena: Allocator, io: Io, dir_path: ?[]const u8) Config { const dir = dir_path orelse return .{}; const path = std.fs.path.join(arena, &.{ dir, "config" }) catch return .{}; const text = Io.Dir.cwd().readFileAlloc(io, path, arena, .limited(1 << 20)) catch return .{}; return parse(arena, text) catch .{};}
/// Writes `key = value` into the file, keeping everything else it has.pub fn save(gpa: Allocator, io: Io, dir_path: ?[]const u8, key: []const u8, value: []const u8) void { var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); const a = arena.allocator(); const dir = dir_path orelse return;
var config = load(a, io, dir); config.put(a, key, value) catch return;
var out: Io.Writer.Allocating = .init(a); serialize(&config, &out.writer) catch return;
const cwd = Io.Dir.cwd(); cwd.createDirPath(io, dir) catch return; const path = std.fs.path.join(a, &.{ dir, "config" }) catch return; cwd.writeFile(io, .{ .sub_path = path, .data = out.written() }) catch return;}
const testing = std.testing;
test "parse skips comments, blank lines and lines without a value" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const config = try parse(arena.allocator(), \\# a comment \\ \\theme = Nord \\nonsense \\ extra= kept ); try testing.expectEqualStrings("Nord", config.get("theme").?); try testing.expectEqualStrings("kept", config.get("extra").?); try testing.expect(config.get("nonsense") == null);}
test "serialize writes the known keys first and keeps the rest" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); var config = try parse(arena.allocator(), "extra = kept\ntheme = Nord\n"); try config.put(arena.allocator(), "theme", "Dracula"); var out: Io.Writer.Allocating = .init(arena.allocator()); try serialize(&config, &out.writer); try testing.expectEqualStrings( \\# ziptop config -- edit by hand, or with the theme picker (t) \\theme = Dracula \\extra = kept \\ , out.written());}
test "save and load round trip" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const base = try tmp.dir.realPathFileAlloc(testing.io, ".", arena.allocator()); const dir = try std.fs.path.join(arena.allocator(), &.{ base, "ziptop" });
try testing.expect(load(arena.allocator(), testing.io, dir).get("theme") == null); save(testing.allocator, testing.io, dir, "theme", "Gruvbox Dark"); try testing.expectEqualStrings("Gruvbox Dark", load(arena.allocator(), testing.io, dir).get("theme").?);}