Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134//! What the app keeps between runs: one small file per name in//! $XDG_STATE_HOME/parcel, or ~/.local/state/parcel. It is state the UI//! changes, such as the list's filters, not configuration someone edits.//! The tests pass none, so they start from the defaults and write nothing.
const std = @import("std");const Io = std.Io;const Environ = std.process.Environ;const mem = std.mem;const log = std.log.scoped(.settings);
const Settings = @This();
io: Io,dir: Io.Dir,
/// Opens the directory, creating it. Null, logged, when there is no home/// directory to put it in or it cannot be made.pub fn open(io: Io, environ: *const Environ.Map) ?Settings { var buf: [Io.Dir.max_path_bytes]u8 = undefined; const path = if (environ.get("XDG_STATE_HOME")) |state| mem.print(&buf, "{s}/parcel", .{state}) catch return null else if (environ.get("HOME")) |home| mem.print(&buf, "{s}/.local/state/parcel", .{home}) catch return null else { log.warn("neither XDG_STATE_HOME nor HOME is set; nothing will be saved", .{}); return null; }; const dir = Io.Dir.cwd().createDirPathOpen(io, path, .{}) catch |err| { log.warn("{s}: {t}; nothing will be saved", .{ path, err }); return null; }; return .{ .io = io, .dir = dir };}
pub fn close(self: Settings) void { self.dir.close(self.io);}
/// The directory under this one that holds an account's directory each.pub const accounts_dir = "accounts";
/// Account `name`'s own settings, in a directory of its own under/// `accounts_dir`, created. Null, logged, when it cannot be made.pub fn account(self: Settings, name: []const u8) ?Settings { var buf: [Io.Dir.max_path_bytes]u8 = undefined; const path = mem.print(&buf, "{s}/{s}", .{ accounts_dir, name }) catch return null; const dir = self.dir.createDirPathOpen(self.io, path, .{}) catch |err| { log.warn("{s}: {t}; nothing of it will be saved", .{ path, err }); return null; }; return .{ .io = self.io, .dir = dir };}
/// The accounts' names, sorted, which is the order Ctrl and a digit/// counts them in: each directory under `accounts_dir`.pub fn accounts(self: Settings, arena: mem.Allocator) ![]const []const u8 { var dir = self.dir.openDir(self.io, accounts_dir, .{ .iterate = true }) catch |err| return switch (err) { error.FileNotFound => &.{}, else => err, }; defer dir.close(self.io); var names: std.ArrayList([]const u8) = .empty; var it = dir.iterate(); while (try it.next(self.io)) |entry| { if (entry.kind != .directory or entry.name[0] == '.') continue; try names.append(arena, try arena.dupe(u8, entry.name)); } mem.sort([]const u8, names.items, {}, lessThan); return names.items;}
fn lessThan(_: void, a: []const u8, b: []const u8) bool { return mem.order(u8, a, b) == .lt;}
/// Moves `files` from this directory to account `name`'s, as when the/// one account parcel had before becomes the first of several. Those/// missing are skipped.pub fn moveTo(self: Settings, name: []const u8, files: []const []const u8) !void { const to = self.account(name) orelse return error.NoAccountDirectory; defer to.close(); for (files) |file| { Io.Dir.rename(self.dir, file, to.dir, file, self.io) catch |err| switch (err) { error.FileNotFound => {}, else => return err, }; }}
/// What was saved under `name`, read into `buf`; null if nothing was.pub fn load(self: Settings, name: []const u8, buf: []u8) ?[]u8 { return self.dir.readFile(self.io, name, buf) catch |err| { if (err != error.FileNotFound) log.warn("{s}: {t}", .{ name, err }); return null; };}
/// What was saved under `name`, up to `limit`, which the caller frees;/// null if nothing was.pub fn loadAlloc(self: Settings, name: []const u8, gpa: mem.Allocator, limit: Io.Limit) ?[]u8 { return self.dir.readFileAlloc(self.io, name, gpa, limit) catch |err| { if (err != error.FileNotFound) log.warn("{s}: {t}", .{ name, err }); return null; };}
/// Forgets what was saved under `name`.pub fn remove(self: Settings, name: []const u8) void { self.dir.deleteFile(self.io, name) catch |err| { if (err != error.FileNotFound) log.warn("{s}: {t}", .{ name, err }); };}
/// Saves `bytes` under `name`, replacing the file whole so a crash midway/// leaves the old one. A failure is logged: losing a preference should not/// stop the app.pub fn save(self: Settings, name: []const u8, bytes: []const u8) void { write(self, name, bytes, .default_file) catch |err| log.warn("{s}: {t}", .{ name, err });}
/// Saves `bytes` only the user may read, such as a refresh token or/// their mail. A failure is returned for the caller to judge.pub fn saveSecret(self: Settings, name: []const u8, bytes: []const u8) !void { return write(self, name, bytes, @fromBackingInt(0o600));}
fn write(self: Settings, name: []const u8, bytes: []const u8, permissions: Io.File.Permissions) !void { var file = try self.dir.createFileAtomic(self.io, name, .{ .replace = true, .permissions = permissions }); defer file.deinit(self.io); try file.file.writeStreamingAll(self.io, bytes); try file.replace(self.io);}