Skip to content

Instantly share code, notes, and snippets.

@MindfulMinun
Last active July 2, 2024 12:25
Show Gist options
  • Save MindfulMinun/80ac136be45a5eb0fd2d0793da27ebb9 to your computer and use it in GitHub Desktop.
Save MindfulMinun/80ac136be45a5eb0fd2d0793da27ebb9 to your computer and use it in GitHub Desktop.
A Deno IPC wrapper to interface with Hyprland compositors.
#!/usr/bin/env -S deno run --ext=ts --allow-env=HYPRLAND_INSTANCE_SIGNATURE,XDG_RUNTIME_DIR --allow-read=/${XDG_RUNTIME_DIR}/hypr --allow-write=/${XDG_RUNTIME_DIR}/hypr
// Requires
// - deno: https://deno.land/
// - hyprland: https://hyprland.org/
import { TextLineStream } from "https://deno.land/std@0.224.0/streams/text_line_stream.ts"
export const enum Icons {
none = -1,
warning = 0,
info = 1,
hint = 2,
error = 3,
confused = 4,
ok = 5,
}
type Address = string
export interface Window {
address: Address,
mapped: boolean,
hidden: boolean,
at: [ number, number ],
size: [ number, number ],
workspace: { id: number, name: string },
floating: boolean,
monitor: number,
class: string,
title: string,
initialClass: string,
initialTitle: string,
pid: number,
xwayland: boolean,
pinned: boolean,
fullscreen: boolean,
fullscreenMode: number,
fakeFullscreen: boolean,
grouped: unknown[],
swallowing: string,
focusHistoryID: number
}
export type Device = {
address: Address
name: string
}
export type Mouse = Device & { defaultSpeed: number }
export type Keyboard = Device & {
rules: string
model: string
layout: string
variant: string
options: string
active_keymap: string
main: boolean
}
export interface LayerOutputInterface {
levels: Record<string, {
address: Address
x: number
y: number
w: number
h: number
namespace: string
}[]>
}
export interface Monitor {
id: number
name: string
description: string
make: string
model: string
serial: string
width: number
height: number
refreshRate: number
x: number
y: number
activeWorkspace: { id: number, name: string }
specialWorkspace: { id: number, name: string }
reserved: [number, number, number, number]
scale: number
transform: number
focused: boolean
dpmsStatus: boolean
vrr: boolean
activelyTearing: boolean
disabled: boolean
currentFormat: string
availableModes: string[]
}
export interface Event {
name: string
data: string
}
export class EventDecoderStream extends TransformStream<string, Event> {
constructor() {
super({
transform: (eventstr, controller) => {
const [name, data] = eventstr.split('>>')
controller.enqueue({ name, data })
}
})
}
}
export interface Workspace {
id: number
name: string
monitor: string
monitorID: number
windows: number
hasfullscreen: boolean
lastwindow: Address
lastwindowtitle: string
}
export interface Bind {
locked: boolean,
mouse: boolean,
release: boolean,
repeat: boolean,
non_consuming: boolean,
modmask: number,
submap: string,
key: string,
keycode: number,
catch_all: boolean,
dispatcher: string,
arg: string,
}
type DispatcherDirection = 'u' | 'd' | 'l' | 'r'
/** These args are probably wrong */
export interface DispatchersArgs {
'exec': [string]
'killactive': []
'closewindow': []
'workspace': [string]
'movetoworkspace': [string]
'movetoworkspacesilent': [string]
'togglefloating': [] | ['active']
'setfloating': [] | ['active']
'settiled': [] | ['active']
'fullscreen': [] | [0 | 1 | 2]
'fakefullscreen': []
'dpms': ['on' | 'off' | 'toggle'] | ['on' | 'off' | 'toggle', string]
'pin': [] | [string]
'movefocus': [DispatcherDirection]
'movewindow': [DispatcherDirection | `mon:${string}` | `mon:${string} silent`]
'swap': [DispatcherDirection]
'centerwindow': [] | [1]
'cyclenext': [] | ['prev' | 'tiled' | 'floating' | 'prev tiled' | 'prev floating']
'focuswindow': [string]
'focusmonitor': [string]
'splitratio': [number]
'toggleopaque': []
'movecursortocorner': [0 | 1 | 2 | 3]
'movecursor': [number, number]
'renameworkspace': [string, string]
'exit': []
'forcerendererreload': []
'movecurrentworkspacetomonitor': [string]
'focusworkspaceoncurrentmonitor': [string]
'moveworkspacetomonitor': [string, string]
'swapactiveworkspaces': [string, string]
'togglespecialworkspace': [] | [string]
'focusurgentorlast': [] | [string]
'togglegroup': [] | [string]
'changegroupactive': [] | [string]
'focuscurrentorlast': [] | [string]
'lockgroups': ['lock' | 'unlock' | 'toggle']
'lockactivegroup': ['lock' | 'unlock' | 'toggle']
'moveintogroup': ['u' | 'd' | 'l' | 'r']
'moveoutofgroup': [] | ['active' | string]
'movewindoworgroup': ['u' | 'd' | 'l' | 'r']
'movegroupwindow': ['b' | string]
'denywindowfromgroup': ['on' | 'off' | 'toggle']
'setignoregrouplock': ['on' | 'off' | 'toggle']
'global': [string]
'summap': ['reset' | string]
}
/**
* An IPC wrapper to interface with Hyprland compositors.
*/
export class SugarRush {
#sock1?: Deno.UnixConn
constructor(
private instanceSignature: string
= Deno.env.get('HYPRLAND_INSTANCE_SIGNATURE') || ''
) { }
/** Gets the active window and its properties */
activewindow() {
return this.hyprctlJson<Window>('j/activewindow')
}
/** Gets the active workspace and its properties */
activeworkspace() {
return this.hyprctlJson<Workspace>('j/activeworkspace')
}
/** Lists all registered binds */
binds() {
return this.hyprctlJson<Bind[]>('j/binds')
}
/** Lists all registered windows with their properties */
clients() {
return this.hyprctlJson<Window[]>('j/clients')
}
/** Gets the current cursor position in global layout coordinates */
cursorpos() {
return this.hyprctlJson<{ x: number, y: number }>('j/cursorpos')
}
/** List all decorations and their info */
decorations(query: string) {
return this.hyprctl(`decorations ${query}`)
}
/** Lists all connected keyboards and mice */
devices() {
return this.hyprctlJson<{
mice: Mouse[]
keyboards: Keyboard[]
tables: Device[]
touch: Device[]
switches: Device[]
}>('j/devices')
}
/** Dismisses all or up to `count` notifications */
dismissnotify(count?: number) {
return this.hyprctl(`dismissnotify ${count ?? ''}`)
}
/** Lists all running instances of Hyprland with their info */
instances() {
return this.hyprctlJson<{
instance: string,
time: number,
pid: number,
wl_socket: string
}[]>('j/instances')
}
/** Lists all the surface layers */
layers() {
return this.hyprctlJson<
Record<string, LayerOutputInterface>
>('j/layers')
}
/** lists all layouts available (including plugin'd ones) */
layouts() {
return this.hyprctlJson<string[]>('j/layouts')
}
/** Lists active outputs with their properties, 'monitors all' lists active and inactive outputs */
monitors() {
return this.hyprctlJson<Monitor[]>('j/monitors')
}
/** Sends a notification using the built-in Hyprland notification system */
notify({
icon = Icons.none,
duration = 3000,
color = 0x55aabbff,
message = ''
}: {
icon?: Icons
duration?: number
color?: number
message: string
}) {
return this.hyprctl(`notify ${icon} ${duration} rgba(${color.toString(16).padStart(8, '0')}) ${message}`)
}
version() {
return this.hyprctlJson<{
branch: string
commit: string
dirty: boolean
commit_message: string
commit_date: string
tag: string
commits: number
flags: unknown[]
}>('j/version')
}
workspaces() {
return this.hyprctlJson<Workspace[]>('j/workspaces')
}
dispatch<Dispatcher extends keyof DispatchersArgs>(
dispatcher: Dispatcher,
...commands: DispatchersArgs[Dispatcher]
) {
return this.hyprctl(`dispatch ${dispatcher} ${commands.join(' ')}`)
}
/** Opens the IPC socket */
private async open1() {
if (this.#sock1) return
const runtimedir = Deno.env.get('XDG_RUNTIME_DIR') || '/tmp'
const path = `${runtimedir}/hypr/${this.instanceSignature}/.socket.sock`
this.#sock1 = await Deno.connect({
path,
transport: "unix"
})
}
private close1() {
// this.#sock1?.close()
this.#sock1 = undefined
}
async hyprctl(cmd: string) {
await this.open1()
this.#sock1?.write(new TextEncoder().encode(cmd))
this.close1()
const text = await new Response(this.#sock1?.readable).text()
// console.log(text)
return text
}
async hyprctlJson<T = unknown>(cmd: string) {
await this.open1()
this.#sock1?.write(new TextEncoder().encode(cmd))
const json = await new Response(this.#sock1?.readable).json()
this.close1()
return json as T
}
async *events() {
const runtimedir = Deno.env.get('XDG_RUNTIME_DIR') || '/tmp'
const eventSock = await Deno.connect({
path: `/${runtimedir}/hypr/${this.instanceSignature}/.socket2.sock`,
transport: "unix"
})
yield* eventSock.readable
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream())
.pipeThrough(new EventDecoderStream())
}
}
if (import.meta.main) {
const hyprland = new SugarRush()
const version = await hyprland.version()
console.log(version)
await hyprland.notify({ message: `Hello from SugarRush!` })
const { x, y } = await hyprland.cursorpos()
await hyprland.dispatch('movecursor', x + 100, y + 100)
for await (const event of hyprland.events()) {
console.log(event)
await hyprland.notify({ message: `${event.name}: ${event.data}` })
}
};