feat: switch to lightweight ini config format
This commit is contained in:
parent
4cc0ba139a
commit
9781ef0acb
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,6 +1,6 @@
|
||||||
[submodule "libs/tomlc99"]
|
|
||||||
path = libs/tomlc99
|
|
||||||
url = https://github.com/cktan/tomlc99.git
|
|
||||||
[submodule "libs/known-folders"]
|
[submodule "libs/known-folders"]
|
||||||
path = libs/known-folders
|
path = libs/known-folders
|
||||||
url = https://github.com/ziglibs/known-folders.git
|
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)
|
![screenshot](img/screenshot.png)
|
||||||
|
|
||||||
## Configuration
|
## 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 file configures how gpower2 will execute actions when a button is clicked.
|
||||||
|
|
||||||
This is the default config:
|
This is the default config:
|
||||||
```toml
|
```ini
|
||||||
shutdown_command = ["systemctl", "poweroff"]
|
[commands]
|
||||||
reboot_command = ["systemctl", "reboot"]
|
shutdown = systemctl poweroff
|
||||||
suspend_command = ["systemctl", "suspend"]
|
reboot = systemctl reboot
|
||||||
hibernate_command = ["systemctl", "hibernate"]
|
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.
|
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:
|
Example custom config to activate i3lock before suspend/hibernate:
|
||||||
```toml
|
```ini
|
||||||
suspend_command = ["sh", "-c", "i3lock -c 660000 && systemctl suspend"]
|
[commands]
|
||||||
hibernate_command = ["sh", "-c", "i3lock -c 660000 && systemctl hibernate"]
|
suspend = sh -c 'i3lock -c 660000 && systemctl suspend'
|
||||||
|
hibernate = sh -c 'i3lock -c 660000 && systemctl hibernate'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## 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");
|
const exe = b.addExecutable("gpower2", "src/main.zig");
|
||||||
exe.setTarget(target);
|
exe.setTarget(target);
|
||||||
exe.setBuildMode(mode);
|
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.strip = mode != .Debug;
|
||||||
|
|
||||||
exe.install();
|
exe.install();
|
||||||
|
@ -39,7 +32,16 @@ pub fn build(b: *std.build.Builder) void {
|
||||||
const exe_tests = b.addTest("src/main.zig");
|
const exe_tests = b.addTest("src/main.zig");
|
||||||
exe_tests.setTarget(target);
|
exe_tests.setTarget(target);
|
||||||
exe_tests.setBuildMode(mode);
|
exe_tests.setBuildMode(mode);
|
||||||
|
addLibs(exe_tests);
|
||||||
|
|
||||||
const test_step = b.step("test", "Run unit tests");
|
const test_step = b.step("test", "Run unit tests");
|
||||||
test_step.dependOn(&exe_tests.step);
|
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
1
libs/ini
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 0591af0178f9022cf50b9c50de07b8fe8f70f8b4
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 034b23ed3e4e5ee5345040eabed470f204d7f668
|
|
256
src/config.zig
256
src/config.zig
|
@ -1,5 +1,6 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const kf = @import("known-folders");
|
const kf = @import("known-folders");
|
||||||
|
const ini = @import("ini");
|
||||||
const c = @import("ffi.zig").c;
|
const c = @import("ffi.zig").c;
|
||||||
|
|
||||||
pub var global_config: ?*Config = null;
|
pub var global_config: ?*Config = null;
|
||||||
|
@ -12,80 +13,83 @@ pub const Config = struct {
|
||||||
|
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
command_arena: std.heap.ArenaAllocator,
|
command_arena: std.heap.ArenaAllocator,
|
||||||
toml: ?*c.toml_table_t,
|
|
||||||
|
|
||||||
pub fn parse(alloc: std.mem.Allocator) !Config {
|
pub fn parse(alloc: std.mem.Allocator) !Config {
|
||||||
var toml: ?*c.toml_table_t = null;
|
var self = Config{
|
||||||
toml: {
|
.alloc = alloc,
|
||||||
var config_dir = kf.open(
|
.command_arena = std.heap.ArenaAllocator.init(alloc),
|
||||||
|
};
|
||||||
|
|
||||||
|
var config_dir = (kf.open(
|
||||||
alloc,
|
alloc,
|
||||||
.roaming_configuration,
|
.roaming_configuration,
|
||||||
.{},
|
.{},
|
||||||
) catch break :toml orelse break :toml;
|
) catch return self) orelse return self;
|
||||||
defer config_dir.close();
|
defer config_dir.close();
|
||||||
|
|
||||||
const config = config_dir.realpathAlloc(alloc, "gpower2/config.toml") catch {
|
const file = config_dir.openFile("gpower2/config.ini", .{}) catch |err| {
|
||||||
std.log.warn("Couldn't open config directory, skipping config", .{});
|
std.log.warn("Failed to open config file ({e}). Skipping config!", .{err});
|
||||||
break :toml;
|
return self;
|
||||||
};
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (toml) |t| {
|
var parser = ini.parse(std.heap.c_allocator, file.reader());
|
||||||
errdefer c.toml_free(t);
|
defer parser.deinit();
|
||||||
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);
|
|
||||||
|
|
||||||
if (array) |a| {
|
var current_section: ?[]const u8 = null;
|
||||||
var len = c.toml_array_nelem(a);
|
while (try parser.next()) |rec| {
|
||||||
var i: usize = 0;
|
switch (rec) {
|
||||||
|
.section => |sec| {
|
||||||
var cmd = try arena.allocator().alloc([]u8, @intCast(usize, len));
|
if (current_section) |prev| {
|
||||||
|
std.heap.c_allocator.free(prev);
|
||||||
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));
|
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 {
|
pub fn deinit(self: *Config) void {
|
||||||
if (self.toml) |t| {
|
self.command_arena.deinit();
|
||||||
c.toml_free(t);
|
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
|
// partially yoinked from https://github.com/Swoogan/ziggtk
|
||||||
pub const c = @cImport({
|
pub const c = @cImport({
|
||||||
@cInclude("gtk/gtk.h");
|
@cInclude("gtk/gtk.h");
|
||||||
@cInclude("toml.h");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Could not get `g_signal_connect` to work. Zig says "use of undeclared identifier". Reimplemented here
|
/// 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 c = ffi.c;
|
||||||
const gui = @import("gui.zig");
|
const gui = @import("gui.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = @import("config.zig");
|
||||||
|
_ = @import("ffi.zig");
|
||||||
|
_ = @import("gui.zig");
|
||||||
|
}
|
||||||
|
|
||||||
pub fn log(
|
pub fn log(
|
||||||
comptime level: std.log.Level,
|
comptime level: std.log.Level,
|
||||||
comptime scope: @TypeOf(.EnumLiteral),
|
comptime scope: @TypeOf(.EnumLiteral),
|
||||||
|
@ -17,17 +23,18 @@ pub fn log(
|
||||||
.debug => c.G_LOG_LEVEL_DEBUG,
|
.debug => c.G_LOG_LEVEL_DEBUG,
|
||||||
};
|
};
|
||||||
|
|
||||||
const s = std.fmt.allocPrintZ(
|
var buf: [1024]u8 = undefined;
|
||||||
std.heap.c_allocator,
|
|
||||||
|
const s = std.fmt.bufPrintZ(
|
||||||
|
&buf,
|
||||||
format,
|
format,
|
||||||
args,
|
args,
|
||||||
) catch return;
|
) catch return;
|
||||||
defer std.heap.c_allocator.free(s);
|
|
||||||
|
|
||||||
var fields = [_]c.GLogField{
|
var fields = [_]c.GLogField{
|
||||||
c.GLogField{
|
c.GLogField{
|
||||||
.key = "GLIB_DOMAIN",
|
.key = "GLIB_DOMAIN",
|
||||||
.value = "gpower2-" ++ @tagName(scope),
|
.value = if (scope == .default) "gpower2" else "gpower2-" ++ @tagName(scope),
|
||||||
.length = -1,
|
.length = -1,
|
||||||
},
|
},
|
||||||
c.GLogField{
|
c.GLogField{
|
||||||
|
@ -44,11 +51,28 @@ pub fn log(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glib filters log messages
|
||||||
|
pub const log_level = .debug;
|
||||||
|
|
||||||
pub fn main() !u8 {
|
pub fn main() !u8 {
|
||||||
var conf = try cfg.Config.parse(std.heap.c_allocator);
|
var conf = try cfg.Config.parse(std.heap.c_allocator);
|
||||||
defer conf.deinit();
|
defer conf.deinit();
|
||||||
cfg.global_config = &conf;
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue