hyprman/src/hyprland.nim

191 lines
5.6 KiB
Nim
Raw Normal View History

import std/[
os, osproc, strformat, strutils,
2025-01-14 16:27:52 -06:00
streams, tables, net, sugar, times, unicode
]
2024-10-21 00:12:17 -05:00
import ./[lib, swww]
2024-10-20 07:46:02 -05:00
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
2024-10-20 07:46:02 -05:00
id*: int
2024-10-25 09:43:03 -05:00
transform: int
2024-10-20 07:46:02 -05:00
2024-10-25 09:43:03 -05:00
proc isRotated(m: Monitor): bool =
# 1 -> 90 degrees
m.transform == 1
2024-10-20 07:46:02 -05:00
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"
2025-01-14 16:27:52 -06:00
# truncate icons at 3 windows
2025-01-14 16:19:38 -06:00
for ws in workspaces.mitems:
2025-01-14 16:27:52 -06:00
if ws.icon.runeLen > 3:
ws.icon =
($(ws.icon.toRunes()[0..2]) & "+")
2025-01-14 16:19:38 -06:00
2024-10-20 07:46:02 -05:00
var ewwClasses =
collect:
for m in monitors:
workspaces.setActive m
stdout.write (ewwClasses.toJson() & "\n")
flushFile stdout
2024-10-20 15:45:05 -05:00
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
]
2024-10-20 07:46:02 -05:00
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()
2024-10-20 07:46:02 -05:00
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()
2024-10-21 19:16:09 -05:00
if (current - last).inSeconds > config.timeout:
oneShotSwww()
last = current
2024-10-20 07:46:02 -05:00
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?"
)
2024-10-25 09:43:03 -05:00
oneShotSwww()
var lastSwww= now()
2024-10-20 07:46:02 -05:00
while true:
var line: string
socket.readLine line
let (e, _) = parseEvent(line)
handleHyprEvent e
maybeTriggerSwww lastSwww