woven
Press a key, see all your workspaces and windows at once, click to focus. Plus a fully Lua-driven status bar.
What It Does
Woven is a Wayland overlay daemon. It provides a workspace overview, a persistent status bar with Lua-configured widgets, an AI workspace namer, per-app accent colors, a control center GUI, and a full plugin system. The entire system is scriptable — write Lua plugins to add bar widgets, custom panels, and behavior hooks. Plugins can persist state with woven.store, make HTTP requests with woven.http.get, and react to compositor events. Install community plugins directly from woven-ctrl.
Window Cards
Each window in the overview is a card showing its title. Hover a card to reveal action buttons:
| Button | Action |
|---|---|
| ✕ | Close window |
| ⧉ | Toggle float |
| ⊞ | Toggle fullscreen |
| ⬡ | Toggle pin |
Overview Controls
| Action | Result |
|---|---|
| Click a window card | Focus that window, close overlay |
| Hover a window card | Show action buttons |
| Right-click / any key | Close overlay |
| Scroll | Scroll through workspaces |
Compositor Support
| Compositor | Status | Notes |
|---|---|---|
| Niri | ✓ Primary target | Full support, designed for Niri |
| Hyprland | ✓ Supported | Well-tested |
| Sway | ⚠ Basic support | Works, some edge cases |
| GNOME | ✕ Not supported | Does not implement wlr-layer-shell |
| KDE | ✕ Not supported | Out of scope |
Project Structure
woven/
├── crates/woven-sys/ main daemon — Lua VM, IPC server, compositor backend
├── crates/woven-render/ render thread — Wayland surface, tiny-skia painter
├── crates/woven-common/ shared types, IPC protocol, plugin API
├── crates/woven-ctrl/ Iced GUI control panel + CLI
├── crates/woven-protocols/ Wayland protocol bindings
├── crates/woven-plugin/ plugin system crate
├── crates/woven-bar/ status bar — widget engine, Wayland layer surface
├── plugins/ bundled Lua plugins
├── runtime/ Lua runtime files loaded at startup
└── get.sh one-line installer
| Path | Contents |
|---|---|
~/.config/woven/woven.lua | Your main config |
~/.config/woven/plugins/ | User plugins (local or from GitHub) |
~/.config/woven/runtime/ | Lua runtime files (shipped with woven) |
/run/user/$UID/woven.sock | IPC socket |
Installation
Quick Install
Downloads the latest prebuilt release and installs everything automatically.
curl -fsSL https://viewerofall.pages.dev/install/woven/install.sh | sh
Installs: binaries to ~/.local/bin/, runtime + plugins + config skeleton to ~/.config/woven/, systemd user service, desktop entry, and icon.
~/.local/bin is in your $PATH. Add export PATH="$HOME/.local/bin:$PATH" to your shell rc if not.AUR (Arch / CachyOS)
yay -S woven
Compositor Setup
Niri
spawn-at-startup "woven"
binds {
Super+Grave { spawn "woven-ctrl" "--toggle"; }
}
Hyprland
exec-once = woven
bind = SUPER, grave, exec, woven-ctrl --toggle
Sway
exec woven
bindsym Super+grave exec woven-ctrl --toggle
Manual Install — From Release
Download the tarball from the releases page, then:
tar -xzf v2.5.0.tar.gz
cd woven-v2.5.0
mkdir -p ~/.local/bin ~/.config/woven ~/.config/systemd/user
cp exec/woven exec/woven-ctrl ~/.local/bin/
cp woven.lua ~/.config/woven/ # skip if you already have one
cp -r runtime plugins ~/.config/woven/
cp woven.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now woven.service
Manual Install — From Source
git clone https://github.com/viewerofall/woven.git && cd woven
cargo build --release
mkdir -p ~/.local/bin ~/.config/woven ~/.config/systemd/user
cp target/release/woven target/release/woven-ctrl ~/.local/bin/
cp woven.lua ~/.config/woven/
cp -r runtime plugins ~/.config/woven/
cp woven.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now woven.service
Verify
systemctl --user status woven
woven-ctrl --toggle # overlay should appear
Uninstall
systemctl --user stop woven && systemctl --user disable woven
rm ~/.local/bin/woven ~/.local/bin/woven-ctrl
rm -rf ~/.config/woven
rm ~/.config/systemd/user/woven.service
systemctl --user daemon-reload
Configuration
Config lives at ~/.config/woven/woven.lua. Edit directly or use woven-ctrl for the GUI editor. Apply changes live:
woven-ctrl --reload
woven.theme()
Controls all visual properties of the overlay.
woven.theme({
background = "#1e1e2e",
border = "#6c7086",
text = "#cdd6f4",
accent = "#cba6f7",
border_radius = 12,
font = "JetBrainsMono Nerd Font",
font_size = 13,
opacity = 0.92,
})
| Key | Type | Description |
|---|---|---|
background | hex string | Overlay background color |
border | hex string | Window card border color |
text | hex string | Window title text color |
accent | hex string | Focused window / active highlight |
border_radius | number | Card corner radius (px) |
font | string | Font name — match exact fc-list output |
font_size | number | Font size in points |
opacity | float 0–1 | Overall overlay opacity |
woven.workspaces()
woven.workspaces({
show_empty = true,
min_width = 200,
max_width = 400,
})
| Key | Type | Default | Description |
|---|---|---|---|
show_empty | bool | false | Show workspaces with no windows |
min_width | number | 200 | Min workspace column width (px) |
max_width | number | 400 | Max workspace column width (px) |
woven.settings()
woven.settings({
scroll_dir = "vertical",
overlay_opacity = 0.92,
})
| Key | Type | Default | Description |
|---|---|---|---|
scroll_dir | string | "horizontal" | "horizontal" or "vertical" |
overlay_opacity | float 0–1 | 0.92 | Overlay grid opacity |
woven.animations()
woven.animations({
overlay_open = { curve = "ease_out_cubic", duration_ms = 180 },
overlay_close = { curve = "ease_out_cubic", duration_ms = 180 },
scroll = { curve = "ease_out_cubic", duration_ms = 180 },
})
Available Curves
| Value | Description |
|---|---|
linear | Constant speed |
ease_out_cubic | Fast start, slow end — good for opening |
ease_in_cubic | Slow start, fast end — good for closing |
ease_in_out_cubic | Slow at both ends — good for scrolling |
spring | Physics-based spring motion |
woven.namer()
AI workspace namer — automatically names workspaces based on open windows. Runs locally, no network calls.
woven.namer({ enabled = true })
Per-App Accent Colors
Override the auto-generated accent color for specific apps in the workspace overview. Uses window class names (lowercase).
require("plugins.app_rules").setup({
["kitty"] = "#ff6c6b",
["firefox"] = "#ff9500",
["discord"] = "#7289da",
["code"] = "#007acc",
})
Full Default Config
woven.theme({
background = "#1e1e2e",
border = "#6c7086",
text = "#cdd6f4",
accent = "#cba6f7",
border_radius = 10,
font = "JetBrainsMono Nerd Font",
font_size = 13,
opacity = 0.80,
})
woven.workspaces({
show_empty = true,
min_width = 200,
max_width = 400,
})
woven.settings({
scroll_dir = "vertical",
overlay_opacity = 0.92,
})
woven.animations({
overlay_open = { curve = "ease_out_cubic", duration_ms = 180 },
overlay_close = { curve = "ease_out_cubic", duration_ms = 180 },
scroll = { curve = "ease_out_cubic", duration_ms = 180 },
})
woven.bar({
enabled = true,
position = "bottom",
style = "bubbles",
height = 32,
modules = {
left = { "activities", "workspaces" },
center = { "weather", "clock", "date" },
right = { "cava", "|", "network", "audio", "battery" },
},
})
woven.namer({ enabled = true })
require("plugins.app_rules").setup({
["kitty"] = "#ff6c6b",
})
require("plugins.ws_logger").setup()
Bar
Woven includes a built-in Wayland layer-shell status bar. Configuration is entirely through woven.bar({}) in woven.lua — there is no separate config file. Changes take effect on woven-ctrl --reload.
woven.bar()
woven.bar({
enabled = true,
position = "bottom", -- "top" | "bottom" | "left" | "right"
style = "bubbles", -- "bubbles" | "solid"
height = 32,
modules = {
left = { "activities", "workspaces" },
center = { "weather", "clock", "date" },
right = { "cava", "|", "network", "audio", "battery" },
},
})
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Start/stop the bar thread |
position | string | "top" | Edge of screen to anchor to |
style | string | "solid" | Bar visual style |
height | number | 32 | Bar thickness in pixels |
modules | table | see below | Module placement |
Modules
Modules are placed in three zones: left, center, and right. In bubbles style, use "|" as a separator to split a zone into multiple pill groups.
modules = {
left = { "activities", "workspaces" },
center = { "weather", "clock", "date" },
right = { "cava", "|", "network", "audio", "battery" },
}
| Module | Description |
|---|---|
activities | Activity/workspace indicator dot |
workspaces | Clickable workspace switcher |
window_title | Focused window title (requires compositor IPC) |
clock | Current time |
date | Current date |
weather | Weather conditions (requires coordinates) |
network | Network interface status and speed |
audio | Volume level |
battery | Battery level and charging status |
cava | Audio spectrum visualizer (requires cava) |
cpu | CPU usage percentage |
memory | RAM usage |
disk | Disk usage (root filesystem) |
temp | CPU temperature |
media | Now playing (MPRIS) |
notifications | Notification count indicator |
"|" | Separator — splits into separate bubble groups (bubbles style only) |
Styles
bubbles
Each module group gets its own rounded pill background. Groups are defined by "|" separators in the module lists. The bar background is transparent — pills float on your wallpaper.
solid
Traditional full-width bar with a solid background. Separators are ignored in solid mode.
Theme
Bar colors are configured via the optional theme block inside woven.bar():
woven.bar({
enabled = true,
style = "bubbles",
theme = {
background = "#0a0010",
foreground = "#cdd6f4",
accent = "#c792ea",
dim = "#4a3060",
radius = 6,
font_size = 12.5,
},
bubbles = {
background = "#1a0a2e",
radius = 12,
gap = 6,
padding = 10,
margin = 4,
},
modules = {
left = { "activities", "workspaces" },
center = { "weather", "clock", "date" },
right = { "cava", "|", "network", "audio", "battery" },
},
})
| theme key | Description |
|---|---|
background | Solid-mode bar background / pill overlay tint |
foreground | Primary text color |
accent | Highlighted / active element color |
dim | Inactive / secondary text color |
radius | Widget pill corner radius (px) |
font_size | Font size in logical pixels |
| bubbles key | Description |
|---|---|
background | Pill background color |
radius | Pill corner radius (px) |
gap | Gap between adjacent pills (px) |
padding | Horizontal padding inside each pill (px) |
margin | Vertical inset from bar edge (px) |
Weather Widget
The weather widget requires coordinates. Set them in the weather block or via environment variables:
woven.bar({
-- ...
weather = { lat = 40.7128, lon = -74.0060 },
})
Or set environment variables before starting woven:
export WOVEN_LAT=40.7128
export WOVEN_LON=-74.0060
woven-ctrl Bar Settings
The woven-ctrl GUI includes a Bar tab for toggling the bar, switching position (top/bottom/left/right), and switching style (bubbles/solid) without editing Lua directly. The changes are written back to woven.lua and reloaded automatically.
woven.lua. There is no bar.toml — if you have one from an older install, delete it.Plugins
Plugins are Lua files loaded from ~/.config/woven/plugins/. Woven ships with a set of bundled plugins, and you can write your own or install community ones.
Bundled Plugins
| Plugin | File | Description |
|---|---|---|
| app_rules | app_rules.lua | Per-app accent color overrides |
| ws_logger | ws_logger.lua | Workspace/window event logger (journalctl) |
| clock | clock.lua | Bar clock widget |
| date | date.lua | Bar date widget |
| battery | battery.lua | Bar battery widget |
| network | network.lua | Bar network widget |
| cava | cava.lua | Audio spectrum visualizer |
| nowplaying | nowplaying.lua | MPRIS now-playing widget |
| launcher | launcher.lua | App launcher integration |
| greeting | greeting.lua | Time-based greeting overlay |
| uptime | uptime.lua | System uptime widget |
| sysinfo | sysinfo.lua | CPU/RAM info widget |
Lua Plugin API
Every plugin is a Lua module with a setup() function. Load them from woven.lua:
require("plugins.my_plugin").setup({ option = "value" })
Event Hooks
React to workspace and window events inside a plugin or directly in woven.lua:
woven.on("workspace_focus", function(data)
woven.log.info("focused workspace: " .. tostring(data.id))
end)
woven.on("window_open", function(data)
woven.log.info("opened: " .. data.class .. " — " .. data.title)
end)
Available Events
| Event | Payload fields |
|---|---|
workspace_focus | id, name |
window_open | class, title, workspace_id |
window_close | class, title |
window_focus | class, title |
Core APIs
| API | Purpose |
|---|---|
woven.store.get(key) | Retrieve value from persistent KV store |
woven.store.set(key, value) | Store value — survives hot-reloads and restarts |
woven.http.get(url) | Sync HTTP GET, returns body string or nil |
woven.log.info(msg) | Log to journalctl at info level |
woven.log.warn(msg) | Log at warn level |
woven.on(event, fn) | Register an event hook |
woven.on_error(fn) | Register error handler — receives error string, shows toast + notify-send |
woven.namer({}) | Enable AI workspace naming |
Plugin Example
-- ~/.config/woven/plugins/my_plugin.lua
local M = {}
function M.setup(cfg)
woven.on("workspace_focus", function(data)
local count = (woven.store.get("focus_count") or 0) + 1
woven.store.set("focus_count", count)
woven.log.info("workspace switches: " .. count)
end)
end
return M
Then load it in woven.lua:
require("plugins.my_plugin").setup({})
woven-ctrl
woven-ctrl is both a GUI control panel and a CLI tool. Run it bare for the GUI, pass flags for terminal/compositor use.
GUI Mode
woven-ctrl
Graphical control panel for: theme editing, preset switching, Lua editor, bar configuration (position, style, enable/disable), plugin management (install from GitHub, enable/disable, per-plugin settings), and overlay control.
CLI Reference
| Command | Description |
|---|---|
woven-ctrl | Open the GUI control panel |
woven-ctrl --toggle | Toggle overlay open/closed |
woven-ctrl --show | Force overlay open |
woven-ctrl --hide | Force overlay closed |
woven-ctrl --reload | Reload woven.lua without restarting |
woven-ctrl --setup | Run first-time setup wizard |
IPC Socket
woven-ctrl communicates via Unix socket:
/run/user/<UID>/woven.sock
Scripting Examples
Hide on game launch
woven-ctrl --hide; %command%; woven-ctrl --show
Auto-reload on config save
while inotifywait -e close_write ~/.config/woven/woven.lua; do
woven-ctrl --reload
done
Check daemon status
systemctl --user is-active --quiet woven && echo running || echo stopped
Architecture
Crate Layout
| Crate | Role |
|---|---|
woven-sys | Main process: Lua VM, IPC server, compositor backend, bar thread orchestration |
woven-render | Render thread: Wayland layer surface, tiny-skia painter, input handling |
woven-bar | Status bar: widget engine, Wayland layer surface, per-module renderers |
woven-common | Shared types and IPC protocol definitions |
woven-ctrl | Iced GUI + CLI control panel |
woven-protocols | Generated Wayland protocol bindings |
woven-plugin | Plugin system crate |
Daemon Lifecycle
woven starts
└─ woven-sys initializes
└─ Lua VM loads ~/.config/woven/runtime/boot.lua
└─ Lua VM loads ~/.config/woven/woven.lua
└─ woven.theme() / woven.settings() / woven.workspaces() apply
└─ woven.bar({ enabled=true }) → spawns bar thread (woven-bar)
└─ woven.namer() → enables AI workspace naming
└─ require("plugins.*") → loads bundled/user plugins
└─ IPC socket created at /run/user/$UID/woven.sock
└─ Compositor backend connects (Niri / Hyprland / Sway)
└─ Render thread idles (surface mapped, keyboard non-interactive)
woven-ctrl --toggle
└─ if hidden → render thread activates keyboard, queries compositor,
renders workspace cards, commits surface
└─ if visible → keyboard deactivated, surface hides
Bar Thread
The bar runs in its own thread spawned by woven-sys when woven.bar({ enabled=true }) is executed. The thread owns its own Wayland connection and layer surface. On --reload, the old bar thread is signaled to shut down and a new one is spawned with the updated config.
woven.bar({}) called in Lua
└─ signal old bar thread to exit (AtomicBool)
└─ if enabled=false → done
└─ deserialize config: Lua table → JSON → BarConfig
└─ spawn new bar thread
└─ create Wayland layer surface (zwlr_layer_shell_v1)
└─ build widget instances from modules config
└─ enter render loop (33ms tick if cava active, else 1s)
Wayland Surface (Overlay)
layer: TOP
anchor: TOP | BOTTOM | LEFT | RIGHT (fullscreen)
exclusive_zone: -1 (overlay — doesn't push other surfaces)
keyboard: exclusive while visible, none when hidden
Surface is kept permanently mapped. Show/hide toggles keyboard interactivity — the compositor renders the surface as transparent when nothing is painted.
Compositor Backends
| Compositor | Detection env var | IPC |
|---|---|---|
| Niri | $NIRI_SOCKET | Niri IPC socket |
| Hyprland | $HYPRLAND_INSTANCE_SIGNATURE | Hyprland socket |
| Sway | $SWAYSOCK | Sway IPC socket |
Lua VM
mlua (Lua 5.4) runs in woven-sys. Loads on startup and on every --reload. Config is applied by executing woven.lua — each woven.* call immediately applies its effect (theme, bar thread spawn, plugin registration).
Key Dependencies
| Crate | Purpose |
|---|---|
smithay-client-toolkit | Wayland client abstractions (SCT 0.20) |
wayland-protocols | zwlr_layer_shell_v1, screencopy, xdg |
mlua | Lua 5.4 runtime |
tiny-skia | Software rasterizer |
fontdue | Font rasterization |
iced | woven-ctrl GUI framework |
tokio | Async runtime (IPC server) |
serde_json | Lua → JSON → BarConfig deserialization |
Troubleshooting
Overlay doesn't appear on --toggle
Check the daemon is running:
systemctl --user status woven
Check logs:
journalctl --user -u woven -n 50
Check IPC socket:
ls /run/user/$(id -u)/woven.sock
Daemon starts then immediately exits
Usually a Lua error in woven.lua. Check logs:
journalctl --user -u woven -n 50
Validate Lua syntax before reloading:
luac -p ~/.config/woven/woven.lua
Bar doesn't appear / appears when disabled
The bar is configured only through woven.bar({}) in woven.lua. If you have a bar.toml from an older install, delete it — it is no longer read:
rm ~/.config/woven/bar.toml
Toggle the bar:
woven.bar({ enabled = false }) -- disable
woven.bar({ enabled = true }) -- enable
Then reload:
woven-ctrl --reload
Overlay appears but is blank
The compositor backend can't get workspace data. On Niri:
niri msg workspaces
Should return instantly. If $NIRI_SOCKET isn't set, woven can't connect.
window_title bar module is blank
window_title requires compositor IPC (SWAYSOCK / NIRI_SOCKET). On Niri, it uses the Niri socket for window queries. If the socket isn't reachable, the widget stays blank — replace it with clock or date in center.
Font not rendering / showing boxes
fc-list | grep -i "jetbrains"
Use the exact string from fc-list output in woven.lua. Font names are case-sensitive.
Overlay doesn't close on keypress
Another layer-shell surface may be holding exclusive keyboard focus. Workaround: bind woven-ctrl --hide explicitly.
Getting Help
File an issue at github.com/viewerofall/woven/issues. Include: woven version, compositor + version, journalctl --user -u woven -n 50, and your woven.lua.
Roadmap
v2.5 — Current
- Full Lua bar config:
woven.bar({})is the single source of truth — no TOML intermediary - Bubbles bar style: rounded pill groups with staggered intro animations
- Bar widgets: cava, clock, date, network, audio, battery, cpu, memory, disk, temp, weather, media, workspaces, activities, window_title
- AI workspace namer: local, no network calls
- Per-app accent colors: app_rules plugin
- Plugin system: event hooks, bar widget / panel / overlay slots, install from GitHub
- Plugin manager: woven-ctrl Plugins tab — install, enable/disable, remove, per-plugin settings modal
- woven.store: Persistent KV store — survives hot-reloads and restarts
- woven.http.get: Sync HTTP GET in Lua plugins (ureq-based)
- Error handler:
woven.on_errorhook, toast overlay, notify-send fallback - woven-ctrl bar tab: GUI for enable/disable, position, style
- Hot-reload:
woven-ctrl --reloadrestarts bar thread with new config
Near Term — Planned
- Inline workspace rename: Click workspace name in overview, type, Enter to rename
- Bar quick-strip: Right-click bar for volume/brightness sliders
- Window peek on hover: Screencopy thumbnail on workspace card hover
- Low battery indicator: Bar color shift or blinking icon below 15%
v3 — Ecosystem Unification
- Cross-socket awareness: woven and woven-shell discover each other
- Event bridge: battery low, media change, workspace change → plugin callbacks
- woven-notify: Notification daemon (
org.freedesktop.NotificationsDBus) - woven.ipc Lua API:
woven.ipc.emit("battery.low", { pct = 12 })
Not Planned
- GNOME / KDE: No
wlr-layer-shell— out of scope - macOS / Windows: Out of scope