Control
Control media, Discord, displays, windows and workspaces, send notifications, open files and URLs, and change the power state or profile over the WebSocket API.
Control playback, Discord, displays, windows and workspaces, send notifications, open files and URLs, and manage the system’s power state and profile.
Media control
Send the MEDIA_CONTROL event with an action to control media playback. It controls the player the media data describes, unless you set player to another player’s id from its players.
Available actions:
PLAYPAUSESTOPNEXTPREVIOUSVOLUME_UP,VOLUME_DOWN: change the player’s volume by 5%.MUTE: set the player’s volume to 0, or back to where it was when it’s already muted.SEEK: setpositionto the seconds from the start of the track. Checkis_seek_enabledfirst.SET_VOLUME: setvolumeto 0-1.SET_SHUFFLE: setshuffletotrueorfalse.SET_REPEAT: setrepeattoNONE,TRACKorLIST.
On Windows, only the actions up to MUTE work, and not with player. System Bridge presses the media keys there, so the actions reach whichever app responds to them.
{
"id": "abc123",
"token": "abc123",
"event": "MEDIA_CONTROL",
"data": {
"action": "PAUSE"
}
}
{
"id": "abc123",
"token": "abc123",
"event": "MEDIA_CONTROL",
"data": {
"action": "SEEK",
"player": "vlc",
"position": 90
}
}
Discord control
Send the DISCORD_CONTROL event with an action to join or leave voice channels, open a text channel, toggle your camera or screenshare, play a soundboard sound, or change your Discord voice settings. Discord must be set up first.
Available actions:
JOIN_VOICE_CHANNEL: setchannel_idto a voice channel’s ID. If you’re already in a voice channel, this moves you.LEAVE_VOICE_CHANNELOPEN_TEXT_CHANNEL: setchannel_idto a text channel’s ID to show it in Discord. Discord refuses forum channels.MUTE,UNMUTE,TOGGLE_MUTEDEAFEN,UNDEAFEN,TOGGLE_DEAFENSET_INPUT_VOLUME: setvalueto 0-100.SET_INPUT_DEVICE: setdevice_idto a deviceidfrominput.devicesin thediscorddata.SET_OUTPUT_VOLUME: setvalueto 0-200.SET_OUTPUT_DEVICE: setdevice_idto a deviceidfromoutput.devices.SET_VOICE_MODE: setmodetoVOICE_ACTIVITYorPUSH_TO_TALK.SET_VOICE_THRESHOLD: setvalueto the voice activity sensitivity, -100 to 0 dB.SET_PUSH_TO_TALK_DELAY: setvalueto the push to talk release delay, 0-2000 milliseconds.ENABLE_QOS,DISABLE_QOS,TOGGLE_QOSENABLE_SILENCE_WARNING,DISABLE_SILENCE_WARNING,TOGGLE_SILENCE_WARNINGMUTE_USER,UNMUTE_USER,TOGGLE_USER_MUTE: setuser_idto a user’s ID. This mutes them for you only.TOGGLE_USER_MUTEneeds the user to be in your call.SET_USER_VOLUME: setuser_id, and setvalueto 0-200. This changes their volume for you only.TOGGLE_VIDEO: turn your camera on or off.TOGGLE_SCREENSHARE: start or stop sharing your screen. Starting opens Discord’s source picker, so pick a screen or window to share.PLAY_SOUND: setsound_idto a soundidfromsoundsin thediscorddata. You must be in a call. Discord may refuse a sound from a server other than the one you’re in.
The volume values are the percentages that Discord’s sliders show. In testing, Discord ignored changes to automatic sensitivity, noise suppression, echo cancellation and automatic gain control made over RPC, so those settings are only available as data.
{
"id": "abc123",
"token": "abc123",
"event": "DISCORD_CONTROL",
"data": {
"action": "SET_INPUT_VOLUME",
"value": 80
}
}
The response type is DISCORD_CONTROLLED. If Discord is not connected, the response is an ERROR.
Audio control
Send the AUDIO_CONTROL event to set an audio output’s, input’s or app’s volume or mute state, to make an output or input the default, or to move an app to another output. Set any of them together. See audio for what’s supported.
id(required): Anidfrom theaudiodata’soutputs,inputsorapps.volume(optional): Volume in percent, 0-100.muted(optional):trueto mute,falseto unmute.default(optional):trueto make it the default output or input.output(optional): An output’sid, to move an app’s sound to it. Linux only.
{
"id": "abc123",
"token": "abc123",
"event": "AUDIO_CONTROL",
"data": {
"id": "alsa_output.pci-0000_00_1f.3.analog-stereo",
"volume": 40,
"muted": false
}
}
The response type is AUDIO_CONTROLLED, and the audio module updates straight after. An unknown id, or something that doesn’t apply to it, such as making an app the default, gets an ERROR with the subtype BAD_REQUEST. A platform that can’t control audio gets INVALID_ACTION.
Bluetooth control
Connect, disconnect or forget a paired device, and turn Bluetooth on or off. Addresses are the device addresses from the bluetooth data. See Bluetooth for what each platform supports.
| Event | Data | Response |
|---|---|---|
BLUETOOTH_CONNECT |
address |
BLUETOOTH_CONNECTED |
BLUETOOTH_DISCONNECT |
address |
BLUETOOTH_DISCONNECTED |
BLUETOOTH_FORGET |
address |
BLUETOOTH_FORGOTTEN |
BLUETOOTH_SET_POWER |
powered, true or false |
BLUETOOTH_POWER_SET |
Forgetting a device unpairs it, so it has to be paired again before it can connect.
{
"id": "abc123",
"token": "abc123",
"event": "BLUETOOTH_CONNECT",
"data": {
"address": "80:C3:BA:7B:93:4E"
}
}
The response comes once the device has connected, which can take up to 30 seconds, and the bluetooth module updates straight after. An unknown address gets an ERROR with the subtype BAD_REQUEST, and something the platform can’t do gets INVALID_ACTION.
Camera control
Send the CAMERA_CONTROL event to set one of a webcam’s controls, such as its brightness, zoom or autofocus. It works while another app is using the camera. Linux only.
id(required): The camera’sidfrom thecamerasdata.control(required): The control’sidfrom that camera’scontrols.value(required): A whole number within the control’sminimumandmaximum, in itssteps. A boolean’s is0or1, and a menu’s is one of itsoptions’value.
{
"id": "abc123",
"token": "abc123",
"event": "CAMERA_CONTROL",
"data": {
"id": "usb-Insta360_Insta360_Link_2C-video-index0",
"control": "zoom_absolute",
"value": 200
}
}
The response type is CAMERA_CONTROLLED, and the cameras module updates straight after. An unknown camera or control, a read-only control or a value it can’t take gets an ERROR with the subtype BAD_REQUEST. A control another one sets automatically, such as focus while autofocus is on, can’t be set until that one is turned off.
Camera capture
Send the CAMERA_CAPTURE event to take a picture with a webcam. Linux only, and needs ffmpeg.
id(required): The camera’sidfrom thecamerasdata.widthandheight(optional): One of the camera’ssizesto take it at. Without them, it’s taken at the camera’s current size. While another app is streaming from the camera through PipeWire, it’s taken at that app’s size whatever these are.
{
"id": "abc123",
"token": "abc123",
"event": "CAMERA_CAPTURE",
"data": {
"id": "usb-Insta360_Insta360_Link_2C-video-index0",
"width": 1920,
"height": 1080
}
}
The response type is CAMERA_CAPTURED, with the picture as a base64 JPEG:
{
"id": "abc123",
"type": "CAMERA_CAPTURED",
"subtype": "NONE",
"data": {
"id": "usb-Insta360_Insta360_Link_2C-video-index0",
"content_type": "image/jpeg",
"image": "/9j/4AAQSkZJRgABAQ..."
},
"message": "Picture taken"
}
A camera can see the room it’s in, so this only works with the cameras.capture setting on. It’s off by default, and otherwise gets an ERROR with the subtype INVALID_ACTION. Turn it on with UPDATE_SETTINGS.
A camera nothing is using is started for the picture, at the size it’s set to, and stopped again, which takes a second or two. While another app streams it through PipeWire, as browsers and most video call apps do, the picture is shared from that stream, which needs GStreamer’s PipeWire plugin. An app with the camera open directly has it to itself, so that gets an ERROR naming the app. The cameras and system modules update after each picture, as starting the camera shows in camera_usage.
System control
Change system settings. Linux only.
| Event | Data | Response |
|---|---|---|
SYSTEM_SET_POWER_PROFILE |
profile, one of the system data’s power_profiles |
SYSTEM_POWER_PROFILE_SET |
SYSTEM_SET_KEEP_AWAKE |
enabled, true or false, and optionally duration in seconds |
SYSTEM_KEEP_AWAKE_SET |
Switching the power profile needs power-profiles-daemon, or tuned-ppd on Fedora.
Keeping awake stops the system idling, so it doesn’t lock, blank the screen or sleep for being left alone. With a duration, it lets the system idle again after that many seconds. On Omarchy it turns on the bar’s stay awake switch. Elsewhere it holds a logind idle inhibitor, which most desktops and idle daemons, such as hypridle, follow, until it’s turned off or System Bridge stops.
{
"id": "abc123",
"token": "abc123",
"event": "SYSTEM_SET_POWER_PROFILE",
"data": {
"profile": "performance"
}
}
The system module updates straight after. A profile that isn’t in power_profiles gets an ERROR.
Desktop control
Focus, close and change windows, show a workspace, and switch the keyboard layout. Window and workspace IDs are the ids from the desktop data. See desktop for what each desktop supports.
| Event | Data | Response |
|---|---|---|
DESKTOP_CLOSE_WINDOW |
id |
DESKTOP_WINDOW_CLOSED |
DESKTOP_FOCUS_WINDOW |
id |
DESKTOP_WINDOW_FOCUSED |
DESKTOP_FOCUS_WORKSPACE |
id |
DESKTOP_WORKSPACE_FOCUSED |
DESKTOP_SET_KEYBOARD_LAYOUT |
layout, one of the keyboard’s layouts |
DESKTOP_KEYBOARD_LAYOUT_SET |
DESKTOP_SET_WINDOW_STATE |
id, and any of fullscreen, maximised and minimised as true or false |
DESKTOP_WINDOW_STATE_SET |
Closing a window asks it to close, as its close button does, so an app may ask to save first.
{
"id": "abc123",
"token": "abc123",
"event": "DESKTOP_SET_WINDOW_STATE",
"data": {
"id": "0x555dbf21c9e0",
"fullscreen": true
}
}
The desktop module updates straight after. An unknown window, workspace or layout gets an ERROR with the subtype BAD_REQUEST, and something the desktop can’t do gets INVALID_ACTION.
Display control
Send the DISPLAY_CONTROL event to set a display’s brightness, its power state, or both. See displays for what’s supported.
id(required): The display’sidfrom thedisplaysdata.brightness(optional): Brightness in percent, 0-100.power_state(optional):on,standby,suspendoroff. External monitors only, except on Hyprland, which turns any display on or off.
{
"id": "abc123",
"token": "abc123",
"event": "DISPLAY_CONTROL",
"data": {
"id": "HDMI-A-2",
"brightness": 40
}
}
The response type is DISPLAY_CONTROLLED, and the displays module updates straight after. A display that can’t be controlled that way gets an ERROR with the subtype INVALID_ACTION.
Hyprland control
On Hyprland, float, pin, move, resize and kill windows, rename workspaces and move them between displays, run dispatchers on the focused window, trigger global shortcuts, switch the submap, read and set any config option, read and move the cursor, read the log, send custom events, and reload the config. With the desktop.hyprlandCode setting on, run commands, any dispatch and Lua too. Anywhere else these get an ERROR with the subtype INVALID_ACTION.
| Event | Data | Response |
|---|---|---|
HYPRLAND_DISPATCH |
dispatcher, a direction for move_focus and swap_window, and optionally the id of a window to run it on |
HYPRLAND_DISPATCHED |
HYPRLAND_DISPATCH_RAW |
dispatch, written as the running config takes it |
HYPRLAND_RAW_DISPATCHED |
HYPRLAND_EVAL |
code, one line of Lua |
HYPRLAND_EVALUATED |
HYPRLAND_EXEC |
command |
HYPRLAND_EXECUTED |
HYPRLAND_GET_CURSOR_POSITION |
None | HYPRLAND_CURSOR_POSITION, with x and y |
HYPRLAND_GET_LOG |
None | HYPRLAND_LOG, with lines, oldest first |
HYPRLAND_GET_OPTION |
name, any option |
HYPRLAND_OPTION, with the option’s value |
HYPRLAND_KILL_WINDOW |
id |
HYPRLAND_WINDOW_KILLED |
HYPRLAND_MOVE_CURSOR |
x and y, in pixels across every display |
HYPRLAND_CURSOR_MOVED |
HYPRLAND_MOVE_WINDOW |
id, and the workspace to move it to |
HYPRLAND_WINDOW_MOVED |
HYPRLAND_MOVE_WORKSPACE |
id, a workspace id, and the display to move it to, from the displays data |
HYPRLAND_WORKSPACE_MOVED |
HYPRLAND_RELOAD_CONFIG |
None | HYPRLAND_CONFIG_RELOADED |
HYPRLAND_RENAME_WORKSPACE |
id, a workspace id, and its new name, or none for the default |
HYPRLAND_WORKSPACE_RENAMED |
HYPRLAND_RUN_GLOBAL_SHORTCUT |
name, from the global_shortcuts in the desktop data’s hyprland |
HYPRLAND_GLOBAL_SHORTCUT_RUN |
HYPRLAND_SEND_EVENT |
name, the event’s text |
HYPRLAND_EVENT_SENT |
HYPRLAND_SET_OPTION |
name, any option, and its value |
HYPRLAND_OPTION_SET |
HYPRLAND_SET_SUBMAP |
name, a submap the binds in the desktop data’s hyprland are in, or none for the default |
HYPRLAND_SUBMAP_SET |
HYPRLAND_SET_WINDOW |
id, and any of floating and pinned as true or false, x and y, and width and height |
HYPRLAND_WINDOW_SET |
Moving a window leaves the focus where it is. Hyprland only pins floating windows, so pinning a tiled window floats it first, and tiling a pinned window unpins it. x and y place the window’s top-left corner in pixels across every display, and width and height size it, each as a pair. Hyprland only places a floating window exactly, so float it in the same request. Killing a window ends its app without asking, unlike DESKTOP_CLOSE_WINDOW, so anything unsaved is lost. Only the global shortcuts apps have registered and the dispatchers below can run, unless the desktop.hyprlandCode setting is on.
{
"id": "abc123",
"token": "abc123",
"event": "HYPRLAND_RUN_GLOBAL_SHORTCUT",
"data": {
"name": "omarchy:panel.omarchy.clock"
}
}
Dispatchers
The dispatchers act on the focused window, as their binds would. With an id, that window is focused first:
| Dispatcher | Effect |
|---|---|
center_window |
Center the window. Floating windows only. |
cycle_next_window |
Focus the next window on the workspace. |
cycle_previous_window |
Focus the previous window on the workspace. |
group_next |
Show the next window in the window’s group. |
group_previous |
Show the previous window in the window’s group. |
lock_group |
Lock the window’s group, so windows can’t be moved in or out. |
lock_groups |
Lock every group. |
move_focus |
Move the focus to the window in a direction. |
swap_window |
Swap the window with the one in a direction. |
toggle_group |
Turn the window into a group, or break up its group. |
toggle_split |
Switch the split the window is in between horizontal and vertical, in the dwindle layout. |
unlock_group |
Unlock the window’s group. |
unlock_groups |
Unlock every group. |
The direction is down, left, right or up. When Hyprland refuses, such as centring a tiled window, you get an ERROR with its reason.
{
"id": "abc123",
"token": "abc123",
"event": "HYPRLAND_DISPATCH",
"data": {
"dispatcher": "move_focus",
"direction": "left"
}
}
Options
Option names take dots or colons, such as general.col.active_border or general:layout. An option Hyprland doesn’t have gets an ERROR with the subtype NOT_FOUND. HYPRLAND_OPTION replies with its name, its type, set for whether the config sets it rather than leaving the default, and its value in the field for its type: bool, int, float, vec2 as two numbers, or string for string, css, gradient and custom options.
{
"id": "abc123",
"token": "abc123",
"event": "HYPRLAND_GET_OPTION",
"data": { "name": "general.col.active_border" }
}
{
"id": "abc123",
"type": "HYPRLAND_OPTION",
"subtype": "NONE",
"data": {
"name": "general.col.active_border",
"type": "gradient",
"set": true,
"bool": null,
"int": null,
"vec2": [],
"float": null,
"string": "66cccccc 0deg"
}
}
HYPRLAND_SET_OPTION reads the option’s type first, then takes a value to match: true or false for bool, a whole number for int, any number for float, and text for string. A vec2 takes two numbers, as "5 10" or [5, 10], a css option up to four whole numbers, such as "5 10", and a gradient colours and an optional angle, as HYPRLAND_OPTION reports them, such as "ff33ccff 00ff99ee 45deg". A value set this way lasts until Hyprland’s config reloads.
The desktop data’s hyprland.options reports animations.enabled, decoration.blur.enabled, decoration.dim_inactive, decoration.rounding, decoration.shadow.enabled, general.border_size, general.gaps_in, general.gaps_out, input.touchpad.natural_scroll and misc.vrr as they change. Those only take a number of 0 or more.
{
"id": "abc123",
"token": "abc123",
"event": "HYPRLAND_SET_OPTION",
"data": {
"name": "animations.enabled",
"value": false
}
}
Running code
HYPRLAND_EXEC, HYPRLAND_DISPATCH_RAW and HYPRLAND_EVAL can do anything you can, so they only run with the desktop.hyprlandCode setting on. It’s off by default, and otherwise they get an ERROR with the subtype INVALID_ACTION. Turn on Run commands, dispatches and Lua through Hyprland on the web client’s Desktop page, or use UPDATE_SETTINGS.
HYPRLAND_EXECruns a command in Hyprland’s environment, as a bind toexecwould.HYPRLAND_DISPATCH_RAWruns any dispatch, written as the running config takes it: Lua such ashl.dsp.focus({ workspace = "2" })when thedesktopdata’shyprland.version.config_providerislua, otherwise classic such asworkspace 2.HYPRLAND_EVALruns one line of Lua, with a Lua config. Hyprland only replies whether it ran, not what it returned.
{
"id": "abc123",
"token": "abc123",
"event": "HYPRLAND_EVAL",
"data": { "code": "hl.notification.create({ text = \"Hi\" })" }
}
Omarchy control
While the Omarchy shell runs, open, close or toggle its menu and the panels of its plugins. Otherwise these get an ERROR with the subtype INVALID_ACTION.
| Event | Data | Response |
|---|---|---|
OMARCHY_MENU |
action, and the id of a menu to open or toggle |
OMARCHY_MENU_CHANGED |
OMARCHY_PANEL |
action and the id of a panel |
OMARCHY_PANEL_CHANGED |
The action is open, close or toggle. The ids are in the desktop data’s omarchy. A menu also opens by one of its aliases, so capture opens trigger.capture. An id the shell doesn’t have gets an ERROR with the subtype BAD_REQUEST.
{
"id": "abc123",
"token": "abc123",
"event": "OMARCHY_PANEL",
"data": {
"action": "toggle",
"id": "omarchy.audio"
}
}
Send notification
Send the NOTIFICATION event to show a desktop notification. title and message are required; the rest are optional.
{
"id": "abc123",
"token": "abc123",
"event": "NOTIFICATION",
"data": {
"title": "Hello",
"message": "World"
}
}
Available fields:
title(required): The notification title.message(required): The notification body.icon(optional): The icon name.duration(optional): How long to show the notification, in milliseconds.actionUrl(optional): A URL to open when the notification is clicked.actionPath(optional): A file or folder path to open when the notification is clicked.sound(optional): Path to a sound file to play with the notification.urgency(optional):low,normalorcritical. Critical notifications stay until they’re dismissed. Linux only.buttons(optional): Up to 3 buttons, each with anidand alabel. Theidcan be anything butdefault. Linux only.
The response type is NOTIFICATION_SENT, and its data is what was sent with the notification’s id added. Use the id to dismiss it, and to match the events about it. It’s 0 where the notification can’t be followed, such as on Windows.
{
"id": "abc123",
"token": "abc123",
"event": "NOTIFICATION",
"data": {
"title": "Washing machine",
"message": "The wash has finished",
"urgency": "critical",
"buttons": [
{ "id": "snooze", "label": "Remind me in 10 minutes" },
{ "id": "done", "label": "Done" }
]
}
}
Notification events
On Linux, every connected client is sent an event when a notification System Bridge sent is clicked or closes, whichever client sent it:
| Type | Data |
|---|---|
NOTIFICATION_ACTION |
id, and action: the button’s id, or default when the notification itself was clicked |
NOTIFICATION_CLOSED |
id, and reason: expired, dismissed (by you) or closed (with NOTIFICATION_DISMISS) |
These come with the id system, as they don’t answer a request.
Dismiss notification
Send the NOTIFICATION_DISMISS event with a notification’s id to close it. The response type is NOTIFICATION_DISMISSED. Linux only, and only for notifications System Bridge sent.
{
"id": "abc123",
"token": "abc123",
"event": "NOTIFICATION_DISMISS",
"data": {
"id": 47
}
}
Open a path
Send the OPEN event with a path to open a file or folder with the default application.
{
"id": "abc123",
"token": "abc123",
"event": "OPEN",
"data": {
"path": "C:\\users\\user\\Downloads\\example.txt"
}
}
Open a URL
Send the OPEN event with a url to open it in the default browser.
{
"id": "abc123",
"token": "abc123",
"event": "OPEN",
"data": {
"url": "https://timmo.dev"
}
}
Process control
Send the PROCESS_KILL event to end a process. It’s asked to stop, as closing it would, unless you force it.
id(required): The process’sidfrom theprocessesdata.force(optional):trueto end it straight away, losing anything unsaved.
{
"id": "abc123",
"token": "abc123",
"event": "PROCESS_KILL",
"data": {
"id": 12345
}
}
The response type is PROCESS_KILLED, and the processes module updates straight after. A process that doesn’t exist, or System Bridge itself, gets an ERROR with the subtype BAD_REQUEST, and one the backend’s user isn’t allowed to end gets an ERROR with the system’s reason.
Power control
Control the system power state with one of the following events. Each takes empty data.
POWER_HIBERNATEPOWER_LOCKPOWER_LOGOUTPOWER_RESTARTPOWER_SHUTDOWNPOWER_SLEEP
{
"id": "abc123",
"token": "abc123",
"event": "POWER_SLEEP",
"data": {}
}
Power state changes
Every connected client gets a POWER_STATE_CHANGED message with the id system just before the system sleeps or shuts down, and again when it wakes, whatever started it. data.state is one of:
| State | When |
|---|---|
sleeping |
Before sleep or hibernate |
shutting_down |
Before power off or restart. Linux only; elsewhere the connection just closes |
resumed |
After waking |
{
"id": "system",
"type": "POWER_STATE_CHANGED",
"subtype": "NONE",
"data": {
"state": "sleeping"
}
}
On waking, System Bridge also announces itself over mDNS again, so clients that discover it, such as Home Assistant, hear it’s back.
The TypeScript connector has them as the powerStateChanges stream, and the Python connector passes them to listen()’s power_state_callback.