feat: switch to lightweight ini config format
This commit is contained in:
parent
4cc0ba139a
commit
9781ef0acb
|
@ -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
|
||||
|
|
20
README.md
20
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
|
||||
|
|
18
build.zig
18
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");
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 0591af0178f9022cf50b9c50de07b8fe8f70f8b4
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 034b23ed3e4e5ee5345040eabed470f204d7f668
|
262
src/config.zig
262
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, "\\");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
32
src/main.zig
32
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();
|
||||
|
||||
|
|
Loading…
Reference in New Issue