Last active
July 2, 2024 12:25
-
-
Save MindfulMinun/80ac136be45a5eb0fd2d0793da27ebb9 to your computer and use it in GitHub Desktop.
A Deno IPC wrapper to interface with Hyprland compositors.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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}` }) | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://doc.deno.land/https://gist.githubusercontent.com/MindfulMinun/80ac136be45a5eb0fd2d0793da27ebb9/raw/sugarrush.ts/~/SugarRush