Compare commits
4 commits
bc50f37187
...
f86c7f6f9b
Author | SHA1 | Date | |
---|---|---|---|
f86c7f6f9b | |||
1da3e989ea | |||
26a3212dad | |||
babe900107 |
13 changed files with 495 additions and 0 deletions
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# 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
21
LICENSE
Normal 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
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# hyprman
|
16
config.nims
Normal file
16
config.nims
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
# useful while hwylterm is tracking HEAD
|
||||||
|
task update, "update deps":
|
||||||
|
rmFile "nimble.lock"
|
||||||
|
rmDir "nimbledeps"
|
||||||
|
exec "nimble lock -l"
|
||||||
|
exec "nimble setup -l"
|
||||||
|
|
||||||
|
task build, "build":
|
||||||
|
selfExec "c --outdir:bin src/hyprman.nim"
|
||||||
|
|
||||||
|
# begin Nimble config (version 2)
|
||||||
|
--noNimblePath
|
||||||
|
when withDir(thisDir(), system.fileExists("nimble.paths")):
|
||||||
|
include "nimble.paths"
|
||||||
|
# end Nimble config
|
48
flake.lock
Normal file
48
flake.lock
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nim2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1725469705,
|
||||||
|
"narHash": "sha256-4/iSUwB3XKmybdt4R87VhBPiD3z4BL6RrD9LyrrkesM=",
|
||||||
|
"owner": "daylinmorgan",
|
||||||
|
"repo": "nim2nix",
|
||||||
|
"rev": "42af12ca45025c7146bc24e27f5d8b65b003d663",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "daylinmorgan",
|
||||||
|
"repo": "nim2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1727716680,
|
||||||
|
"narHash": "sha256-uMVkVHL4r3QmlZ1JM+UoJwxqa46cgHnIfqGzVlw5ca4=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "b5b22b42c0d10c7d2463e90a546c394711e3a724",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nim2nix": "nim2nix",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
56
flake.nix
Normal file
56
flake.nix
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
description = "hyprman";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||||
|
nim2nix.url = "github:daylinmorgan/nim2nix";
|
||||||
|
nim2nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
nim2nix,
|
||||||
|
nixpkgs,
|
||||||
|
self,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
inherit (nixpkgs.lib) genAttrs cleanSource;
|
||||||
|
supportedSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-linux"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
forAllSystems =
|
||||||
|
f:
|
||||||
|
genAttrs supportedSystems (
|
||||||
|
system:
|
||||||
|
f (
|
||||||
|
import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ nim2nix.overlays.default ];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forAllSystems (pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
nim
|
||||||
|
nimble
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
packages = forAllSystems (pkgs: {
|
||||||
|
default = self.packages.${pkgs.system}.hyprman;
|
||||||
|
hyprman = pkgs.buildNimblePackage {
|
||||||
|
pname = "hyprman";
|
||||||
|
version = "${self.shortRev or "dirty"}";
|
||||||
|
src = cleanSource ./.;
|
||||||
|
nimbleDepsHash = "sha256-72FYXiYIgEDX2j/bBADGvwX6+kd+7py0RHTz2WeyXO8=";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style);
|
||||||
|
};
|
||||||
|
}
|
17
hyprman.nimble
Normal file
17
hyprman.nimble
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Package
|
||||||
|
|
||||||
|
version = "0.1.0"
|
||||||
|
author = "Daylin Morgan"
|
||||||
|
description = "hyrpman "
|
||||||
|
license = "MIT"
|
||||||
|
srcDir = "src"
|
||||||
|
bin = @["hyprman"]
|
||||||
|
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
requires "nim >= 2.0.8"
|
||||||
|
requires "cligen"
|
||||||
|
requires "yaml"
|
||||||
|
requires "jsony"
|
||||||
|
requires "https://github.com/daylinmorgan/hwylterm#HEAD"
|
46
nimble.lock
Normal file
46
nimble.lock
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"packages": {
|
||||||
|
"cligen": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"vcsRevision": "54f1da5d63cf7e116625e2392e85ae54c2b70719",
|
||||||
|
"url": "https://github.com/c-blake/cligen.git",
|
||||||
|
"downloadMethod": "git",
|
||||||
|
"dependencies": [],
|
||||||
|
"checksums": {
|
||||||
|
"sha1": "853785ddace4ee4f3c6c21bdf7f5e9ff0358eb3f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hwylterm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"vcsRevision": "5c71355b5f319a9c174ea88132c7c67a78d00030",
|
||||||
|
"url": "https://github.com/daylinmorgan/hwylterm",
|
||||||
|
"downloadMethod": "git",
|
||||||
|
"dependencies": [],
|
||||||
|
"checksums": {
|
||||||
|
"sha1": "30f8f61787c36b63d484f3d5e149995fad16c63c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
154
src/hyprland.nim
Normal file
154
src/hyprland.nim
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import std/[os, strformat, strutils, streams, tables, net, sugar]
|
||||||
|
import ./lib
|
||||||
|
export tables
|
||||||
|
import yaml, jsony
|
||||||
|
|
||||||
|
type
|
||||||
|
# Remove effects?
|
||||||
|
HyprlandDefect* = Defect
|
||||||
|
Workspace = object
|
||||||
|
name*: string
|
||||||
|
id*: int
|
||||||
|
Client = object
|
||||||
|
class*: string
|
||||||
|
workspace*: Workspace
|
||||||
|
Icons = Table[string, string]
|
||||||
|
Config = object # config vs icons?
|
||||||
|
classes*: Icons
|
||||||
|
`no-client`*: string
|
||||||
|
`default-icon`*: string
|
||||||
|
ActiveWorkspace = object
|
||||||
|
name*: string
|
||||||
|
id*: int
|
||||||
|
Monitor = object
|
||||||
|
activeWorkspace*: ActiveWorkspace
|
||||||
|
id*: int
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
proc loadConfig*(): Config =
|
||||||
|
let configPath = getConfigDir() / "hyprman" / "config.yml"
|
||||||
|
if fileExists(configPath):
|
||||||
|
var s = newFileStream(configPath)
|
||||||
|
load(s, result)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
let config = loadConfig()
|
||||||
|
|
||||||
|
func pickIcon(c: Config, class: string): string =
|
||||||
|
for k, v in c.classes:
|
||||||
|
if class in k:
|
||||||
|
result = v
|
||||||
|
|
||||||
|
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"
|
||||||
|
# flush?
|
||||||
|
|
||||||
|
proc handleHyprEvent(event: string) =
|
||||||
|
let
|
||||||
|
s = event.split(">>", 1)
|
||||||
|
event = s[0]
|
||||||
|
# use enum?
|
||||||
|
case event:
|
||||||
|
of "monitoraddedv2":
|
||||||
|
# TODO: open as many bars as necessary depending on num monitors
|
||||||
|
# is it ok to just call open bar again?
|
||||||
|
notify("monitor added")
|
||||||
|
of "openwindow", "workspacev2":
|
||||||
|
writeEwwClasses()
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
|
||||||
|
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?"
|
||||||
|
)
|
||||||
|
|
||||||
|
while true:
|
||||||
|
var line: string
|
||||||
|
socket.readLine line
|
||||||
|
handleHyprEvent line
|
||||||
|
|
33
src/hyprman.nim
Normal file
33
src/hyprman.nim
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
## hyprman, the hyprland companion
|
||||||
|
|
||||||
|
import std/[osproc, strformat]
|
||||||
|
import hwylterm/cligen, cligen
|
||||||
|
import ./[mako,hyprland, lib]# state]
|
||||||
|
|
||||||
|
hwylCli(clCfg)
|
||||||
|
|
||||||
|
proc start() =
|
||||||
|
## launch eww
|
||||||
|
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}")
|
||||||
|
|
||||||
|
proc watch() =
|
||||||
|
## watch hyprland events for eww class changes
|
||||||
|
watchHyprland()
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
const
|
||||||
|
config = //{"config": "path/to/config"}
|
||||||
|
makoHelp = config // {
|
||||||
|
"count" : "# of notifications",
|
||||||
|
"json" : "output as json",
|
||||||
|
"reverse": "swap notification order"
|
||||||
|
}
|
||||||
|
dispatchMulti(
|
||||||
|
[makoCmd, cmdName = "mako", usage = clCfg.use, help = makoHelp],
|
||||||
|
[start, usage = clCfg.use],
|
||||||
|
[watch, usage = clCfg.use]
|
||||||
|
)
|
5
src/lib.nim
Normal file
5
src/lib.nim
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import std/[osproc, strformat]
|
||||||
|
|
||||||
|
proc notify*(message: string) =
|
||||||
|
discard execCmd fmt"notify-send --app-name=hyprman --transient '{message}'"
|
||||||
|
|
56
src/mako.nim
Normal file
56
src/mako.nim
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import std/[algorithm, strutils, json, osproc, wordwrap, terminal]
|
||||||
|
import hwylterm
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
proc makoCmd*(config = "", count = 10, json = false, reverse = false) =
|
||||||
|
## interact with mako
|
||||||
|
let history = getMakoHistory().toHistory().filter(reverse, count)
|
||||||
|
if json:
|
||||||
|
echo $(%* history)
|
||||||
|
else:
|
||||||
|
for n in history.notifications:
|
||||||
|
echo $bb(n)
|
||||||
|
|
8
todo.md
Normal file
8
todo.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# hyprman todo's
|
||||||
|
|
||||||
|
- [ ] include in class the 'other' active monitor?
|
||||||
|
- [ ] switch to usu once parser is stable again
|
||||||
|
- [ ] make swww powered wallpaper cycler
|
||||||
|
|
||||||
|
<!-- generated with <3 by daylinmorgan/todo -->
|
||||||
|
|
Loading…
Reference in a new issue