From 9781ef0acbc6759eed48ff1c93489fad30386417 Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Tue, 27 Sep 2022 18:27:36 +0200 Subject: [PATCH] feat: switch to lightweight ini config format --- .gitmodules | 6 +- README.md | 20 ++-- build.zig | 18 ++-- libs/ini | 1 + libs/tomlc99 | 1 - src/config.zig | 262 +++++++++++++++++++++++++++++++++++++------------ src/ffi.zig | 1 - src/main.zig | 32 +++++- 8 files changed, 250 insertions(+), 91 deletions(-) create mode 160000 libs/ini delete mode 160000 libs/tomlc99 diff --git a/.gitmodules b/.gitmodules index 6faee65..93adb22 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "libs/tomlc99"] - path = libs/tomlc99 - url = https://github.com/cktan/tomlc99.git [submodule "libs/known-folders"] path = libs/known-folders url = https://github.com/ziglibs/known-folders.git +[submodule "libs/ini"] + path = libs/ini + url = https://github.com/ziglibs/ini diff --git a/README.md b/README.md index 4076232..ca643f6 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,24 @@ A power dialog written in zig using GTK4. ![screenshot](img/screenshot.png) ## Configuration -gpower2 uses a config file located at `$XDG_CONFIG_HOME/gpower/config.toml`. +gpower2 uses a config file located at `$XDG_CONFIG_HOME/gpower/config.ini`. This file configures how gpower2 will execute actions when a button is clicked. This is the default config: -```toml -shutdown_command = ["systemctl", "poweroff"] -reboot_command = ["systemctl", "reboot"] -suspend_command = ["systemctl", "suspend"] -hibernate_command = ["systemctl", "hibernate"] +```ini +[commands] +shutdown = systemctl poweroff +reboot = systemctl reboot +suspend = systemctl suspend +hibernate = systemctl hibernate ``` This config will be used if the config file can't be opened. If a value is omitted, gpower2 will use the default. Example custom config to activate i3lock before suspend/hibernate: -```toml -suspend_command = ["sh", "-c", "i3lock -c 660000 && systemctl suspend"] -hibernate_command = ["sh", "-c", "i3lock -c 660000 && systemctl hibernate"] +```ini +[commands] +suspend = sh -c 'i3lock -c 660000 && systemctl suspend' +hibernate = sh -c 'i3lock -c 660000 && systemctl hibernate' ``` ## Dependencies diff --git a/build.zig b/build.zig index 3ea1fab..f2ac744 100644 --- a/build.zig +++ b/build.zig @@ -14,15 +14,8 @@ pub fn build(b: *std.build.Builder) void { const exe = b.addExecutable("gpower2", "src/main.zig"); exe.setTarget(target); exe.setBuildMode(mode); + addLibs(exe); - exe.linkLibC(); - exe.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); - exe.linkSystemLibrary("gtk4"); - exe.addIncludeDir("libs/tomlc99"); - exe.addCSourceFile("libs/tomlc99/toml.c", &[0][]u8{}); - - // needed to prevent crash caused by UBSAN because the tomlc99 has some UB - exe.disable_sanitize_c = true; exe.strip = mode != .Debug; exe.install(); @@ -39,7 +32,16 @@ pub fn build(b: *std.build.Builder) void { const exe_tests = b.addTest("src/main.zig"); exe_tests.setTarget(target); exe_tests.setBuildMode(mode); + addLibs(exe_tests); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&exe_tests.step); } + +fn addLibs(step: *std.build.LibExeObjStep) void { + step.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); + step.addPackagePath("ini", "libs/ini/src/ini.zig"); + + step.linkLibC(); + step.linkSystemLibrary("gtk4"); +} diff --git a/libs/ini b/libs/ini new file mode 160000 index 0000000..0591af0 --- /dev/null +++ b/libs/ini @@ -0,0 +1 @@ +Subproject commit 0591af0178f9022cf50b9c50de07b8fe8f70f8b4 diff --git a/libs/tomlc99 b/libs/tomlc99 deleted file mode 160000 index 034b23e..0000000 --- a/libs/tomlc99 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 034b23ed3e4e5ee5345040eabed470f204d7f668 diff --git a/src/config.zig b/src/config.zig index e755a97..790c199 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,5 +1,6 @@ const std = @import("std"); const kf = @import("known-folders"); +const ini = @import("ini"); const c = @import("ffi.zig").c; pub var global_config: ?*Config = null; @@ -12,80 +13,83 @@ pub const Config = struct { alloc: std.mem.Allocator, command_arena: std.heap.ArenaAllocator, - toml: ?*c.toml_table_t, pub fn parse(alloc: std.mem.Allocator) !Config { - var toml: ?*c.toml_table_t = null; - toml: { - var config_dir = kf.open( - alloc, - .roaming_configuration, - .{}, - ) catch break :toml orelse break :toml; - defer config_dir.close(); - - const config = config_dir.realpathAlloc(alloc, "gpower2/config.toml") catch { - std.log.warn("Couldn't open config directory, skipping config", .{}); - break :toml; - }; - defer alloc.free(config); - const config_z = try std.cstr.addNullByte(alloc, config); - defer alloc.free(config_z); - - const c_file = c.fopen(config_z, "r"); - - if (c_file == null) { - std.log.warn("Opening config failed, skipping config", .{}); - break :toml; - } - - defer _ = c.fclose(c_file); - - var errbuf: [50:0]u8 = undefined; - toml = c.toml_parse_file(c_file, &errbuf, errbuf.len); - if (toml == null) { - std.log.err("Failed to parse TOML config: {s}", .{errbuf}); - return error.TomlError; - } - } - - var arena = std.heap.ArenaAllocator.init(alloc); - errdefer arena.deinit(); - var self = Config{ .alloc = alloc, - .command_arena = arena, - .toml = toml, + .command_arena = std.heap.ArenaAllocator.init(alloc), }; - if (toml) |t| { - errdefer c.toml_free(t); - inline for (.{ - .shutdown_command, - .reboot_command, - .suspend_command, - .hibernate_command, - }) |f_tag| { - const f = @tagName(f_tag); - const array = c.toml_array_in(t, f); + var config_dir = (kf.open( + alloc, + .roaming_configuration, + .{}, + ) catch return self) orelse return self; + defer config_dir.close(); - if (array) |a| { - var len = c.toml_array_nelem(a); - var i: usize = 0; + const file = config_dir.openFile("gpower2/config.ini", .{}) catch |err| { + std.log.warn("Failed to open config file ({e}). Skipping config!", .{err}); + return self; + }; - var cmd = try arena.allocator().alloc([]u8, @intCast(usize, len)); + var parser = ini.parse(std.heap.c_allocator, file.reader()); + defer parser.deinit(); - while (i < len) : (i += 1) { - const maybe_s = c.toml_string_at(a, @intCast(c_int, i)); - if (maybe_s.ok == 0) { - std.log.err("{s} in config contains non-string value!", .{f}); - return error.InvalidConfig; - } - cmd[i] = try arena.allocator().dupe(u8, std.mem.sliceTo(maybe_s.u.s, 0)); + var current_section: ?[]const u8 = null; + while (try parser.next()) |rec| { + switch (rec) { + .section => |sec| { + if (current_section) |prev| { + std.heap.c_allocator.free(prev); + } + current_section = try std.heap.c_allocator.dupe(u8, sec); + }, + .property => |kv| { + if (current_section == null or + !std.mem.eql(u8, "commands", current_section.?)) + { + std.log.err( + \\Config contained property outside of 'commands' section! + , + .{}, + ); + return error.PropertyOutsideCommandSection; } - @field(self, f) = cmd; - } + if (std.mem.eql(u8, "shutdown", kv.key)) { + self.shutdown_command = try CommandParser.parse( + kv.value, + self.command_arena.allocator(), + ); + } else if (std.mem.eql(u8, "reboot", kv.key)) { + self.reboot_command = try CommandParser.parse( + kv.value, + self.command_arena.allocator(), + ); + } else if (std.mem.eql(u8, "suspend", kv.key)) { + self.suspend_command = try CommandParser.parse( + kv.value, + self.command_arena.allocator(), + ); + } else if (std.mem.eql(u8, "hibernate", kv.key)) { + self.hibernate_command = try CommandParser.parse( + kv.value, + self.command_arena.allocator(), + ); + } else { + std.log.err("Unknown config property '{s}'!", .{kv.key}); + return error.UnknownProperty; + } + }, + .enumeration => { + std.log.err( + \\Found enumeration in config file! + \\Only sections and properties are allowed! + , + .{}, + ); + return error.EnumerationInConfig; + }, } } @@ -93,8 +97,136 @@ pub const Config = struct { } pub fn deinit(self: *Config) void { - if (self.toml) |t| { - c.toml_free(t); + self.command_arena.deinit(); + self.* = undefined; + } +}; + +const CommandParser = struct { + const State = enum { + default, + single_quote, + double_quote, + }; + + str: []const u8, + offset: usize = 0, + state: State = .default, + component: std.ArrayList(u8), + + /// Parses a command and returns the argv array, allocated using alloc. + fn parse(str: []const u8, alloc: std.mem.Allocator) ![][]u8 { + var parser = init(str, alloc); + defer parser.deinit(); + + var out = std.ArrayList([]u8).init(alloc); + errdefer out.deinit(); + + while (try parser.next()) |part| { + try out.append(part); + } + + return out.toOwnedSlice(); + } + + fn init(str: []const u8, alloc: std.mem.Allocator) CommandParser { + return .{ + .str = str, + .component = std.ArrayList(u8).init(alloc), + }; + } + + fn deinit(self: *CommandParser) void { + self.component.deinit(); + self.* = undefined; + } + + /// Parses the next argv item. + /// The returned slice will be owned by the provided alloc. + /// Returns null if there are no more items left. + fn next(self: *CommandParser) !?[]u8 { + if (self.offset >= self.str.len) + return null; + + self.component.clearRetainingCapacity(); + + while (self.offset < self.str.len) { + const ch = self.str[self.offset]; + self.offset += 1; + + switch (self.state) { + .default => { + if (std.ascii.isSpace(ch)) { + // double space safety + if (self.component.items.len > 0) + break; + continue; + } + + switch (ch) { + '\'' => self.state = .single_quote, + '\"' => self.state = .double_quote, + '\\' => try self.escapeSeq(), + else => try self.component.append(ch), + } + }, + + .single_quote => switch (ch) { + '\'' => self.state = .default, + '\\' => try self.escapeSeq(), + else => try self.component.append(ch), + }, + + .double_quote => switch (ch) { + '\"' => self.state = .default, + '\\' => try self.escapeSeq(), + else => try self.component.append(ch), + }, + } + } + + if (self.state != .default) { + return error.UnclosedDelimeter; + } + + return try self.component.allocator.dupe(u8, self.component.items); + } + + fn escapeSeq(self: *CommandParser) !void { + if (self.offset >= self.str.len) + return error.UnfinishedEscape; + + const ch = self.str[self.offset]; + self.offset += 1; + + if (self.state == .single_quote) { + if (ch != '\'') + try self.component.append('\\'); + try self.component.append(ch); + } else { + try self.component.append(ch); } } }; + +fn assertParserNext(p: *CommandParser, expected: []const u8) !void { + const n = try p.next(); + try std.testing.expect(n != null); + + defer p.component.allocator.free(n.?); + + try std.testing.expectEqualStrings(expected, n.?); +} + +test "CommandParser" { + var p1 = CommandParser.init( + \\"foo\" "bar 'baz\'' \\ + , + std.testing.allocator, + ); + defer p1.deinit(); + + try assertParserNext(&p1, "foo\" bar"); + try assertParserNext(&p1, "baz'"); + try assertParserNext(&p1, "\\"); +} diff --git a/src/ffi.zig b/src/ffi.zig index f23fda1..a56680e 100644 --- a/src/ffi.zig +++ b/src/ffi.zig @@ -1,7 +1,6 @@ // partially yoinked from https://github.com/Swoogan/ziggtk pub const c = @cImport({ @cInclude("gtk/gtk.h"); - @cInclude("toml.h"); }); /// Could not get `g_signal_connect` to work. Zig says "use of undeclared identifier". Reimplemented here diff --git a/src/main.zig b/src/main.zig index d5f4888..411a7aa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,6 +4,12 @@ const ffi = @import("ffi.zig"); const c = ffi.c; const gui = @import("gui.zig"); +test { + _ = @import("config.zig"); + _ = @import("ffi.zig"); + _ = @import("gui.zig"); +} + pub fn log( comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), @@ -17,17 +23,18 @@ pub fn log( .debug => c.G_LOG_LEVEL_DEBUG, }; - const s = std.fmt.allocPrintZ( - std.heap.c_allocator, + var buf: [1024]u8 = undefined; + + const s = std.fmt.bufPrintZ( + &buf, format, args, ) catch return; - defer std.heap.c_allocator.free(s); var fields = [_]c.GLogField{ c.GLogField{ .key = "GLIB_DOMAIN", - .value = "gpower2-" ++ @tagName(scope), + .value = if (scope == .default) "gpower2" else "gpower2-" ++ @tagName(scope), .length = -1, }, c.GLogField{ @@ -44,11 +51,28 @@ pub fn log( ); } +// glib filters log messages +pub const log_level = .debug; + pub fn main() !u8 { var conf = try cfg.Config.parse(std.heap.c_allocator); defer conf.deinit(); cfg.global_config = &conf; + std.log.debug( + \\Using Configs: + \\ + \\Shutdown Command: {s} + \\Reboot Command: {s} + \\Suspend Command: {s} + \\Hibernate Command: {s} + , .{ + conf.shutdown_command, + conf.reboot_command, + conf.suspend_command, + conf.hibernate_command, + }); + var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); defer arena.deinit();