diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c82b07 --- /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..7dbbef2 --- /dev/null +++ b/build.zig @@ -0,0 +1,33 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const exe = b.addExecutable(.{ + .name = "hyprman", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + b.installArtifact(exe); + const run_cmd = b.addRunArtifact(exe); + 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_unit_tests = b.addTest(.{ + // .root_source_file = b.path("src/main.zig"), + // .target = target, + // .optimize = optimize, + // }); + // + // const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + // const test_step = b.step("test", "Run unit tests"); + // test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..8a9c12b --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,67 @@ +.{ + .name = "hyprman", + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e08c434 --- /dev/null +++ b/flake.lock @@ -0,0 +1,95 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1714314149, + "narHash": "sha256-yNAevSKF4krRWacmLUsLK7D7PlfuY3zF0lYnGYNi9vQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "cf8cc1201be8bc71b7cbbbdaf349b22f4f99c7ae", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1706063522, + "narHash": "sha256-o1m9en7ovSjyktXgX3n/6GJEwG06WYa/9Mfx5hTTf5g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "95c1439b205d507f3cb88aae76e02cd6a01ac504", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "zig2nix": "zig2nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1714441187, + "narHash": "sha256-ZxbKh27jHGbzF0JM6l80jSzpTMhYrzkg75g9RnVPPiE=", + "owner": "Cloudef", + "repo": "zig2nix", + "rev": "d2e6a19c4c97df944142ad9d6f8b661f8bb6a716", + "type": "github" + }, + "original": { + "owner": "Cloudef", + "repo": "zig2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..81b48a2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,42 @@ +{ + description = "hyprman"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + zig2nix.url = "github:Cloudef/zig2nix"; + }; + + outputs = + inputs@{ + self, + nixpkgs, + zig2nix, + ... + }: + let + inherit (nixpkgs.lib) genAttrs; + supportedSystems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + "aarch64-darwin" + ]; + forAllSystems = f: genAttrs supportedSystems (system: f nixpkgs.legacyPackages.${system}); + zig-env = + system: + zig2nix.outputs.zig-env.${system} { zig = zig2nix.outputs.packages.${system}.zig.default.bin; }; + in + { + devShells = forAllSystems (pkgs: { + default = (zig-env pkgs.system).mkShell { }; + }); + packages = forAllSystems (pkgs: { + hyprman = (zig-env pkgs.system).package { + name = "hyprman"; + version = "2023.1001"; + src = nixpkgs.lib.cleanSource ./.; + }; + default = self.packages.${pkgs.system}.hyprman; + }); + formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style); + }; +} diff --git a/src/hyprland.zig b/src/hyprland.zig new file mode 100644 index 0000000..2ae3c4b --- /dev/null +++ b/src/hyprland.zig @@ -0,0 +1,99 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub const Workspace = struct { + id: u8, + name: []const u8, +}; + +pub const Client = struct { + workspace: Workspace, + class: []const u8, + title: []const u8, +}; + +pub const Monitor = struct { id: u8, activeWorkspace: Workspace }; + +pub fn getSocketPath(allocator: Allocator, path: []const u8) ![]const u8 { + const runtime_dir = std.posix.getenv("XDG_RUNTIME_DIR").?; + const hyprland_instance = std.posix.getenv("HYPRLAND_INSTANCE_SIGNATURE").?; + return try std.fmt.allocPrint( + allocator, + "{s}/hypr/{s}/{s}", + .{ runtime_dir, hyprland_instance, path }, + ); +} + +pub fn getDataFromSocket(allocator: Allocator, comptime msg: []const u8, comptime T: type) ![]T { + const sock = try std.net.connectUnixSocket( + try getSocketPath(allocator, ".socket.sock"), + ); + defer sock.close(); + + _ = try sock.write(msg); + const out = try sock.reader().readAllAlloc(allocator, std.math.maxInt(u32)); + const value = try std.json.parseFromSliceLeaky( + []T, + allocator, + out, + .{ .ignore_unknown_fields = true }, + ); + + return value; +} + +const Event = enum { + monitoradded, + monitorremoved, +}; + +pub fn runEwwCommand(event: Event, allocator: Allocator) !void { + std.time.sleep(1 * std.time.ns_per_s); + std.log.debug("reacting to monitor change", .{}); + var p = std.ChildProcess.init( + &.{ + "eww", + switch (event) { + .monitoradded => "open", + .monitorremoved => "close", + }, + "bar1", + }, + allocator, + ); + _ = try p.spawnAndWait(); +} + +pub fn startEww(allocator: Allocator) !void { + const monitors = try getDataFromSocket(allocator, "[-j]/monitors", Monitor); + std.log.debug("starting eww", .{}); + for (0..monitors.len) |i| { + const bar = try std.fmt.allocPrint(allocator, "bar{d}", .{i}); + defer allocator.free(bar); + + var p = std.ChildProcess.init( + &.{ "eww", "open", bar }, + allocator, + ); + _ = try p.spawnAndWait(); + } +} +pub fn watchMonitors(allocator: Allocator) !void { + try startEww(allocator); + const sock = try std.net.connectUnixSocket( + try getSocketPath(allocator, ".socket2.sock"), + ); + defer sock.close(); + while (true) { + const out = try sock.reader().readUntilDelimiterAlloc( + allocator, + '\n', + std.math.maxInt(u32), + ); + defer allocator.free(out); + var iterator = std.mem.split(u8, out, ">>"); + if (std.meta.stringToEnum(Event, iterator.next().?)) |event| { + try runEwwCommand(event, allocator); + } + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..c365df2 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,164 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const hypr = @import("hyprland.zig"); +const Client = hypr.Client; +const Monitor = hypr.Monitor; + +const Icons = struct { + allocator: Allocator, + map: std.ArrayHashMapUnmanaged([]const u8, []const u8, std.array_hash_map.StringContext, true), + + pub fn init(allocator: Allocator) !Icons { + var config_dir: ?[]const u8 = std.posix.getenv("XDG_CONFIG_DIR"); + if (config_dir == null) { + config_dir = try std.fmt.allocPrint( + allocator, + "{s}/.config", + .{std.posix.getenv("HOME").?}, + ); + } + defer { + if (config_dir) |cd| allocator.free(cd); + } + + const icon_path = try std.fmt.allocPrint(allocator, "{s}/hyprman/icons.json", .{config_dir.?}); + defer allocator.free(icon_path); + + const file = try std.fs.openFileAbsolute(icon_path, .{}); + defer file.close(); + + const buffer = try file.readToEndAlloc(allocator, (try file.stat()).size); + defer allocator.free(buffer); + + const icons = (try std.json.ArrayHashMap([]const u8).jsonParseFromValue( + allocator, + try std.json.parseFromSliceLeaky(std.json.Value, allocator, buffer, .{}), + .{}, + )).map; + + return Icons{ + .allocator = allocator, + .map = icons, + }; + } + + pub fn get(self: *const Icons, k: []const u8) []const u8 { + return self.map.get(k) orelse ""; + } +}; + +const WorkspaceIcon = struct { + id: u8, + icon: []const u8, + class: []const u8, +}; + +pub fn allocWorkspaces( + allocator: Allocator, + len_monitors: usize, + len_workspaces: usize, +) ![][]WorkspaceIcon { + var list = std.ArrayList([]WorkspaceIcon).init(allocator); + errdefer { + for (list.items) |slice| allocator.free(slice); + list.deinit(); + } + for (0..len_monitors) |_| { + var workspaces = std.ArrayList(WorkspaceIcon).init(allocator); + errdefer workspaces.deinit(); + for (0..len_workspaces) |i| { + var id: u8 = @intCast(i); + id += 1; + try workspaces.append(WorkspaceIcon{ + .id = id, + .class = try std.fmt.allocPrint(allocator, "ws-button-{d}", .{id}), + .icon = "", + }); + } + try list.append(try workspaces.toOwnedSlice()); + } + return list.toOwnedSlice(); +} + +pub fn listenWorkspaces(child_allocator: Allocator, iconLookup: Icons) !void { + var arena = std.heap.ArenaAllocator.init(child_allocator); + while (true) { + defer _ = arena.reset(.free_all); + const allocator = arena.allocator(); + + const clients = try hypr.getDataFromSocket(allocator, "[-j]/clients", Client); + const monitors = try hypr.getDataFromSocket(allocator, "[-j]/monitors", Monitor); + const workspaces = try allocWorkspaces(allocator, monitors.len, 9); + + for (clients) |c| { + for (0..monitors.len) |i| { + var ws = &workspaces[i][c.workspace.id - 1]; + ws.icon = try std.fmt.allocPrint(allocator, "{s}{s}", .{ + ws.icon, + iconLookup.get(c.class), + }); + } + } + + for (workspaces, 0..) |ws_list, i| { + for (ws_list) |*ws| { + if (monitors[i].activeWorkspace.id == ws.id) + ws.*.class = + try std.fmt.allocPrint( + allocator, + "{s} {s}", + .{ ws.class, "ws-button-open" }, + ); + + if (std.mem.eql(u8, ws.icon, "")) + ws.*.icon = ""; + } + } + const stderr = std.io.getStdOut().writer(); + try std.json.stringify(workspaces, .{}, stderr); + try stderr.writeAll("\n"); + + std.time.sleep(500 * std.time.ns_per_ms); + } +} + +const Cmd = enum { + workspaces, + monitors, +}; + +const Context = struct { + allocator: Allocator, + cmd: Cmd, + pub fn init(allocator: Allocator) !Context { + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + if ((args.len == 1) or (args.len > 3)) { + std.debug.print("please provide one of: workspaces, monitors\n", .{}); + std.process.exit(1); + } + const cmd = std.meta.stringToEnum(Cmd, args[1]); + if (cmd == null) { + std.debug.print("unknown cmd: {s}\n", .{args[1]}); + std.process.exit(1); + } + return .{ .allocator = allocator, .cmd = cmd.? }; + } +}; + +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + const ctx = try Context.init(allocator); + switch (ctx.cmd) { + .workspaces => { + const iconLookup = try Icons.init(allocator); + try listenWorkspaces(allocator, iconLookup); + }, + .monitors => { + try hypr.watchMonitors(allocator); + }, + } +}