JWM is a tag-based window manager and compositor with Compiz-style aesthetics
and a built-in desktop shell. The primary production surface is Wayland DRM/KMS
(wayland-udev); X11 (x11rb / xcb) remains a first-class compatibility
surface with the same policy and IPC. Nested Wayland backends exist for CI and
development only.
It combines tag-based tiling, multiple layouts, multi-monitor control, animations
and compositor effects with a JSON IPC control plane that is designed to be
diagnosable (doctor, health, capabilities, support bundles).
Release status: JWM has not published a stable release. The manifest version identifies development builds, not a production-support commitment. See compatibility, the tested upgrade/rollback lifecycle, the maintainer release process, and the hardware validation gate for the first release.
| Reliability | Built-in shell | Effects | |
|---|---|---|---|
| JWM | Doctor / health / capabilities / support bundles; dual X11 + Wayland | Control center, notifications, launcher, lock, clipboard, idle | Cube/prism, expose, wobbly, glow — damage-aware |
| Hyprland | Large community daily-drive | Typically external (Quickshell, …) | Strong visual polish |
| niri | Focused scrollable model | Typically external | Clean, lighter chrome |
| Sway | Protocol maturity and trust | External | Minimal by design |
JWM's bet is shell + effects + control-plane hardness on one policy tree — not a new tiling paradigm and not a feature-count race.
- Production backend: direct Wayland DRM/KMS (
wayland-udev). Barejwmselects it when compiled in (JWM_BACKEND/--backendstill override). - X11RB and XCB window-manager backends with an integrated X11 compositor (equal policy parity; not the default session).
- Nested X11 and nested winit Wayland backends with XWayland for development.
- Tile, monocle, floating, scrolling, grid, deck, fibonacci, centered-master, bstack, three-column, tatami, fullscreen, and vertical-stack layouts.
- Tags, per-monitor state, overview/expose, display layout UI, screenshots, screen/audio recording, session restore, gestures, accessibility filters, HDR/VRR/color-management plumbing, and direct-scanout diagnostics.
- Music and video playback from the Shell Hub, with
searchable local libraries, playlists, a native player window, and shared
media keys / now-playing controls (
mpvruntime). - Opt-in, data-only desktop visual extensions: any program can publish bounded RGBA images through the independent SDK/protocol.
- Full-screen WaterLily.jl simulation frames on the X11RB/XCB compositor, produced by juliafun/waterlily on CPU, CUDA, or ROCm (no Julia dependency in JWM).
- Live configuration reload and a newline-delimited JSON IPC API exposed through
jwm-tool. - Authenticated JWM-to-JWM remote viewing and XTEST control for trusted X11
LANs, through
jwm-remote(x11rb and xcb sessions). - Read-only startup health checks, semantic configuration diagnostics, and privacy-aware support bundles.
JWM requires the normal Linux X11, Wayland, DRM/GBM, libinput, libseat, EGL/GL,
ALSA, D-Bus, and font/rendering development packages for your distribution.
The built-in MP4 recorder additionally requires the ffmpeg and ffprobe
executables (the ffmpeg package on Debian/Ubuntu).
The minimum supported Rust version with the committed Cargo.lock is 1.89;
it is declared in Cargo.toml and checked in CI.
On a fresh Debian/Ubuntu machine, scripts/bootstrap_deps.sh installs every
native dependency plus the Rust toolchain (via rustup, since distro packages are
older than the 1.89 floor) in one step:
bash scripts/bootstrap_deps.sh # apt packages + Rust + default bar prerequisites
JWM_CN_MIRROR=1 bash scripts/bootstrap_deps.sh # China: rustup + cargo via rsproxy.cn
bash scripts/bootstrap_deps.sh --help # options include --with-portal, --with-tauri, --cnThe default xilem_bar builds with Cargo. For optional Tauri bars, run
bash scripts/bootstrap_deps.sh --with-tauri and follow the
WebKitGTK and frontend prerequisites.
--with-portal (or JWM_WITH_PORTAL=1) adds the PipeWire headers needed by the
screencast portal. On non-Debian distros the script prints the required library
groups to map to your package manager. Then build:
cargo build --locked --release
cargo test --locked --lib --bins --testsThe release build produces jwm, jwm-tool, jwm-support, and jwm-remote.
Before starting a display backend, inspect the environment and configuration:
target/release/jwm --backend wayland-udev --doctor
target/release/jwm --backend x11rb --doctor --jsonDoctor is the daily-drive gate: a blocking error means do not treat the session as production-ready. See hardware validation.
To update a directory of projects from GitHub or an SSH host, see the repository updater.
X11 and Wayland use separate files under ~/.config/jwm. Prefer a Wayland
session for day-to-day use:
target/release/jwm --update-config
target/release/jwm --backend wayland-udev --check-config
target/release/jwm --backend x11rb --check-config
# Primary production session (also the CLI default when compiled in):
target/release/jwm --backend wayland-udev
# X11 compatibility:
target/release/jwm --backend x11rbSupported backend names are x11rb, xcb, wayland-udev, wayland-x11, and
wayland-winit. See startup and configuration for aliases,
logging, benchmarking, restart behavior, and doctor output.
IPC set_config and set_config_batch apply the same whole-configuration
semantic validation before publishing hot-tunable values. A single update also
fails if an unrelated validation error would remain. Use a batch for correlated
hot-tunable changes that must become valid together; non-hot-tunable values must
be edited in the configuration file.
The installation helper builds JWM, the Eva voice service, the independent
jwm-shell host, and the selected status bar (xilem_bar by default;
use --bar to choose another crate under bars/), installs
the session files (Wayland session recommended), and checks both configuration
templates against their last installed versions. The records are stored beside
each config as .config_x11.toml.installed-defaults and
.config_wayland.toml.installed-defaults. Existing configs without a record are
adopted unchanged. Values, comments, ordering, and user-added optional settings
keep the existing configuration. Added, removed, or renamed template fields
trigger a backup and replacement unless the config already has the new fields;
missing configs are created. The installer's --gen-config option uses this
same policy. To explicitly reset both files, run jwm --gen-config.
An ordinary reinstall keeps a customized status_bar.name; an explicit
--bar NAME also updates that setting in both files.
Eva follows the selected debug/release profile, jobs and build cache, and is
installed to the login user's ~/.local/bin/jwm-eva. Existing eva.toml is
preserved. The installer enables and restarts the user service when the user
service manager and graphical session are active; otherwise it installs the
files and prints the commands to enable the service after login. Use --skip-eva to omit Eva, or
--skip-jwm for a bar-only installation. Eva can also be updated independently
with bash scripts/install-eva.sh; see Eva setup.
The source installer includes native JWM Notes (jwm-notes) and
JWM Image Viewer (jwm-image-viewer). Both share the sidebars' Cairo/Pango
self-drawn GUI route, with native X11/Wayland windows and no GTK dependency.
Notes provides searchable, automatically saved notes and a UTF-8 text editor. The viewer supports PNG, JPEG, WebP, GIF (first frame), BMP,
TIFF, ICO and portable bitmaps, with zoom, rotation, fullscreen, drag and drop,
and browsing adjacent images. See native applications.
The source installer makes these the default text/image applications alongside
JWM Music and JWM Video for audio/video, only in JWM sessions.
It writes ~/.config/jwm-mimeapps.list (or
$XDG_CONFIG_HOME/jwm-mimeapps.list), leaving the shared mimeapps.list and
Ubuntu/GNOME defaults unchanged. Existing unrelated JWM associations are kept;
a changed JWM-specific file is backed up as jwm-mimeapps.list.bak. Both X11
and Wayland session entries set XDG_CURRENT_DESKTOP=jwm; log out and select
the updated JWM session after installing if your current session lacks it.
scripts/install-jwm.sh --helpIf a transparent Tauri bar develops old glyph trails or pale rectangles during
metric updates with WebKitGTK 2.50.4 or 2.52.3, see the upstream bitmap-copy fix and native
regression test. This is a WebKit backing-store bug;
changing CSS opacity or clearing the GTK window does not fix its source pixels.
WebKitGTK 2.52.6 includes the fix. The bar installer preserves an installed
private runtime through its launcher when rebuilding a Tauri frontend.
All six Linux tauri_*_bar builds require the pkg-config WebKitGTK development
SDK to be version 2.52.6 or newer; older or missing SDKs fail before the Tauri
build proceeds.
All 28 status-bar frontends can keep their own top bar while JWM supervises one
jwm-shell process for the shared left, right and bottom panels. These panels
are enabled by default in hover mode; changing status_bar.name
then replaces only the top bar. See the configuration and rollback guide
and implementation acceptance record. Native
Wayland physical-output acceptance is tracked separately from nested tests.
The source installer also installs all 14 bundled static PNG wallpapers into
~/.config/jwm/wallpapers/ (or $XDG_CONFIG_HOME/jwm/wallpapers/). Open the
wallpaper picker with Alt+Ctrl+W to choose them alongside your own images.
To install or refresh only those pictures without compiling:
bash scripts/install-wallpapers.sh
bash scripts/test-wallpaper-install.sh # offline regression testReinstalls update JWM-managed images only while their content is unchanged; custom or edited images are never replaced, and existing wallpaper settings are preserved. See static wallpaper installation.
The installer uses CARGO_TARGET_DIR when set (relative paths are resolved from
the project root), otherwise target/. Native bar installation uses a separate
bar-install/<bar> cache and the bar's committed dependency lock. If Cargo
reports a missing generated XCB source such as out/randr.rs, the helper cleans
only XCB artifacts for the selected build profile and retries once. Other build
failures retain their exit status.
Native bars need only the default bootstrap dependencies. Selecting a Tauri web
bar also needs the Tauri 2 Linux libraries (bootstrap_deps.sh --with-tauri) and
builds its frontend: React/Solid/Svelte/Vue variants require Node.js plus
pnpm; Leptos/Yew variants require trunk, Tauri CLI 2, and the
wasm32-unknown-unknown Rust target. The helper checks these prerequisites up
front and prints the exact install command for anything missing.
The source installer rejects unknown options, missing option values, and
non-positive or malformed --jobs values before building or installing.
The bridge activation file uses the target login user's data directory when
run through sudo. Both the bridge and portal installers honor an absolute
XDG_DATA_HOME; the portal also honors an absolute XDG_CONFIG_HOME.
After installing the bridge, the installer reloads a replaced jwm-bridge
process belonging to the desktop user. Replacing its executable alone leaves
the old D-Bus owner running and can make applications wait 25 seconds during
notification discovery. Other notification servers and an already-current
bridge are left alone; staged release installs never touch the live session.
Enable NVIDIA DRM kernel modesetting before using the physical Wayland backend.
The setting also matters under X11: WebKit's DMA-BUF renderer needs working GBM
buffer allocation. With NVIDIA DRM KMS disabled, a Tauri bar can start, map its
transparent window and initialize its frontend, yet display no contents. A
verified NVIDIA 570 / WebKitGTK 2.50.4 case fell back to Mesa's kms_swrast
allocator and logged:
KMS: DRM_IOCTL_MODE_CREATE_DUMB failed: Permission denied
Failed to create GBM buffer of size 2550x42: Permission denied
This error alone does not establish missing user/group permissions: it can occur even when the login user already has read/write ACLs on the DRM devices. Check the loaded NVIDIA module setting:
sudo cat /sys/module/nvidia_drm/parameters/modesetY means enabled; N means disabled. If the path is absent, first check that
the NVIDIA DRM module and driver are installed and loaded. NVIDIA documents the
GBM requirement for DRM KMS
and the persistent activation steps.
On Debian/Ubuntu with initramfs-tools, first resolve any conflicting NVIDIA
modeset=0 module option or nvidia-drm.modeset=0 / nomodeset kernel argument.
Then add the module option and rebuild the initramfs for installed kernels:
echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm-modeset.conf
sudo update-initramfs -u -k allIf you edited GRUB's kernel arguments, also run sudo update-grub. Save your
work and reboot the machine to load the new module setting:
sudo rebootAfter reboot, repeat the sudo cat check above and confirm Y. Restarting JWM
or logging out alone does not reload the NVIDIA module. Recheck WebKit rendering
after enabling KMS; this setting does not rule out other driver/GBM problems.
For compatibility, JWM defaults to WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 for
managed Tauri bars in NVIDIA X11 sessions. This retains WebKit's accelerated
compositor and transfers frames through shared memory. Enabling KMS does not
automatically remove this compatibility default. To test the DMA-BUF hardware
transport after confirming Y, set WEBKIT_DMABUF_RENDERER_FORCE_SHM=0 in the
environment used to launch a future JWM X11 session. See the
bar transport and override notes.
Native Wayland touchpad settings and per-device diagnostics are documented in
touchpad input; use jwm-tool msg get_inputs to see
requested, supported and effective settings. Workspace swipe behavior is
documented in touchpad gestures.
jwm-tool sends typed JSON commands and queries over JWM's private Unix socket:
jwm-tool msg get_windows
jwm-tool msg get_show_bar
jwm-tool msg get_vf
jwm-tool msg get_cp
jwm-tool msg get_pl
jwm-tool msg view --args '{"tag":2}'
jwm-tool msg setlayout --args '{"layout":"scrolling"}'
jwm-tool msg spawn --args '{"cmd":["alacritty"]}'
jwm-tool msg '' --subscribe 'window,tag,layout,bar'
jwm-tool health
jwm-tool health --json
jwm-tool capabilities --jsonget_show_bar occupancy JSON includes tag, prev_tag, layout, gap,
mfact, nmaster, and selected_id.
It also includes sel_tags, previous_tags, and active_tags.
It also includes window_count.
It also includes on_view_count.
It also includes floating_count.
It also includes minimized_count.
It also includes sticky_count.
It also includes urgent_count.
It also includes fullscreen_count.
It also includes pip_count.
It also includes maximized_count.
It also includes above_count.
It also includes below_count.
It also includes scratchpad_count.
It also includes tabbed_count.
It also includes dock_count.
It also includes desktop_count.
It also includes never_focus_count.
It also includes skip_taskbar_count.
It also includes skip_pager_count.
It also includes no_decorations_count.
It also includes drag_float_count.
It also includes swallowed_count.
It also includes demands_attention_count.
It also includes fixed_count.
It also includes strut_count.
It also includes maximize_promoted_count.
It also includes status_bar_count.
It also includes prev_layout.
It also includes closed_placement_count.
jwm-tool health also prints a compact closed_placement line from
get_status.closed_placement (get_cp).
It also prints a compact prev_layout line from get_status.prev_layout
(get_pl).
It also prints compact cfact / gaps / mfact / nmaster lines from the
matching get_status nests.
It also prints a compact layout line from get_status.layout (get_lt).
It also prints a compact tabs line from get_status.tabs (get_tab).
It also prints a compact selected line from get_status.selected (get_sel).
It also prints a compact struts line from get_status.struts (get_strut).
It also prints a compact scratchpads line from get_status.scratchpads (get_pads).
It also prints a compact focused line from get_status.focused (get_fw).
It also prints a compact monitors line from get_status.monitors (get_mons).
It also prints a compact workspaces line from get_status.workspaces (get_ws).
It also prints a compact windows line from get_status.windows (get_wins).
It also prints a compact tree line from get_status.tree (get_tree).
It also prints a compact effects line from get_status.effects (get_fx).
It also prints a compact mic line from get_status.mic (get_mute).
It also prints a compact bench line from get_status.bench (get_bench, get_bm).
It also prints a compact floating line from get_status.floating.
It also prints a compact minimized line from get_status.minimized.
It also prints a compact sticky line from get_status.sticky.
It also prints a compact urgent line from get_status.urgent.
It also prints a compact fullscreen line from get_status.fullscreen.
It also prints a compact pip line from get_status.pip.
It also prints a compact notifications line from get_status.notifications (get_notif).
It also prints a compact blur line from get_status.blur.
It also prints a compact hdr line from get_status.hdr.
It also prints a compact dnd line from get_status.dnd (get_dnd).
It also prints a compact system_ui line from get_status.system_ui (get_ui).
It also prints a compact idle line from get_status.idle (get_idl).
It also prints a compact clipboard line from get_status.clipboard (get_clip).
It also prints a compact session_lock line from get_status.session_lock (get_lock).
It also prints a compact tearing line from get_status.tearing (get_th).
It also prints a compact xwayland line from get_status.xwayland (get_xw).
It also prints a compact scrolling line from get_status.scrolling (get_scroll).
It also prints a compact color_management line from get_status.color_management (get_cm).
It also prints a compact night_light line from get_status.night_light (get_nl).
It also prints a compact magnifier line from get_status.magnifier (get_mag).
It also prints a compact peek line from get_status.peek (get_pk).
It also prints a compact expose line from get_status.expose.
It also prints a compact gesture line from get_status.gesture (get_gest).
It also prints a compact wayland line from get_status.wayland (get_wl).
It also prints a compact recording line from get_status.recording (get_rec).
It also prints a compact audio_recording line from get_status.audio_recording (get_arec).
It also prints a compact capture line from get_status.capture (get_cap).
It also prints a compact waterlily line from get_status.waterlily (get_wly).
It also prints a compact audio line from get_status.audio (get_devices).
It also prints a compact wallpaper line from get_status.wallpaper (get_wall).
It also prints a compact bluetooth line from get_status.bluetooth (get_bt).
It also prints a compact resources line from get_status.resources (get_res).
It also prints a compact connectivity line from get_status.connectivity (get_conn).
It also prints a compact power line from get_status.power.
It also prints a compact media line from get_status.media.
It also prints a compact version_info line from get_status.version_info.
It also prints a compact metrics line from get_status.metrics.
It also prints a compact ipc_caps line from get_status.capabilities.
Malformed JSON, invalid argument types, overflow, empty spawn commands, unknown
commands, and { "success": false } responses produce a non-zero exit status,
so the tool is safe to use from scripts.
A subscription is acknowledged before the event stream starts with
{"success": true, "data": {"unknown_topics": [...], "subscribed": [...], "dropped": [...], "dropped_total": N}}. These fields are additive — the
acknowledgement used to carry no data — so a client that ignores data is
unaffected. unknown_topics lists up to 16 requested topics whose first /
segment is not a registered subscription topic (see subscription_topics in
get_capabilities), such as a mistyped windows: they are kept, but no event
can ever match them. subscribed is the list the server stored, trimmed and
deduplicated, in request order (bar is stored as monitor/bar,
workspace as tag). dropped names up to 16 topics it did not
store as {"topic": ..., "reason": ...} entries, the reason being empty,
too_long (over 128 bytes), duplicate or limit (past 64 topics), and
dropped_total counts every dropped topic, including those past the first 16.
health is a backend-neutral live snapshot of the running JWM instance. Its
versioned JSON includes the actual selected backend, uptime, configuration
health, window/monitor/workspace counts, active features, and compositor metrics
when the backend exposes them. capabilities discovers the supported IPC
commands, queries, and subscription topics. The older jwm-tool status command
retains its existing meaning: it reports the optional process supervisor rather
than querying JWM's live IPC socket.
save_session writes a private, atomic snapshot under
$XDG_STATE_HOME/jwm/session.json (normally
~/.local/state/jwm/session.json); restore also recognizes the legacy cache
location. restore_session reapplies monitor, tag, per-monitor tiling order,
and floating-window state.
jwm-support combines the offline startup doctor with optional live health and
capability queries in a versioned JSON document:
jwm-support --backend wayland-udev --output jwm-support.json
jwm-support --backend x11rb --offline --output jwm-support.json
jwm-support --strict --compact > jwm-support.jsonFile output is private (0600) and atomically replaced. The collector uses a
small environment allowlist and redacts configuration, executable, runtime,
and IPC error details; it excludes HOME, PATH, D-Bus addresses, process command
lines, window titles, and arbitrary environment variables. Review
support bundles before attaching a
report to a public issue.
If you can SSH to the other machine, one command is enough. It starts a loopback-only host there for this session, with a fresh key handed over the SSH channel, and opens it here through an SSH tunnel:
jwm-remote ssh user@desktop --grab-input # whole desktop
jwm-remote ssh user@desktop --grab-input --monitor DP-0Nothing is copied, kept or opened on the LAN, and closing the viewer stops the
host. Both machines need the same jwm-remote version. For a standing host
without SSH, generate and securely copy one private key, explicitly expose the
host on the trusted LAN, then connect from the other JWM machine:
# Managed release bundle only; source installs already use /usr/local/bin.
export PATH="/usr/local/lib/jwm/current/bin:$PATH"
jwm-remote keygen --output ~/.config/jwm/remote.key
jwm-remote host --listen 0.0.0.0:48221 --allow-lan --allow-input \
--key-file ~/.config/jwm/remote.key
jwm-remote connect 192.168.1.50:48221 --grab-input \
--key-file ~/.config/jwm/remote.keyDirect LAN traffic is authenticated and encrypted with ChaCha20-Poly1305, but without forward secrecy: a leaked key file decrypts previously recorded sessions. Rotate the key, or carry the default loopback listener through SSH, when that matters. See the complete setup, security boundary, tuning, and current limits in JWM remote control.
The default modifier is Alt (Mod1). Useful built-in bindings include:
| Binding | Action |
|---|---|
| Alt+Shift+Return | Launch terminal |
| Alt+R | Application launcher (type / for open windows) |
| Alt+Shift+R | Restart JWM; X11 preserves live windows’ tags, output placement, and per-tag layouts |
| Alt+Control+Escape | Lock screen |
| Alt+Shift+Escape | Session menu; logging out requires confirmation |
| Alt+Control+O | Display layout |
| Alt+F10 / Alt+Shift+F10 | Control center / Bluetooth picker |
| Alt+Control+Shift+M / V | Open the music / video library |
| Alt+Control+Shift+, / . | Exchange all tags, windows and layouts with the previous / next monitor; stay on this output |
| Ctrl+Super+F12 | Toggle keyboard shortcut passthrough for the focused remote desktop window |
| Super+Shift+B | Toggle enabled shared sidebars between auto-hide and pinned mode; save the selection |
| Alt+S / Alt+Shift+S | Interactive / immediate desktop screenshot |
| Alt+Control+R | Interactively choose a source and start/stop screen recording |
| Print / Shift+Print | Interactive / immediate desktop screenshot |
| Control+Print | Start/stop screen recording |
| Control+Shift+Print | Move, resize, or replace the active recording source |
| XF86AudioMicMute | Toggle the default microphone's mute (labeled OSD confirms) |
| Alt+Shift+C | Close focused client |
| Alt+N | Minimize focused client |
| Alt+Control+N | Choose a minimized window to restore; Enter or click restores it, Escape cancels |
| Alt+Control+Return | Toggle maximize for the focused tiled or floating window |
| Alt+Control+Space | Toggle the focused window between tiled and floating |
| Alt+Control+Left / Right | Shrink / grow focused window width (2.5%) |
| Alt+Control+Up / Down | Shrink / grow it vertically (2.5%) |
| Alt+Control+Home | Reset layout-local resized dimensions |
| Alt+H / L | Shrink / grow focused window width (2.5%) |
| Alt+Shift+H / L | Grow / shrink height (2.5%; preserves the former factor-key signs) |
| Alt+J / K | Next / previous window; within-column focus in scrolling |
| Alt+Left / Right | Focus the left / right column in scrolling (no-op in other layouts) |
| Alt+Shift+Left / Alt+Shift+Right | Snap the focused floating window to that half of its monitor's work area |
| Alt+Shift+Up | Toggle maximize of the focused floating window (fills the work area; press again to restore) |
Older key lists gain Alt+Control+N in memory when restore_preview has no
configured key or chord binding and that shortcut is unused as a key or chord
leader. See restoring minimized windows.
For Sunlogin / 向日葵 and other remote desktop clients, focus the remote
session window and press Ctrl+Super+F12 (Super is the Win key) to pass JWM keyboard shortcuts to
that window. Press the same key again, or focus another window, to restore
local shortcuts. Returning to the remote window requires enabling it again.
The toggle itself stays local; other keyboard bindings, including the chord
leader, go to the client. Mouse bindings are unchanged. Existing key lists gain
this binding in memory if it is free. A custom direct
toggle_shortcut_passthrough binding replaces the default; a chord alone
cannot serve as the exit. See remote shortcut passthrough.
Focused-window resizing also works with a single client, in every layout. Alt+right-drag keeps a master tiled; larger masters reallocate neighbouring space. See layout resizing for singleton centering, custom bindings, size-hint limits and the explicit floating-resize compatibility action. Existing configuration files keep their own key list; upgrading does not rewrite old H/L bindings. The linked guide includes replacement TOML and reload steps.
Mouse users get the same geometry: dragging a floating window to a side edge
snaps it to that half of the work area (bar, docks and tab bar excluded),
dropping it at the top edge performs the same real maximize as Alt+Shift+Up,
and dropping it into a corner quarters it there.
The quarters have no default bindings, but snap_window top-left /
top-right / bottom-left / bottom-right are bindable and scriptable over
IPC like the three defaults. Alt+Control+Return (togglemaximize)
toggles the focused window's maximize as well, and also takes a tiled
window out of the layout while it is maximized; toggling again puts it back.
See window placement.
| Binding | Action |
|---|---|
| Alt+Control+C | Calculator scratchpad |
| Alt+Control+S | Toggle sticky window |
| Alt+Shift+/ | Show all key and touchpad-gesture bindings (click outside to close) |
These are generated defaults; existing key lists retain their configured keys.
See keyboard upgrades for the
binding changes and adoption steps. WaterLily is launched and controlled with
scripts/waterlily.sh instead of default shortcuts.
During interactive screenshot or recording selection, hover highlights the
window under the pointer with an outside-dim veil (same blue language as the
screenshot editor); a left click picks that window. A bottom-center hint chip lists the mode and primary actions (and the hovered
window title while soft-probing). Press G, W, M, or D to choose a
dragged region, window mode, the monitor under the pointer, or the entire
desktop. Tab and Shift+Tab cycle the same choices; middle-click is the
pointer twin of Tab. Dragging still draws a free region; a near-zero click
over empty desktop leaves selection armed. Recording shows resize handles only
after a source is armed, and the cursor switches to move/resize intents over
those handles (a hand while soft-probing a window). Enter or Space saves a
screenshot or starts/commits recording (and nudges with a toast if nothing is
selected yet). Double-click inside an armed recording region also starts
capture. After selecting a screenshot crop, double-click inside it to save;
selection stays neutral until you explicitly choose a drawing tool. With a
drawing tool selected, clicks inside the crop annotate and double-clicking the
dimmed veil saves. Confirmation requires two clicks within 400 ms and 8 px,
with neither click dragged across the crop edge or beyond that distance.
Arrow keys nudge a committed selection (Shift uses 10-pixel steps), while
Escape, right-click, or the recording shortcut again cancels safely.
Once a screenshot region is committed the selection becomes an editor, and a toolbar floats just outside it — below the selection, or above when there is no room below. The grab cursor follows the active tool inside the selection (I-beam for text, crosshair for drawing) and falls back to a plain pointer over the toolbar or outside the crop. Every tool has both a button and a key, so neither the mouse nor the keyboard is required:
| Tool | Key | Draws |
|---|---|---|
| Pencil | P / F |
Freehand stroke |
| Line | L |
Straight line |
| Arrow | A |
Line with a head |
| Rectangle | R |
Hollow rectangle |
| Filled rectangle | B |
Solid redaction bar |
| Ellipse | C / O |
Hollow ellipse |
| Marker | H |
Translucent highlighter |
| Text | T |
Typed label — click, then type |
| Counter | N |
Auto-numbered bubble; click to place the next one |
| Pixelate | X |
Drag a region down to blocks |
| Invert | I |
Invert a region's colors |
The rest of the strip is the selection's pixel size, the stroke controls, the ink swatch, and the four ways out:
| Control | Key |
|---|---|
| Thinner / thicker stroke | - / +, Ctrl+Down / Ctrl+Up, or the wheel |
| Ink (8-colour ring) | 1…8, or click the swatch to step |
| Undo / redo | Ctrl+Z, Backspace / Ctrl+Shift+Z, Ctrl+Y |
| Long screenshot | S, or the downward-arrow page button |
| Record GIF using this crop | Ctrl+G, or the GIF button |
| Save to file | Enter or Ctrl+S |
| Copy to clipboard | Ctrl+C |
| Cancel | Escape |
For a browser long screenshot, press Alt+S, drag a region around the page's
scrolling content, then press S or click the long-screenshot button. Wait for
the pixel-size hint, move the pointer into the page and scroll down slowly.
JWM samples the live region and stitches overlapping viewports, removing the
repeated content. Pause between scrolls and retain at least a quarter of the
previous viewport. Enter / Space / Ctrl+S saves the combined PNG;
Ctrl+C copies it, and Escape cancels. Capture starts at the current page
position, so scroll to the top before Alt+S to capture the whole page.
This uses the current browser session without an extension or remote-debugging port, on X11 and native Wayland. Exclude browser chrome, fixed headers, sticky sidebars and video from the region; the content must move together vertically. If a scroll is too large or the overlap is ambiguous, the hint asks you to scroll back up; the last accepted image stays intact and saving waits for a matching final viewport. Marks made before starting are retained on the first viewport. During scrolling the pointer belongs to the browser and the toolbar is hidden; finish or cancel with the keyboard. Output is bounded to 65,535 pixels high and the existing 512 MiB RGBA / 64 MiB clipboard PNG limits.
While a text label is open every key is text; Enter commits it, Escape
drops it, and switching tools commits it. A control with nothing to do — undo
with an empty history, thinner at the minimum width — is dimmed rather than
removed, so the row never reflows under the pointer. The strip itself eases
in when the editor appears: one blank frame after it is published, then a
120 ms ease-out fade to full opacity carrying the track, the button chips
and the icons together — a pointer moving across the buttons does not
restart it, withdrawing the strip is instant, and with animations disabled
it is at full opacity from its first frame, rendering no extra frames.
Hovering a button eases its wash in over 120 ms instead of flipping it on;
the leave is still instant, settled states are exactly what they were, and
the hit geometry is unchanged.
The status bars' screenshot pill drives exactly this editor over the control
socket (the take_screenshot IPC command) rather than launching an external
grabber. Clipboard PNG offers prefer a native owner: X11 uses JWM's ICCCM/INCR
path; Wayland uses the compositor data-device selection
(Backend::set_clipboard_png). wl-copy remains a last-resort helper only when
neither native path is available. take_screenshot_fullscreen captures the
whole desktop with no interaction.
Screenshot admission is bounded across all queues and PNG writers to four concurrent captures. Additional requests return a busy error. A single CPU RGBA readback is limited to 512 MiB, with checked dimensions and fallible allocation; oversized captures fail without attempting a giant readback.
Clipboard captures use a one-result in-memory PNG channel, never a private
PNG file in /tmp. Encoded clipboard PNGs are limited to 64 MiB (a new limit
below the native X11 offer limit); oversized output fails explicitly without
reducing image quality. The existing per-capture RGBA limit remains 512 MiB.
These are per-buffer limits, not a total process-memory budget: annotation
work retains the original PNG plus its RGBA image and replacement PNG, and
native clipboard offers may need a PNG copy. Up to four completion watchers
can retain results; the original capture RGBA is released before publishing
its PNG to an annotation worker. A timeout, refused watcher or committed
shutdown/restart cancels the private destination, so late results are dropped.
Cancellation prevents new native-offer enqueue operations after it returns;
it does not retract an offer already queued with the clipboard owner.
Unsupported memory capture fails rather than falling back to a temporary file.
Explicitly saved screenshots retain atomic no-clobber file publication.
A finished capture announces itself: a toast carries the saved path (or
confirms the clipboard copy), and a failed one surfaces even while Do Not
Disturb is on. While a recording runs, a small REC chip with the running
time sits in the bottom-right corner of the recorded output — drawn after
the frame's pixels are read, so it never lands in the video or in
screenshots — and starting and stopping both raise a toast with the output
path. Standalone audio recording (Alt+Ctrl+M) gets the same cue: a red
dot and a static MIC label take the corner while the recorder runs —
stacking directly above the REC chip when both run together — appearing
only once the recorder actually started and clearing on stop, success or
failure.
For an animated GIF, press Alt+Ctrl+R, choose a region or window, then click
GIF or press F (V selects MP4). Enter, Space, or the REC button
starts capture; Alt+Ctrl+R again stops it. MP4 is the initial default and the
last format choice is remembered across restarts. The screenshot editor's
GIF button / Ctrl+G opens the same recorder with its crop already selected.
GIF capture has no audio and leaves a standalone microphone recording running.
On stop, a background worker creates a looping, palette-optimized GIF at 15 fps,
up to 960 pixels wide, without enlarging small captures. A GIF saved toast
announces the completed .gif in the recording output directory. If capture or
export fails, its source video is preserved for recovery; partial GIFs are not
published and existing files are not overwritten. See
GIF recording for IPC and output details.
The X11 compositor freezes the desktop behind the interactive selector/editor
by default. To keep clients and animations live instead, set this in the
[behavior] section; config reload and set_config take effect immediately:
[behavior]
screenshot_freeze_enabled = falseSource-install configuration synchronization requires Python 3.11+ (tomllib)
or Python 3 with tomli already installed. It refuses to rewrite a symlinked
configuration or an edit that changes values other than status_bar.name;
set that one key manually when synchronization is refused. Existing binaries
and configuration remain available on build or validation failure.
The optional portal/ crate provides JWM's screencast portal backend. Its
installer builds the independent manifest, installs a per-user D-Bus activation
service with the correct home path, and restarts an older activated backend:
scripts/install-portal.sh
scripts/test-portal.shSource selection requires consent: the backend auto-detects an installed rofi
or wofi, or uses JWM_PORTAL_PICKER=rofi|wofi|/path/to/custom-picker from its
D-Bus activation environment. If no picker is available, cannot start, or is
cancelled, the request is cancelled. The installer does not install a picker or
change the session environment. Configure that environment before activating the
portal; variables set only on the requesting application do not configure it.
For deliberately unattended sharing, JWM_PORTAL_OUTPUT=<name-or-description>
or JWM_PORTAL_WINDOW=class:<app_id> / title:<substring> explicitly selects
matching sources. An empty or unmatched output override, or an unmatched window
override, cancels rather than selecting another source. This replaces the earlier
MVP behavior that silently chose the first source (or all sources for multi-pick).
ScreenCast source selection shows monitors and windows in one picker. A single-source
request permits exactly one choice across both kinds. Conflicting or ambiguous
JWM_PORTAL_OUTPUT/JWM_PORTAL_WINDOW overrides cancel that request instead of
sharing multiple sources or silently choosing one. Empty overrides also cancel.
Each session follows CreateSession → SelectSources → Start; selection and
startup can each be attempted once. Duplicate creation and calls in the wrong
stage fail. Closing a session invalidates pending work, so a late picker or
capture result cannot reactivate that session or overwrite a new session at the
same object path. PipeWire FDs are returned only for started sessions.
PipeWire socket discovery honors PIPEWIRE_RUNTIME_DIR, then XDG_RUNTIME_DIR,
then USERPROFILE. Relative PIPEWIRE_REMOTE names are resolved in that directory;
absolute socket paths work directly. Remote FD connection attempts have a 500ms
deadline and leave the D-Bus workers available while waiting.
ScreenCast cursor compatibility: when cursor_mode is omitted, the portal uses the
standard Hidden mode. Applications that want the cursor included must explicitly
request Embedded mode. Earlier JWM versions incorrectly defaulted to Embedded.
Dedicated cancellable capture connections require the session’s named
WAYLAND_DISPLAY socket; an inherited-fd-only WAYLAND_SOCKET session is not
supported by this path. Closing a portal session interrupts that private
connection rather than the discovery connection.
Portal builds require PipeWire 1.2+ development files, pkg-config, and libclang.
The PipeWire runtime package alone does not include the headers. The installer
uses a complete system development installation when available; otherwise it
reuses or builds PipeWire 1.6.10 in ~/.cache/jwm/pipewire-prefix, with Meson,
Ninja, Git, and a C compiler. This prefix is private to the portal build.
For a custom prefix, set JWM_PIPEWIRE_PREFIX; it takes priority over system
metadata. Both lib64 and multiarch layouts are supported. The installer derives
the pkg-config search path and runtime rpath, and honors CARGO_TARGET_DIR:
JWM_PIPEWIRE_PREFIX=/opt/pipewire-1.6.10 scripts/install-portal.shThe built-in shell — the control center, native
notifications with a freedesktop D-Bus service,
media controls driven from MPRIS, the
calendar, the application launcher,
the wallpaper picker and its colour theming,
interactive celestial 3D wallpapers and multi-display scenes,
clipboard history, the idle policy,
the resource rows, and the
session menu and night light — is documented per
feature. Multi-monitor desks can also put one screen behind an opaque,
password-lifted shade while the rest keeps working; see
locking one monitor. Every status bar in bars/ carries an entry that opens the
same Shell Hub,
so a pointer-driven session reaches it without Alt+F10. Every panel key is a
toggle: Alt+F10, Alt+F11, Alt+F12, Alt+F9 and the rest each take their
own surface back down, so nobody has to reach for Esc. They are mutually
exclusive too — one panel's key replaces whatever other panel is up, keeping
the grabs — and none of them replaces the lock screen.
On X11, the native Wallpaper configurator (Alt+Ctrl+W) offers the Sun,
eight planets, Moon, Solar System 3D and a Milky Way panorama. Drag bodies or
the sky, zoom, and click a Solar System planet for an isolated close-up. Choose
Span, Mirror, or Independent layouts; connector-based assignments
and shared groups can give several screens one scene while another screen uses
its own theme. The picker saves these choices and includes source-backed body
facts, material provenance and orbital-clock controls. Solar distances are
compressed and bodies enlarged for navigation; see the
celestial wallpaper guide and
scientific model notes. Native Wayland retains the
static-wallpaper fallback.
The Alt+Ctrl+Tab window switcher and the cube tag-switch transition are both
the Compiz-style lit prism documented in cube effects.
Alt+Tab is a hold-the-modifier MRU window switcher,
Alt+E spreads the visible windows into an expose grid,
Alt+O shows every tag of the monitor at once in the
tags overview, and a
monitor with two or more tiled windows shows a
window tab strip across the top of its tiling area.
A window that comes back without JWM launching it, opened from a terminal,
by an agent inside one, or by a running application, returns to the monitor
and tag where that application was last closed; see
window placement.
Alt+Space cycles layouts over a film strip of live thumbnails; see
the layout picker.
Minimized windows fold into the bar, expose a magnifying Dock shelf and show a
compositor-owned hover preview; the cross-process lifecycle is documented in
the minimized-window Dock.
The compositor's live counters are on Alt+Shift+F12; see
the debug HUD.
On direct DRM/KMS sessions, jwm-tool msg get_wayland_status now exposes a
versioned color_delivery snapshot. Its last_policy_decision describes the
most recent composited-route selection and, when evaluated, inventories any
capture, cursor, drag, lock, or top/overlay elements still outside the common
linear tail. This is an observed diagnostic inventory, not a claim that one
listed class caused the selected route. [] means the inventory ran and found
none; a missing/null value means older or non-applicable status. Each output's last_success
records its actual route only after a page-flip/vblank confirms presentation.
The render-decisions view keeps its legacy linear_tail_observation field and
also reports linear_tail_inventory_state (unknown, observed_clear,
observed_blocked, or malformed), a stable issue code, and bounded
total/known/future counts. Future canonical blocker names remain valid; a bad
shape, duplicate, invalid name, safe/list mismatch, or over-limit inventory is
reported as malformed rather than silently becoming unknown.
Every success carries the policy sequence it was queued under, so partial or
mixed multi-output cohorts remain auditable. Hardware color-property and
participation transitions invalidate prior evidence until a replacement frame
reaches vblank. This keeps configured HDR intent, EDID capability, blocked
decisions, and JWM's KMS-tracked presentation state distinct without claiming
to measure the downstream link or panel; get_hdr_status and
get_color_management_status include the same snapshot.
HDR signalling itself — what asks for it, the eleven named reasons an output
can refuse it, and why it is withdrawn and re-asserted per frame — is
documented in HDR and colour delivery.
Additional operational tools are documented in tools/README.md.
The external Julia simulation worker and frame protocol are documented in
docs/waterlily.md.
Architecture boundaries and the incremental migration plan are in
docs/architecture.md. The delivery sequence for larger
changes is tracked in the evolution roadmap. Daily-drive
acceptance loops and the control-plane selling points are in
docs/daily-drive.md; remaining Wayland gaps are queued in
docs/sota-gap-queue.md. First-release hardware
sign-off uses docs/hardware-validation.md.
Release automation accepts only jwm-v<semver> tags whose version exactly
matches the root jwm package. It builds an x86_64 Linux bundle on Ubuntu 22.04,
a tagged source archive, SHA256SUMS, and artifact provenance. Other systems
should build from source; no multi-architecture binary support is claimed.
Monorepo components use independent SemVer. A JWM tag does not change the version of the bridge, portal, shared protocol, bar core/providers, or bars. See CHANGELOG.md and the config, IPC, session, backend, and driver policies in compatibility.
Before opening a pull request, read CONTRIBUTING.md. Please report security-sensitive problems through the private process described in SECURITY.md, not a public issue.
jwm is distributed under the MIT License. The portal/ crate
(the xdg-desktop-portal ScreenCast backend) is covered by the same license,
as already declared in its manifest.