Compare commits
No commits in common. "main" and "64a9f0ee2be44d821f6781da44c6e8e1248b6c48" have entirely different histories.
main
...
64a9f0ee2b
18 changed files with 443 additions and 558 deletions
36
.gitignore
vendored
36
.gitignore
vendored
|
@ -1,34 +1,2 @@
|
|||
# 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
|
||||
zig-cache
|
||||
zig-out
|
||||
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
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 +0,0 @@
|
|||
# hyprman
|
33
build.zig
Normal file
33
build.zig
Normal file
|
@ -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);
|
||||
}
|
67
build.zig.zon
Normal file
67
build.zig.zon
Normal file
|
@ -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 <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
13
config.nims
|
@ -1,13 +0,0 @@
|
|||
|
||||
# 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
|
79
flake.lock
79
flake.lock
|
@ -1,32 +1,30 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nim2nix": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731683943,
|
||||
"narHash": "sha256-S7J/6qnwvj09XSma37oynLONYP+rJAul/sX9hpK7cN4=",
|
||||
"owner": "daylinmorgan",
|
||||
"repo": "nim2nix",
|
||||
"rev": "a9f1182dbe51675b9666d6a79bebded7d9839ad7",
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "daylinmorgan",
|
||||
"repo": "nim2nix",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1731531548,
|
||||
"narHash": "sha256-sz8/v17enkYmfpgeeuyzniGJU0QQBfmAjlemAUYhfy8=",
|
||||
"lastModified": 1714314149,
|
||||
"narHash": "sha256-yNAevSKF4krRWacmLUsLK7D7PlfuY3zF0lYnGYNi9vQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "24f0d4acd634792badd6470134c387a3b039dace",
|
||||
"rev": "cf8cc1201be8bc71b7cbbbdaf349b22f4f99c7ae",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -36,10 +34,59 @@
|
|||
"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": {
|
||||
"nim2nix": "nim2nix",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
42
flake.nix
42
flake.nix
|
@ -2,54 +2,40 @@
|
|||
description = "hyprman";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
nim2nix.url = "github:daylinmorgan/nim2nix";
|
||||
nim2nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
zig2nix.url = "github:Cloudef/zig2nix";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nim2nix,
|
||||
nixpkgs,
|
||||
inputs@{
|
||||
self,
|
||||
nixpkgs,
|
||||
zig2nix,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (nixpkgs.lib) genAttrs cleanSource;
|
||||
inherit (nixpkgs.lib) genAttrs;
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
forAllSystems =
|
||||
f:
|
||||
genAttrs supportedSystems (
|
||||
forAllSystems = f: genAttrs supportedSystems (system: f nixpkgs.legacyPackages.${system});
|
||||
zig-env =
|
||||
system:
|
||||
f (
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ nim2nix.overlays.default ];
|
||||
}
|
||||
)
|
||||
);
|
||||
zig2nix.outputs.zig-env.${system} { zig = zig2nix.outputs.packages.${system}.zig.default.bin; };
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
nim
|
||||
nimble
|
||||
];
|
||||
};
|
||||
default = (zig-env pkgs.system).mkShell { };
|
||||
});
|
||||
packages = forAllSystems (pkgs: {
|
||||
default = self.packages.${pkgs.system}.hyprman;
|
||||
hyprman = pkgs.buildNimblePackage {
|
||||
pname = "hyprman";
|
||||
version = "unstable";
|
||||
src = cleanSource ./.;
|
||||
nimbleDepsHash = "sha256-tDhf7CrHtgtu9NNFzp3swnfeMYBFrvHsFwR0JeYul7Q=";
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
# 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
36
nimble.lock
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"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
184
src/hyprland.nim
|
@ -1,184 +0,0 @@
|
|||
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
|
99
src/hyprland.zig
Normal file
99
src/hyprland.zig
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
## 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
38
src/lib.nim
|
@ -1,38 +0,0 @@
|
|||
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`
|
164
src/main.zig
Normal file
164
src/main.zig
Normal file
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
53
src/mako.nim
53
src/mako.nim
|
@ -1,53 +0,0 @@
|
|||
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
48
src/swww.nim
|
@ -1,48 +0,0 @@
|
|||
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
10
todo.md
|
@ -1,10 +0,0 @@
|
|||
# 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 -->
|
||||
|
Loading…
Reference in a new issue