A CLI tool and Rust library for querying and controlling the COSMIC desktop compositor. Inspired by swaymsg, but built specifically for COSMIC.
COSMIC exposes compositor functionality as Wayland protocol extensions rather than a Unix socket IPC. cosmicmsg speaks those protocols directly to query and control workspaces, windows, and outputs from the terminal — and exposes the same functionality as a library for other Rust programs to consume.
| Protocol | Used for |
|---|---|
ext-workspace-v1 + zcosmic-workspace-v2 |
Workspace listing and control |
ext-foreign-toplevel-list-v1 + zcosmic-toplevel-info-v1 |
Window listing |
zcosmic-toplevel-management-v1 |
Window actions (activate, close, maximize, …) |
ext-image-copy-capture-v1 + ext-image-capture-source-v1 |
Screen/window/workspace capture |
zcosmic-workspace-image-capture-source-v1 |
Workspace capture source |
wl-output / xdg-output |
Output (monitor) information |
If you are a Nix user, you can run cosmicmsg directly from the remote flake like this:
nix run github:varbhat/cosmicmsg -- -hYou can also clone the source code. A flake.nix is provided. Enter the dev shell with:
nix develop
cargo build --releaseOr run directly from the flake:
nix run .#cosmicmsg -- get-workspacesFormat all source files:
nix fmtDependencies: wayland, libxkbcommon, pkg-config, and a stable Rust toolchain.
cargo build --release
# binary at target/release/cosmicmsgcosmicmsg [OPTIONS] <COMMAND>
Options:
-j, --json Output compact JSON
-p, --pretty Output pretty-printed JSON
-h, --help
-V, --version
List all workspaces across all outputs.
cosmicmsg get-workspaces
cosmicmsg --pretty get-workspacesExample output (human):
1 * [tiling: enabled] on DP-1
2 on DP-1
3 [pinned] on DP-1
The * marks the active workspace.
List all open windows.
cosmicmsg get-toplevels
cosmicmsg --json get-toplevels | jq '.[].app_id'Example output (human):
"kitty" (kitty) [activated] on 1
"Firefox" (firefox) on 2
"zed" (dev.zed.Zed) [maximized] on 2
List all connected monitors.
cosmicmsg get-outputs
cosmicmsg --pretty get-outputsExample output (human):
DP-1 "Dell U2722D" 2560x1440@59.951Hz at (0, 0)
HDMI-A-1 "LG TV" 1920x1080@60.000Hz at (2560, 0)
Show a full tree of outputs → workspaces → windows.
cosmicmsg get-tree
cosmicmsg --pretty get-treeExample output (human):
Output: DP-1 "Dell U2722D" 2560x1440 at (0, 0)
Workspace: 1 * [tiling: enabled]
- "kitty" (kitty)
- "nvim" (kitty)
Workspace: 2
- "Firefox" (firefox)
- "zed" (dev.zed.Zed)
Workspace: 3 [pinned]
Window and workspace selectors match by name first, then fall back to case-insensitive substring. An error is returned if the selector matches more than one item.
# Switch to a workspace
cosmicmsg workspace activate 2
cosmicmsg workspace activate work # substring match
# Rename
cosmicmsg workspace rename 1 dev
cosmicmsg workspace rename dev work
# Query tiling state (omit name for active workspace)
cosmicmsg workspace get-tiling
cosmicmsg workspace get-tiling 2
# Toggle tiling — use "active" to target the current workspace
cosmicmsg workspace set-tiling active enabled
cosmicmsg workspace set-tiling 1 disabled
# Set the tiling default for all new (empty) workspaces
cosmicmsg workspace set-tiling-default enabled
# Pin / unpin (workspace persists across output changes)
cosmicmsg workspace pin 3
cosmicmsg workspace unpin 3
# Reorder workspaces
cosmicmsg workspace move-before 3 1 # move "3" before "1"
cosmicmsg workspace move-after 1 3 # move "1" after "3"Windows are matched by title, app-id, or identifier (exact match preferred, substring fallback).
# Focus a window
cosmicmsg window activate firefox
cosmicmsg window activate "kitty"
# Close
cosmicmsg window close zed
# Maximize / restore
cosmicmsg window maximize firefox
cosmicmsg window unmaximize firefox
# Minimize / restore
cosmicmsg window minimize firefox
cosmicmsg window unminimize firefox
# Fullscreen / restore
cosmicmsg window fullscreen firefox
cosmicmsg window unfullscreen firefox
# Sticky (show on all workspaces)
cosmicmsg window set-sticky kitty
cosmicmsg window unset-sticky kitty
# Move to a different workspace
cosmicmsg window move-to-workspace zed 2
cosmicmsg window move-to-workspace firefox workSubscribe to compositor events and print them as they arrive. Runs indefinitely until interrupted with Ctrl-C.
cosmicmsg subscribe # human-readable
cosmicmsg --json subscribe # ndjson (one JSON object per line)
cosmicmsg --pretty subscribe # pretty-printed JSON per eventExample output (human):
workspace-updated 1 * [tiling: enabled] on DP-1
window-opened "Firefox" (firefox)
window-updated "Firefox — GitHub" (firefox) [activated]
window-closed "Firefox — GitHub" (firefox)
workspace-added 5 on DP-1
workspace-removed 5
Example output (--json), one object per line:
{"type":"window-opened","window":{"title":"kitty","app_id":"kitty","identifier":"...","state":[],"outputs":["DP-1"],"workspaces":["1"]}}
{"type":"window-updated","window":{"title":"nvim","app_id":"kitty","identifier":"...","state":["activated"],"outputs":["DP-1"],"workspaces":["1"]}}
{"type":"workspace-updated","workspace":{"name":"1","id":null,"coordinates":[0],"active":true,"urgent":false,"hidden":false,"pinned":false,"tiling":"enabled","output":"DP-1","group_id":"..."}}
{"type":"window-closed","title":"kitty","app_id":"kitty","identifier":"..."}type field |
When it fires |
|---|---|
workspace-added |
A new workspace appeared |
workspace-updated |
A workspace changed (active flag, name, tiling, pinned, …) |
workspace-removed |
A workspace was destroyed |
window-opened |
A new window was created |
window-updated |
A window changed (title, state flags, workspace, …) |
window-closed |
A window was destroyed |
# Print a desktop notification whenever a window opens
cosmicmsg --json subscribe | jq -r --unbuffered '
select(.type == "window-opened") |
"New window: \(.window.title) (\(.window.app_id))"
' | while read -r msg; do notify-send "cosmicmsg" "$msg"; done
# Watch for workspace switches
cosmicmsg --json subscribe | jq -r --unbuffered '
select(.type == "workspace-updated" and .workspace.active == true) |
"Switched to: \(.workspace.name)"
'
# Log all window titles that ever become active
cosmicmsg --json subscribe | jq -r --unbuffered '
select(.type == "window-updated" and (.window.state[] == "activated")) |
.window.title
'Every command supports --json (compact) and --pretty (indented) flags for scripting.
# Get the name of the active workspace
cosmicmsg --json get-workspaces | jq -r '.[] | select(.active) | .name'
# List all window titles on workspace "2"
cosmicmsg --json get-toplevels | jq -r '.[] | select(.workspaces[] == "2") | .title'
# Check if any window is fullscreen
cosmicmsg --json get-toplevels | jq 'any(.[]; .state[] == "fullscreen")'
# Get the current output resolution
cosmicmsg --json get-outputs | jq '.[] | select(.name == "DP-1") | "\(.width)x\(.height)"'Capture any output, window, or workspace as a PNG. Output goes to a file or stdout (for piping into image viewers like kitty or chafa).
# Capture the first output to stdout and display in kitty
cosmicmsg capture output | kitty +kitten icat
# Capture output DP-1 to a file
cosmicmsg capture output --output DP-1 --file screenshot.png
# Capture a window by title (substring match)
cosmicmsg capture window firefox | kitty +kitten icat
# Downscale to 50% before encoding
cosmicmsg capture output --scale 0.5 | kitty +kitten icat
cosmicmsg capture output --scale 50% --file small.png
# Capture a workspace
cosmicmsg capture workspace 2 | chafa -
# Include the cursor in the capture
cosmicmsg capture output --cursor | kitty +kitten icatPixel format: PNG RGBA8888. Capture uses shared memory (wl_shm) so no GPU is required.
cosmicmsg is also a Rust library. Add it to your Cargo.toml:
[dependencies]
cosmicmsg = "0.1"use cosmicmsg::{collect_state, Error};
fn main() -> Result<(), Error> {
let (state, _eq) = collect_state()?;
for ws in state.workspaces() {
println!("{}: active={}, tiling={:?}", ws.name, ws.active, ws.tiling);
}
for win in state.toplevels() {
println!("{} ({})", win.title, win.app_id);
}
for out in state.outputs() {
println!("{}: {}x{}", out.name.as_deref().unwrap_or("?"), out.width, out.height);
}
// Full output → workspace → window tree
let tree = state.tree();
Ok(())
}use cosmicmsg::{collect_state, workspace_activate, workspace_set_tiling, window_close};
let (mut state, mut eq) = collect_state()?;
// Switch to workspace "2"
workspace_activate(&state, &mut eq, "2")?;
// Enable tiling on the active workspace
workspace_set_tiling(&state, &mut eq, "active", true)?;
// Close a window
window_close(&mut state, &mut eq, "firefox")?;Available mutation functions:
| Function | Description |
|---|---|
workspace_activate |
Switch to a workspace |
workspace_rename |
Rename a workspace |
workspace_set_tiling |
Enable/disable tiling ("active" targets current) |
workspace_set_tiling_default |
Set tiling default for all empty workspaces |
workspace_pin / workspace_unpin |
Pin/unpin a workspace |
window_activate |
Focus a window |
window_close |
Close a window |
window_maximize / window_unmaximize |
Maximize/restore |
window_minimize / window_unminimize |
Minimize/restore |
window_fullscreen / window_unfullscreen |
Fullscreen/restore |
window_set_sticky / window_unset_sticky |
Sticky (all-workspace) |
window_move_to_workspace |
Move window to a workspace |
capture_output |
Capture an output as PNG |
capture_output_raw |
Capture an output as raw RGBA pixels |
capture_window |
Capture a window as PNG |
capture_window_raw |
Capture a window as raw RGBA pixels |
capture_workspace |
Capture a workspace as PNG |
capture_workspace_raw |
Capture a workspace as raw RGBA pixels |
use cosmicmsg::{subscribe, MonitorEvent, Error};
subscribe(|event| {
match event {
MonitorEvent::WindowOpened { window } => {
println!("opened: {} ({})", window.title, window.app_id);
}
MonitorEvent::WorkspaceUpdated { workspace } if workspace.active => {
println!("active workspace: {}", workspace.name);
}
MonitorEvent::WindowClosed { title, .. } => {
println!("closed: {}", title);
}
_ => {}
}
Ok(()) // return Err(...) to stop the loop
})?;use cosmicmsg::{collect_state, capture_output, capture_output_raw, capture_window_raw, Error};
let (mut state, mut eq) = collect_state()?;
let output = state.first_output().ok_or_else(|| Error::NotFound("no outputs".into()))?;
// PNG directly to a writer
let mut buf = Vec::new();
capture_output(&mut state, &mut eq, output.clone(), &mut buf, false, Some(0.5))?;
std::fs::write("screenshot.png", &buf)?;
// Raw RGBA8888 pixels — process them yourself
let raw = capture_output_raw(&mut state, &mut eq, output, false)?;
println!("{}x{}, {} bytes", raw.width, raw.height, raw.data.len());
// Scale and encode manually
let scaled = raw.scale(0.25);
let mut buf = Vec::new();
scaled.encode_png(&mut buf)?;
// Raw window capture
let raw = capture_window_raw(&mut state, &mut eq, "firefox", false)?;
// raw.data is [R, G, B, A, R, G, B, A, ...], row-major, alpha always 255| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Failed to connect to Wayland display |
| 2 | Required protocol not advertised by compositor |
| 3 | No workspace or window matched the selector |
| 4 | Selector matched multiple items (ambiguous) |
| 5 | Other error |
- Requires COSMIC compositor (
cosmic-comp). The protocols used are COSMIC-specific and won't work on other compositors. - Mutations are best-effort. The compositor is free to ignore requests (e.g.
renameif the compositor doesn't advertiserenamecapability). No error is returned in that case — this matches how the underlying Wayland protocols are specified.