Compare commits

...

No commits in common. "64a9f0ee2be44d821f6781da44c6e8e1248b6c48" and "main" have entirely different histories.

18 changed files with 558 additions and 443 deletions

36
.gitignore vendored
View file

@ -1,2 +1,34 @@
zig-cache
zig-out
# Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# End of https://www.toptal.com/developers/gitignore/api/go
# nix
result
hyprman
nimbledeps
nimble.develop
nimble.paths

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Daylin Morgan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
README.md Normal file
View file

@ -0,0 +1 @@
# hyprman

View file

@ -1,33 +0,0 @@
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);
}

View file

@ -1,67 +0,0 @@
.{
.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 <url>` 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",
},
}

13
config.nims Normal file
View file

@ -0,0 +1,13 @@
# useful while hwylterm is tracking HEAD
task update, "update deps":
rmFile "nimble.lock"
rmDir "nimbledeps"
exec "nimble lock -l"
exec "nimble setup -l"
# begin Nimble config (version 2)
--noNimblePath
when withDir(thisDir(), system.fileExists("nimble.paths")):
include "nimble.paths"
# end Nimble config

View file

@ -1,30 +1,32 @@
{
"nodes": {
"flake-utils": {
"nim2nix": {
"inputs": {
"systems": "systems"
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"lastModified": 1731683943,
"narHash": "sha256-S7J/6qnwvj09XSma37oynLONYP+rJAul/sX9hpK7cN4=",
"owner": "daylinmorgan",
"repo": "nim2nix",
"rev": "a9f1182dbe51675b9666d6a79bebded7d9839ad7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "daylinmorgan",
"repo": "nim2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714314149,
"narHash": "sha256-yNAevSKF4krRWacmLUsLK7D7PlfuY3zF0lYnGYNi9vQ=",
"lastModified": 1731531548,
"narHash": "sha256-sz8/v17enkYmfpgeeuyzniGJU0QQBfmAjlemAUYhfy8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "cf8cc1201be8bc71b7cbbbdaf349b22f4f99c7ae",
"rev": "24f0d4acd634792badd6470134c387a3b039dace",
"type": "github"
},
"original": {
@ -34,59 +36,10 @@
"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"
"nim2nix": "nim2nix",
"nixpkgs": "nixpkgs"
}
}
},

View file

@ -2,40 +2,54 @@
description = "hyprman";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
zig2nix.url = "github:Cloudef/zig2nix";
nim2nix.url = "github:daylinmorgan/nim2nix";
nim2nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
inputs@{
self,
{
nim2nix,
nixpkgs,
zig2nix,
self,
...
}:
let
inherit (nixpkgs.lib) genAttrs;
inherit (nixpkgs.lib) genAttrs cleanSource;
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; };
forAllSystems =
f:
genAttrs supportedSystems (
system:
f (
import nixpkgs {
inherit system;
overlays = [ nim2nix.overlays.default ];
}
)
);
in
{
devShells = forAllSystems (pkgs: {
default = (zig-env pkgs.system).mkShell { };
default = pkgs.mkShell {
packages = with pkgs; [
nim
nimble
];
};
});
packages = forAllSystems (pkgs: {
hyprman = (zig-env pkgs.system).package {
name = "hyprman";
version = "2023.1001";
src = nixpkgs.lib.cleanSource ./.;
};
default = self.packages.${pkgs.system}.hyprman;
hyprman = pkgs.buildNimblePackage {
pname = "hyprman";
version = "unstable";
src = cleanSource ./.;
nimbleDepsHash = "sha256-tDhf7CrHtgtu9NNFzp3swnfeMYBFrvHsFwR0JeYul7Q=";
};
});
formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style);
};

16
hyprman.nimble Normal file
View file

@ -0,0 +1,16 @@
# Package
version = "0.1.0"
author = "Daylin Morgan"
description = "hyrpman "
license = "MIT"
srcDir = "src"
bin = @["hyprman"]
# Dependencies
requires "nim >= 2.0.8"
requires "yaml"
requires "jsony"
requires "https://github.com/daylinmorgan/hwylterm#f1cc95f8"

36
nimble.lock Normal file
View file

@ -0,0 +1,36 @@
{
"version": 2,
"packages": {
"hwylterm": {
"version": "0.1.0",
"vcsRevision": "f1cc95f86edcc00665fc8280f57edc0e83d461f9",
"url": "https://github.com/daylinmorgan/hwylterm",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "433522bac3b8f3caae252a1a42867ed8dc91f4d2"
}
},
"jsony": {
"version": "1.1.5",
"vcsRevision": "ea811bec7fa50f5abd3088ba94cda74285e93f18",
"url": "https://github.com/treeform/jsony",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "6aeb83e7481ca8686396a568096054bc668294df"
}
},
"yaml": {
"version": "2.1.1",
"vcsRevision": "48a90e36e82bd97457dae87e86efe423dcd3bb40",
"url": "https://github.com/flyx/NimYAML",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "302727fcd74c79d0697a4e909d26455d61a5b979"
}
}
},
"tasks": {}
}

184
src/hyprland.nim Normal file
View file

@ -0,0 +1,184 @@
import std/[
os, osproc, strformat, strutils,
streams, tables, net, sugar, times
]
import ./[lib, swww]
import yaml, jsony
type
# Remove effects?
HyprlandDefect* = Defect
Workspace = object
name*: string
id*: int
Client = object
class*: string
workspace*: Workspace
ActiveWorkspace = object
name*: string
id*: int
Monitor = object
activeWorkspace*: ActiveWorkspace
name: string
id*: int
transform: int
proc isRotated(m: Monitor): bool =
# 1 -> 90 degrees
m.transform == 1
proc hyprSocketPath(): string =
let runtimeDir = getEnv("XDG_RUNTIME_DIR")
let instancSig = getEnv("HYPRLAND_INSTANCE_SIGNATURE")
result = runtimeDir / "hypr" / instancSig
let
hyprSocket = hyprSocketPath() / ".socket.sock"
hyprSocket2 = hyprSocketPath() / ".socket2.sock"
# TODO: revamp?
proc getData[T](msg: string, to: typedesc[T]): T =
let socket = newSocket(AF_UNIX, SOCK_STREAM, IPPROTO_IP)
try:
socket.connectUnix(hyprSocket)
except OSError:
raise newException(
HyprlandDefect, "Could not connect to Hyprland IPC UNIX path; is Hyprland running?"
)
socket.send msg
var recvData: string
while true:
let response = socket.recv(4096)
if response == "": break
recvData.add response
socket.close() # is this necessary?
result = recvData.fromJson(T)
proc getMonitors*(): seq[Monitor] =
result = getData("[-j]/monitors", seq[Monitor])
proc getClients(): seq[Client] =
result = getData("[-j]/clients", seq[Client])
type
EwwWorkspace = object
icon*, class*: string
id*: int
proc add(ws: var EwwWorkspace, client: Client) =
let clientIcon = config.pickIcon(client.class)
if ws.icon == config.`no-client`:
ws.icon = clientIcon
else:
ws.icon.add clientIcon
func openWorkspaces(monitors: seq[Monitor]): seq[int] =
for m in monitors:
result.add m.activeWorkspace.id
func setActive(workspaces: seq[EwwWorkspace], m: Monitor): seq[EwwWorkspace] =
let id = m.activeWorkspace.id
result = workspaces
result[id - 1].class.add " ws-button-active-" & $(id)
proc writeEwwClasses*() =
let
monitors = getMonitors()
clients = getClients()
var workspaces=
collect:
for i in 0..<9:
EwwWorkspace(
icon: config.`no-client`, class: fmt"ws-button-{i+1}", id: i+1
)
for client in clients:
let id = client.workspace.id - 1
workspaces[id].add client
for id in openWorkspaces(monitors):
workspaces[id - 1].class.add " ws-button-open"
var ewwClasses =
collect:
for m in monitors:
workspaces.setActive m
stdout.write (ewwClasses.toJson() & "\n")
flushFile stdout
const redrawEvents = [
"workspacev2", # emitted on workspace change. Is emitted ONLY when a user requests a workspace change, and is not emitted on mouse movements (see activemon) WORKSPACEID,WORKSPACENAME
"focusedmon", # emitted on the active monitor being changed. MONNAME,WORKSPACENAME
"activewindowv2", # emitted on the active window being changed. WINDOWADDRESS
"fullscreen", # emitted when a fullscreen status of a window changes. 0/1 (exit fullscreen / enter fullscreen)
"monitorremoved", # emitted when a monitor is removed (disconnected) MONITORNAME
"monitoraddedv2", # emitted when a monitor is added (connected) MONITORID,MONITORNAME,MONITORDESCRIPTION
"createworkspacev2", # emitted when a workspace is created WORKSPACEID,WORKSPACENAME
"destroyworkspacev", # emitted when a workspace is destroyed WORKSPACEID,WORKSPACENAME
"moveworkspacev2", # emitted when a workspace is moved to a different monitor WORKSPACEID,WORKSPACENAME,MONNAME
"openwindow", # emitted when a window is opened WINDOWADDRESS,WORKSPACENAME,WINDOWCLASS,WINDOWTITLE
"closewindow", # emitted when a window is closed WINDOWADDRESS
"movewindowv2", # emitted when a window is moved to a workspace WINDOWADDRESS,WORKSPACEID,WORKSPACENAME
]
const monitorChangeEvents = [
"monitorremoved", # emitted when a monitor is removed (disconnected) MONITORNAME
"monitoraddedv2", # emitted when a monitor is added (connected) MONITORID,MONITORNAME,MONITORDESCRIPTION
]
proc parseEvent(line: string): (string, string) =
let s = line.split(">>", 1)
result = (s[0], s[1])
proc streamEwwClasses*() =
let socket = newSocket(AF_UNIX, SOCK_STREAM, IPPROTO_IP)
try:
socket.connectUnix(hyprSocket2)
except OSError:
raise newException(
HyprlandDefect, "Could not connect to Hyprland IPC UNIX path; is Hyprland running?"
)
writeEwwClasses()
while true:
var line: string
socket.readLine line
let (e, _) = parseEvent(line)
if e in redrawEvents:
writeEwwClasses()
proc handleHyprEvent(e: string) =
if e in monitorChangeEvents:
notify("detected monitor change")
oneShotSwww()
for i in 0..<getMonitors().len:
let (output, code) = execCmdEx("eww open bar" & $i)
if code != 0:
notify("eww failed:\n" & output)
proc maybeTriggerSwww(last: var DateTime) =
let current = now()
if (current - last).inSeconds > config.timeout:
oneShotSwww()
last = current
proc watchHyprland*() =
let socket = newSocket(AF_UNIX, SOCK_STREAM, IPPROTO_IP)
try:
socket.connectUnix(hyprSocket2)
except OSError:
raise newException(
HyprlandDefect, "Could not connect to Hyprland IPC UNIX path; is Hyprland running?"
)
oneShotSwww()
var lastSwww= now()
while true:
var line: string
socket.readLine line
let (e, _) = parseEvent(line)
handleHyprEvent e
maybeTriggerSwww lastSwww

View file

@ -1,99 +0,0 @@
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);
}
}
}

59
src/hyprman.nim Normal file
View file

@ -0,0 +1,59 @@
## hyprman, the hyprland companion
import std/[os, osproc, strformat]
import hwylterm/hwylcli
import ./[
hyprland,
lib,
mako,
]
hwylCli:
name "hyprman"
flags:
[global]
config:
i configPath
T string
? "path to config file"
* (getConfigDir() / "hyprman" / "config.yml")
- c
preSub:
config.load(configPath)
subcommands:
[mako]
... "interact with mako"
flags:
count:
T int
? "# of notifications"
* 10
json:
? "output as json"
- j
reverse:
? "swap notification order"
- r
run:
let history = getHistory(reverse, count)
if json: echo $(%* history)
else:
for n in history.notifications:
echo $bb(n)
[start]
... "launch eww"
run:
notify("starting eww")
for i in 0..<getMonitors().len:
let code = execCmd fmt"eww open bar{i}"
if code != 0:
notify(fmt"failed to open eww bar{i}")
[watch]
... "handle monitor changes on hyprland"
run: watchHyprland()
[eww]
... "watch hyprland events for eww class changes"
run: streamEwwClasses()

38
src/lib.nim Normal file
View file

@ -0,0 +1,38 @@
import std/[
os, osproc, streams, strutils, tables
]
export tables
import yaml
proc notify*(message: string) =
var cmd = "notify-send --app-name=hyprman --transient hyprman --expire-time 10000"
cmd.add "\""
cmd.add message
cmd.add "\""
discard execCmd(cmd)
type
Icons = Table[string, string]
Config = object # config vs icons?
timeout* {.defaultVal: 60.}: int
wallpapers* {.defaultVal: "".}: string
classes*: Icons
`no-client`*: string
`default-icon`*: string
var config*: Config
proc load*(c: var Config, p: string) =
if fileExists(p):
var s = newFileStream(p)
load(s, c)
c.wallpapers = expandTilde(c.wallpapers)
func pickIcon*(
c: Config,
class: string
): string =
for k, v in c.classes:
if class in k: result = v
if result == "":
result = c.`default-icon`

View file

@ -1,164 +0,0 @@
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);
},
}
}

53
src/mako.nim Normal file
View file

@ -0,0 +1,53 @@
import std/[algorithm, strutils, json, osproc, wordwrap, terminal]
import hwylterm
export hwylterm, json
# TODO: use jsony in this module
type
MakoNotificationData = object
`type`, data: string
MakoNotification = object
`app-name`, summary, body: MakoNotificationData
MakoHistory = object
`type`: string
data: seq[seq[MakoNotification]]
Notification = object
appName, summary, body: string
History = object
notifications*: seq[Notification]
func toNotification(mn: MakoNotification): Notification =
result.appName = mn.`app-name`.data
result.summary = mn.summary.data
result.body = mn.body.data
func toHistory*(mh: MakoHistory): History =
for mn in mh.data[0]:
result.notifications.add mn.toNotification()
func filter(h: History, reverse: bool, count: int): History =
result.notifications = h.notifications
if reverse:
result.notifications.reverse()
let high = min(count, h.notifications.len - 1)
result.notifications =
result.notifications[0..high]
proc getMakoHistory*(): MakoHistory =
let (output, errCode) = execCmdEx("makoctl history")
if errCode != 0: quit output, errCode
result = parseJson(output).to(MakoHistory)
proc getHistory*(reverse: bool, count: int): History =
getMakoHistory().toHistory().filter(reverse, count)
proc bb*(n: Notification): BbString =
template border(style: string): untyped = "[" & style & "]" & "┃ [/]"
var raw: string
raw.add border("magenta") & "[yellow]" & n.appName & "[/]\n"
raw.add border("green") & "[b]" & n.summary & "[/]\n"
for line in n.body.wrapWords(maxLineWidth = terminalWidth()-2).splitLines():
raw.add border("default") & line & "\n"
result = bb(raw)

48
src/swww.nim Normal file
View file

@ -0,0 +1,48 @@
import std/[os, osproc, strformat, strutils, random]
import ./lib
randomize()
const oneMinute = 1000 * 60
proc loadGallery(): seq[string] =
for path in walkDirRec(
config.wallpapers, yieldFilter = {pcFile, pcLinkToFile}
):
let (_,_, ext) = splitFile(path)
if ext in [".png", ".jpeg"]:
result.add path
proc swwwMonitors(): seq[string] =
let (output, code) = execCmdEx("swww query")
if code != 0: notify "swww failed"
for line in output.strip().splitLines():
result.add line.split(':', 1)[0]
proc setImg(path: string, output: string) =
let code =
execCmd(fmt"swww img --transition-type fade --outputs {output} {path}")
if code != 0: notify "swww failed"
proc oneShotSwww*() =
if config.wallpapers == "": return
if not dirExists(config.wallpapers):
notify(fmt"{config.wallpapers} directory does not exist")
quit(1)
let gallery = loadGallery()
for monitor in swwwMonitors():
setImg(gallery.sample(), monitor)
proc persistentSwww*() =
if config.wallpapers == "": quit(0)
if not dirExists(config.wallpapers):
notify(fmt"{config.wallpapers} directory does not exist")
quit(1)
let gallery = loadGallery()
while true:
for monitor in swwwMonitors():
setImg(gallery.sample(), monitor)
sleep 5 * oneMinute

10
todo.md Normal file
View file

@ -0,0 +1,10 @@
# hyprman todo's
- [ ] include in class the 'other' active monitor?
- [ ] switch to usu once parser is stable again
- [x] make swww powered wallpaper cycler
- [ ] add support for monitor orientation?
- [ ] make `hyprman swww` support monitor changes by watching hyprland IPC
<!-- generated with <3 by daylinmorgan/todo -->