Wayland workspace overlay daemon

woven

Press a key, see all your workspaces and windows at once, click to focus. Plus a fully Lua-driven status bar.

Super+` overlay appears click a window focused
✓ Niri (primary) ✓ Hyprland ⚠ Sway ✕ GNOME

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:

ButtonAction
Close window
Toggle float
Toggle fullscreen
Toggle pin

Overview Controls

ActionResult
Click a window cardFocus that window, close overlay
Hover a window cardShow action buttons
Right-click / any keyClose overlay
ScrollScroll through workspaces

Compositor Support

CompositorStatusNotes
Niri✓ Primary targetFull support, designed for Niri
Hyprland✓ SupportedWell-tested
Sway⚠ Basic supportWorks, some edge cases
GNOME✕ Not supportedDoes not implement wlr-layer-shell
KDE✕ Not supportedOut 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
PathContents
~/.config/woven/woven.luaYour main config
~/.config/woven/plugins/User plugins (local or from GitHub)
~/.config/woven/runtime/Lua runtime files (shipped with woven)
/run/user/$UID/woven.sockIPC 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.

Make sure ~/.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"; }
}
Niri is the primary target. All development happens on Niri first.

Hyprland

exec-once = woven
bind = SUPER, grave, exec, woven-ctrl --toggle

Sway

exec woven
bindsym Super+grave exec woven-ctrl --toggle
⚠ Sway support is implemented but lightly tested.

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,
})
KeyTypeDescription
backgroundhex stringOverlay background color
borderhex stringWindow card border color
texthex stringWindow title text color
accenthex stringFocused window / active highlight
border_radiusnumberCard corner radius (px)
fontstringFont name — match exact fc-list output
font_sizenumberFont size in points
opacityfloat 0–1Overall overlay opacity
Built-in presets selectable in woven-ctrl: Catppuccin Mocha, Dracula, Nord, Tokyo Night, Gruvbox

woven.workspaces()

woven.workspaces({
    show_empty = true,
    min_width  = 200,
    max_width  = 400,
})
KeyTypeDefaultDescription
show_emptyboolfalseShow workspaces with no windows
min_widthnumber200Min workspace column width (px)
max_widthnumber400Max workspace column width (px)

woven.settings()

woven.settings({
    scroll_dir      = "vertical",
    overlay_opacity = 0.92,
})
KeyTypeDefaultDescription
scroll_dirstring"horizontal""horizontal" or "vertical"
overlay_opacityfloat 0–10.92Overlay 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

ValueDescription
linearConstant speed
ease_out_cubicFast start, slow end — good for opening
ease_in_cubicSlow start, fast end — good for closing
ease_in_out_cubicSlow at both ends — good for scrolling
springPhysics-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" },
    },
})
KeyTypeDefaultDescription
enabledbooltrueStart/stop the bar thread
positionstring"top"Edge of screen to anchor to
stylestring"solid"Bar visual style
heightnumber32Bar thickness in pixels
modulestablesee belowModule 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" },
}
ModuleDescription
activitiesActivity/workspace indicator dot
workspacesClickable workspace switcher
window_titleFocused window title (requires compositor IPC)
clockCurrent time
dateCurrent date
weatherWeather conditions (requires coordinates)
networkNetwork interface status and speed
audioVolume level
batteryBattery level and charging status
cavaAudio spectrum visualizer (requires cava)
cpuCPU usage percentage
memoryRAM usage
diskDisk usage (root filesystem)
tempCPU temperature
mediaNow playing (MPRIS)
notificationsNotification 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 keyDescription
backgroundSolid-mode bar background / pill overlay tint
foregroundPrimary text color
accentHighlighted / active element color
dimInactive / secondary text color
radiusWidget pill corner radius (px)
font_sizeFont size in logical pixels
bubbles keyDescription
backgroundPill background color
radiusPill corner radius (px)
gapGap between adjacent pills (px)
paddingHorizontal padding inside each pill (px)
marginVertical 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.

The bar reads config exclusively from 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

PluginFileDescription
app_rulesapp_rules.luaPer-app accent color overrides
ws_loggerws_logger.luaWorkspace/window event logger (journalctl)
clockclock.luaBar clock widget
datedate.luaBar date widget
batterybattery.luaBar battery widget
networknetwork.luaBar network widget
cavacava.luaAudio spectrum visualizer
nowplayingnowplaying.luaMPRIS now-playing widget
launcherlauncher.luaApp launcher integration
greetinggreeting.luaTime-based greeting overlay
uptimeuptime.luaSystem uptime widget
sysinfosysinfo.luaCPU/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

EventPayload fields
workspace_focusid, name
window_openclass, title, workspace_id
window_closeclass, title
window_focusclass, title

Core APIs

APIPurpose
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

CommandDescription
woven-ctrlOpen the GUI control panel
woven-ctrl --toggleToggle overlay open/closed
woven-ctrl --showForce overlay open
woven-ctrl --hideForce overlay closed
woven-ctrl --reloadReload woven.lua without restarting
woven-ctrl --setupRun 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

CrateRole
woven-sysMain process: Lua VM, IPC server, compositor backend, bar thread orchestration
woven-renderRender thread: Wayland layer surface, tiny-skia painter, input handling
woven-barStatus bar: widget engine, Wayland layer surface, per-module renderers
woven-commonShared types and IPC protocol definitions
woven-ctrlIced GUI + CLI control panel
woven-protocolsGenerated Wayland protocol bindings
woven-pluginPlugin 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.

Niri quirk: Never null-buffer unmap surfaces. Keep permanently mapped and toggle keyboard interactivity instead.

Compositor Backends

CompositorDetection env varIPC
Niri$NIRI_SOCKETNiri IPC socket
Hyprland$HYPRLAND_INSTANCE_SIGNATUREHyprland socket
Sway$SWAYSOCKSway 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

CratePurpose
smithay-client-toolkitWayland client abstractions (SCT 0.20)
wayland-protocolszwlr_layer_shell_v1, screencopy, xdg
mluaLua 5.4 runtime
tiny-skiaSoftware rasterizer
fontdueFont rasterization
icedwoven-ctrl GUI framework
tokioAsync runtime (IPC server)
serde_jsonLua → 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

Near Term — Planned

v3 — Ecosystem Unification

Not Planned