sway-groups (swayg)
Group-aware workspace management for sway, with waybar integration via waybar-dynamic.
On "optional" bar integration. In principle
swaygis bar-agnostic — it manages state in sway, and the bar integration is a separate output channel. In practice waybar + waybar-dynamic is currently the only supported renderer, and without it there is no visible feedback on which groups exist or which workspaces the active group contains. So unless you plan to write your own bar module against the waybar-dynamic IPC, treat waybar as a required dependency.
Workspaces are organised into named groups. Each output has an active group, and only workspaces that belong to the active group (plus globals and user-unhidden ones) are shown to waybar and included in group-aware navigation. Workspace state is persisted in a small SQLite DB so switching back to a group restores its last focus.
Key concepts
- Workspace — a sway workspace (
1,2,3:Firefox, …). - Group — a named collection of workspaces. Each output has one active group at a time.
- Global workspace — visible in all groups (e.g. a persistent notes workspace).
- Hidden workspace — a workspace marked as hidden in a specific group.
By default hidden workspaces are invisible to waybar and skipped by
navigation, so you can declutter the bar during presentations or deep
work. Toggle
show_hidden_workspacesto reveal them with a.hiddenCSS class applied (combinable with.global,.focused, …).
Requirements
- Rust toolchain (stable, edition 2024)
- sway
- waybar + waybar-dynamic — see note above
Installation
cargo install --git (recommended right now)
No crates.io publishing needed:
Both binaries land in ~/.cargo/bin/. Make sure that's in your PATH.
cargo install --path (from a local clone)
Later: cargo install from crates.io
Once the crates are published (in order: sway-groups-config →
sway-groups-core → sway-groups-cli / sway-groups-daemon) it becomes:
systemd user unit for the daemon
cargo install cannot install non-binary files, so copy this unit once
into ~/.config/systemd/user/swayg-daemon.service:
[Unit]
Description=swayg daemon - track external sway workspace events
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=%h/.cargo/bin/swayg-daemon
Restart=on-failure
RestartSec=5
Environment=RUST_LOG=sway_groups_daemon=info
[Install]
WantedBy=graphical-session.target
# paste the unit above into ~/.config/systemd/user/swayg-daemon.service
The unit is WantedBy=graphical-session.target. For sway users, make sure
the target actually gets activated — the common recipe is a small session
target that sway starts. Create
~/.config/systemd/user/sway-session.target:
[Unit]
Description=sway compositor session
BindsTo=graphical-session.target
…and in your sway config:
exec systemctl --user --no-block start sway-session.target
waybar-dynamic integration
Install waybar-dynamic, then add two modules to your waybar config:
"cffi/swayg_groups": {
"module_path": "/path/to/libwaybar_dynamic.so",
"name": "swayg_groups"
},
"cffi/swayg_workspaces": {
"module_path": "/path/to/libwaybar_dynamic.so",
"name": "swayg_workspaces"
}
swayg pushes widget updates to these modules automatically after every
state-changing command. For per-state CSS classes see Bar styling
below.
First-time setup
This seeds the DB from sway's current workspaces, creates the default
group (0), and pushes initial bar widgets.
CLI overview
Every command is documented under --help:
High-level tour:
# Groups
# Workspace membership
# Hiding (auto-focuses away when the focused workspace becomes invisible)
# Navigation (group-aware — skips hidden unless show_hidden=true)
# Container moves
# State
# Global flags
swayg status sample:
show_hidden_workspaces = false
eDP-1: active group = "dev"
Visible: 1, 3
Inactive: 2, 4
Hidden: 5
Global: 0
- Visible — in the active group (plus globals) and not user-hidden
- Inactive — belongs to other groups; exists in sway on this output
- Hidden — user-hidden in the active group (only shown if
show_hidden_workspaces = true) - Global —
is_global = trueworkspaces
Configuration
swayg config dump prints the default TOML. Save to
~/.config/swayg/config.toml (or any path passed via --config or
SWAYG_CONFIG=) and edit.
Current sections:
[defaults]—default_group,default_workspace(used when orphan workspaces need a home, e.g. aftergroup delete --force)[bar.workspaces]/[bar.groups]— per-bar tuning: socket instance name, display mode (all|active|none),show_global,show_empty[[assign]]— workspace assignment rules (see below)
Assignment rules
When the daemon sees a new workspace, it normally adds it to the active
group. Assignment rules let you override this per workspace name — useful
together with sway's assign / for_window rules:
# Exact match: put "music" in media + bg, mark global
[[]]
= "music"
= ["media", "bg"]
= true
# Regex match: any workspace starting with "dev_" goes to dev group
[[]]
= "^dev_"
= "regex"
= ["dev"]
match— pattern to match against the workspace name.match_type—"exact"(default) or"regex".groups— groups to add the workspace to. When set, replaces the default "add to active group" behaviour.global— mark the workspace as global (true/false).
If a rule sets global = true but specifies no groups, the workspace
is still added to the active group (in addition to being global).
Multiple rules can match the same workspace — their groups are merged.
Runtime DB flags (separate from the config file):
show_hidden_workspaces— toggled viaswayg workspace show-hidden
Bar styling

Widgets emitted by swayg carry CSS classes you can style. The available
classes are:
swayg_workspaces:focused(focused on this output),visible(visible on another output),urgent,global(is_globalflag),hidden(only sent whenshow_hidden_workspaces = true). Classes combine, e.g..focused.global,.hidden.global.focused.swayg_groups:active(active group on the focused output),urgent(a workspace in the group is urgent).
Example theme (lavender workspaces, blue groups)
This is the theme used in the screenshot above — drop it into your
~/.config/waybar/style.css:
/* ── swayg workspaces — lavender, lime accent for globals ───────── */
}
}
}
}
/* Global workspaces: lime text + border */
}
}
}
}
}
}
}
/* Hidden (only shown when show_hidden_workspaces = true):
faded + italic + dashed border signals "would normally be invisible". */
}
}
}
}
}
}
/* Urgent wins: full visibility, solid border, no italic */
}
}
}
}
/* ── swayg groups — same structure, blue accent ─────────────────── */
}
}
}
}
}
Storage locations
- SQLite DB:
~/.local/share/swayg/swayg.db - Log files:
~/.local/share/swayg/swayg.YYYY-MM-DD(daily rotation) - Config (optional):
~/.config/swayg/config.toml - Daemon state:
/tmp/swayg-daemon-test.state(test daemon only)
Reset all state:
Architecture
Workspace crates:
| Crate | Role |
|---|---|
sway-groups-config |
TOML config schema + loader |
sway-groups-core |
DB entities, services, sway/waybar IPC |
sway-groups-cli → swayg |
User-facing CLI |
sway-groups-daemon → swayg-daemon |
Catches sway IPC events (new/empty workspace, etc.), keeps DB + bars in sync |
sway-groups-dummy-window |
Wayland dummy window for tests (publish = false) |
sway-groups-tests |
Integration tests against a live sway session (publish = false) |
Tables
workspaces,groups— main entitiesworkspace_groups— many-to-many membershiphidden_workspaces— presence-based(workspace_id, group_id)pairsoutputs— per-output state (including active group)settings— global runtime flags (key/value)focus_history,group_state,pending_workspace_events— internal state for nav-back and daemon coordination
Troubleshooting
RUST_LOG=debug swayg <cmd>— verbose tracing to stderr- Log files under
~/.local/share/swayg/ swayg repair— reconcile DB with sway (removes stale workspaces etc.)swayg sync --all --init-bars --init-bars-retries 20 --init-bars-delay-ms 500— afterswaymsg reload, retry pushing to waybar until its socket is back up
Development
The integration test suite spawns a test-mode daemon, temporarily stops
the production daemon, and tears everything down in Drop. All tests
must be able to run against a real sway socket.
Waybar test progress
During test runs a waybar custom module shows which test is running
and overall progress (n/m). The test fixture writes JSON to
/tmp/swayg-test-progress.json which waybar polls every second.
Add the module to your waybar config (e.g. in modules-center):
"custom/swayg_tests": {
"exec": "cat /tmp/swayg-test-progress.json 2>/dev/null || echo '{}'",
"return-type": "json",
"interval": 1,
"tooltip": true
}
Suggested CSS (pill badge, yellow while running, green when done):
}
}
}
License
MIT