feat: switch to lightweight ini config format

This commit is contained in:
LordMZTE 2022-09-27 18:27:36 +02:00
parent 4cc0ba139a
commit 9781ef0acb
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6
8 changed files with 250 additions and 91 deletions

6
.gitmodules vendored
View File

@ -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

View File

@ -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

View File

@ -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");
}

1
libs/ini Submodule

@ -0,0 +1 @@
Subproject commit 0591af0178f9022cf50b9c50de07b8fe8f70f8b4

@ -1 +0,0 @@
Subproject commit 034b23ed3e4e5ee5345040eabed470f204d7f668

View File

@ -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, "\\");
}

View File

@ -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

View File

@ -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();