commit 656cefd185b694196636a0d1d2333c0b645afc6b Author: LordMZTE Date: Mon May 16 15:01:42 2022 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e73c965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-cache/ +zig-out/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..064496e --- /dev/null +++ b/build.zig @@ -0,0 +1,38 @@ +const std = @import("std"); + +pub fn build(b: *std.build.Builder) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard release options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. + const mode = b.standardReleaseOptions(); + + const exe = b.addExecutable("gpower2", "src/main.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); + + exe.linkLibC(); + exe.linkSystemLibrary("gtk4"); + + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_tests = b.addTest("src/main.zig"); + exe_tests.setTarget(target); + exe_tests.setBuildMode(mode); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&exe_tests.step); +} diff --git a/src/action.zig b/src/action.zig new file mode 100644 index 0000000..d98ae3a --- /dev/null +++ b/src/action.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +pub const Action = enum { + Shutdown, + Reboot, + Suspend, + Hibernate, + + pub fn execute( + self: Action, + handle_out: *?std.ChildProcess, + alloc: std.mem.Allocator, + ) !void { + var argv: [2][]const u8 = undefined; + argv[0] = "systemctl"; + + argv[1] = switch (self) { + .Shutdown => "poweroff", + .Reboot => "reboot", + .Suspend => "suspend", + .Hibernate => "hibernate", + }; + + var child = std.ChildProcess.init(&argv, alloc); + try child.spawn(); + handle_out.* = child; + } +}; diff --git a/src/ffi.zig b/src/ffi.zig new file mode 100644 index 0000000..a56680e --- /dev/null +++ b/src/ffi.zig @@ -0,0 +1,16 @@ +// partially yoinked from https://github.com/Swoogan/ziggtk +pub const c = @cImport({ + @cInclude("gtk/gtk.h"); +}); + +/// Could not get `g_signal_connect` to work. Zig says "use of undeclared identifier". Reimplemented here +pub fn connectSignal( + instance: c.gpointer, + detailed_signal: [*c]const c.gchar, + c_handler: c.GCallback, + data: c.gpointer, +) void { + var zero: u32 = 0; + const flags: *c.GConnectFlags = @ptrCast(*c.GConnectFlags, &zero); + _ = c.g_signal_connect_data(instance, detailed_signal, c_handler, data, null, flags.*); +} diff --git a/src/gui.zig b/src/gui.zig new file mode 100644 index 0000000..28f2419 --- /dev/null +++ b/src/gui.zig @@ -0,0 +1,129 @@ +const std = @import("std"); + +const Action = @import("action.zig").Action; +const c = ffi.c; +const ffi = @import("ffi.zig"); +const u = @import("util.zig"); + +pub const GuiState = struct { + child: ?std.ChildProcess = null, + alloc: std.mem.Allocator, + /// Allocator used to allocate userdata that will be cleared at the + /// end of the application lifespan + user_data_arena: std.mem.Allocator, +}; + +pub fn activate(app: *c.GtkApplication, state: *GuiState) callconv(.C) void { + const win = c.gtk_application_window_new(app); + c.gtk_window_set_title(u.c(*c.GtkWindow, win), "gpower2"); + c.gtk_window_set_modal(u.c(*c.GtkWindow, win), 1); + c.gtk_window_set_resizable(u.c(*c.GtkWindow, win), 0); + c.gtk_window_set_icon_name(u.c(*c.GtkWindow, win), "system-shutdown"); + + const eck = c.gtk_event_controller_key_new(); + c.gtk_widget_add_controller(win, eck); + ffi.connectSignal( + eck, + "key-pressed", + u.c(c.GCallback, handleKeypress), + u.c(*c.GtkWindow, win), + ); + + const content = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 20); + c.gtk_window_set_child(u.c(*c.GtkWindow, win), content); + inline for (.{ .top, .bottom, .start, .end }) |fun| { + @field(c, "gtk_widget_set_margin_" ++ @tagName(fun))(content, 20); + } + + inline for (.{ + Action.Shutdown, + Action.Reboot, + Action.Suspend, + Action.Hibernate, + }) |action| { + c.gtk_box_append( + u.c(*c.GtkBox, content), + powerButton(state, u.c(*c.GtkWindow, win), action), + ); + } + + c.gtk_widget_show(win); +} + +const ButtonHandlerData = struct { + state: *GuiState, + action: Action, + win: *c.GtkWindow, +}; + +fn powerButton( + state: *GuiState, + win: *c.GtkWindow, + action: Action, +) *c.GtkWidget { + const text = @tagName(action); + const icon = switch (action) { + Action.Shutdown => "system-shutdown", + Action.Reboot => "system-reboot", + Action.Suspend => "system-suspend", + Action.Hibernate => "system-hibernate", + }; + + var udata = state.user_data_arena.create(ButtonHandlerData) catch @panic("Failed to allocate button handler data!!"); + udata.* = ButtonHandlerData{ + .state = state, + .win = win, + .action = action, + }; + + const container = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 2); + + const button = c.gtk_button_new(); + c.gtk_box_append(u.c(*c.GtkBox, container), button); + + ffi.connectSignal(button, "clicked", u.c(c.GCallback, handleClick), udata); + + const image = c.gtk_image_new_from_icon_name(icon); + c.gtk_button_set_child(u.c(*c.GtkButton, button), image); + + c.gtk_image_set_pixel_size(u.c(*c.GtkImage, image), 60); + + const label = c.gtk_label_new(text); + c.gtk_box_append(u.c(*c.GtkBox, container), label); + + return container; +} + +fn handleClick( + btn: *c.GtkButton, + udata: *ButtonHandlerData, +) void { + _ = btn; + _ = udata; + + udata.action.execute(&udata.state.child, udata.state.alloc) catch |e| { + // TODO: error dialog + std.log.err("Error spawning child: {}", .{e}); + }; + + c.gtk_window_close(udata.win); +} + +fn handleKeypress( + eck: *c.GtkEventControllerKey, + keyval: c.guint, + keycode: c.guint, + state: c.GdkModifierType, + win: *c.GtkWindow, +) c.gboolean { + _ = eck; + _ = keycode; + _ = state; + + if (keyval == c.GDK_KEY_Escape) { + c.gtk_window_close(win); + return 1; + } else { + return 0; + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..4bf8626 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,32 @@ +const std = @import("std"); +const ffi = @import("ffi.zig"); +const u = @import("util.zig"); +const c = ffi.c; +const gui = @import("gui.zig"); + +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); + defer arena.deinit(); + + var state = gui.GuiState{ + .alloc = std.heap.c_allocator, + .user_data_arena = arena.allocator(), + }; + + const app = c.gtk_application_new("de.mzte.gpower2", c.G_APPLICATION_FLAGS_NONE); + defer c.g_object_unref(app); + + ffi.connectSignal(app, "activate", @ptrCast(c.GCallback, gui.activate), &state); + + const status = c.g_application_run( + u.c(*c.GApplication, app), + @intCast(i32, std.os.argv.len), + u.c([*c][*c]u8, std.os.argv.ptr), + ); + + if (state.child) |*ch| { + _ = try ch.wait(); + } + + std.os.exit(@intCast(u8, status)); +} diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..fb244d1 --- /dev/null +++ b/src/util.zig @@ -0,0 +1,4 @@ +/// shortcut for ptrCast +pub fn c(comptime T: type, x: anytype) T { + return @ptrCast(T, x); +}