EWM
EWM is a window manager based on Emacs for Wayland.
Installation
Make sure you have emacs-wayland installed. You will also need wl-clipboard and mesa.
Then, clone the repo, https://codeberg.org/ezemtsov/ewm.git, and build from source using
cd compositor cargo build --features=screencast
To launch, run the following in the TTY:
EWM_MODULE_PATH=$(pwd)/target/debug/libewm_core.so \ emacs --fg-daemon -L ../lisp -l ewm -f ewm-start-module
--fg-daemon is required because EWM creates frames dynamically as outputs are discovered, so Emacs must start without initial frames. Add --init-directory to point at your config if it's not in the default location.
In your emacs init file, add:
(use-package ewm
:custom
(ewm-output-config '(("DP-1" :width 2560 :height 1440)))
:bind (:map ewm-mode-map
("s-d" . consult-buffer)))
and start the compositor with (M-x ewm-start-module).
Using Wayland Surfaces
When the compositor starts, Wayland surfaces appear as special buffers. Use standard Emacs commands:
-
C-x b- switch between apps and regular buffers -
C-x 2,C-x 3- split windows (surfaces follow) -
C-x 0,C-x 1- close/maximize windows
Launch applications with s-d (ewm-launch-app). To exit EWM, just exit Emacs as you normally would (C-x C-c or M-x save-buffers-kill-emacs). The compositor shuts down with Emacs and you return to the TTY.
Configuring Keybindings
All bindings in ewm-mode-map are automatically intercepted by the compositor so they work even when a Wayland surface has focus.
Window Navigation
You can override any default binding with use-package:
Example:
(use-package ewm
:bind (:map ewm-mode-map
("s-d" . consult-buffer)
("s-<return>" . vterm)))
Intercept Prefixes
ewm-intercept-prefixes lists keys that always reach Emacs, even when a surface has focus. Each entry is either a plain key or (key :fullscreen):
- Plain keys are intercepted normally but not during fullscreen (the fullscreen surface receives them instead).
- :fullscreen keys are intercepted even when a fullscreen surface has focus.
Override the full list with setopt (or :custom in use-package):
(setopt ewm-intercept-prefixes
'("C-x" "C-u" "C-h" "M-x" "M-`" ; tmm-menubar
("<Print>" :fullscreen))) ; screenshot in fullscreen
Surface Key Translation
ewm-surface-emulate-keys translates Super+KEY into another modifier+KEY when focus is on a non-Emacs Wayland surface, so familiar Super shortcuts behave like the Linux desktop default of Ctrl+KEY inside clients like Firefox or VS Code. Emacs frames are unaffected — ewm-mode-map takes effect there.
Defaults:
| Key in Emacs | Key sent to non-Emacs surface |
|---|---|
| s-c | C-c (copy) |
| s-v | C-v (paste) |
Override with setopt:
(setopt ewm-surface-emulate-keys
'((?\s-c . "ctrl")
(?\s-v . "ctrl")
(?\s-a . "ctrl"))) ; s-a -> C-a (select all) in clients
Input Methods
EWM routes text input between Emacs and Wayland clients, so Emacs input methods (e.g., russian-computer) work transparently in client text fields.
How It Works
Surface buffers are visual proxies: they display the client's content but contain no editable text. All insertions are intercepted and forwarded:
- A keystroke or yank inserts text into the surface buffer.
- ewm-surface--after-change catches the insertion, deletes it from the buffer, and calls ewm-im-commit to send it to the client via the Wayland text-input-v3 protocol.
This means any Emacs command that inserts text automatically works in Wayland clients, including yank (s-v), self-insert-command, insert-char (C-x 8 RET), emoji-insert, and snippet expansion. For example, you can use C-x 8 RET to insert Unicode characters or M-x emoji-insert to pick an emoji, and it will appear in a Firefox text field.
Text Input Mode
When a client text field gains focus (e.g., clicking a Firefox URL bar), the compositor sends a text-input-activated event. With auto-mode enabled, this activates ewm-text-input-mode, which remaps self-insert-command to send keystrokes directly to the client with input method translation applied.
Enable auto-mode:
(ewm-text-input-auto-mode-enable)
Disable it:
(ewm-text-input-auto-mode-disable)
By default, the active Emacs input method (current-input-method) is used for translation. Override it per-session:
(setq ewm-text-input-method "russian-computer")
Instead of cycling through input methods with a single toggle, bind each language to its own key under a shared prefix. This scales to any number of languages and always switches in one chord:
(bind-keys :map ewm-mode-map
;; s-SPC <letter> to switch language
("s-SPC e" . (lambda () (interactive) (set-input-method nil))) ; English (none)
("s-SPC r" . (lambda () (interactive) (set-input-method 'russian-computer)))
("s-SPC n" . (lambda () (interactive) (set-input-method 'norwegian-keyboard)))
("s-SPC s" . (lambda () (interactive) (set-input-method 'swedish-keyboard))))
Because EWM routes all text through Emacs input methods, this works everywhere, in Emacs buffers and Wayland client text fields alike. Switching to Russian with s-SPC r and then typing in a Firefox URL bar produces Cyrillic characters.
Unicode and Emoji in Client Fields
Any Emacs insertion command works in client text fields:
- C-x 8 RET - insert any Unicode character by name
- M-x emoji-insert - pick an emoji with completion
- C-y / s-v - yank from kill ring
These all go through the same ewm-surface--after-change → ewm-im-commit path, so there is nothing extra to configure.
Input Devices
Configure pointer devices and keyboard via ewm-input-config. Each entry is keyed by device type (symbol) or device name (string for per-device overrides). Omitted properties use the device default.
(setopt ewm-input-config
'((touchpad :natural-scroll t :tap t :dwt t)
(mouse :accel-profile "flat")
(trackpoint :accel-speed 0.5)
(keyboard :repeat-delay 200 :repeat-rate 25
:xkb-layouts "us,ru"
:xkb-variants "dvorak,"
:xkb-options "ctrl:nocaps,grp:alt_shift_toggle")
;; Per-device override (exact name from libinput)
("ELAN0676:00 04F3:3195 Touchpad" :tap nil :accel-speed -0.2)))
Resolution order: device-specific > type default > hardware default.
Pointer Properties
| Property | Type | Description | Applies to |
|---|---|---|---|
| :natural-scroll | bool | Invert scroll direction | all |
| :accel-speed | float | Pointer acceleration, -1.0 to 1.0 | all |
| :accel-profile | string | "flat" or "adaptive" | all |
| :scroll-method | string | "two-finger", "edge", "on-button-down", "no-scroll" | all |
| :left-handed | bool | Swap left/right buttons | all |
| :middle-emulation | bool | Emulate middle button from simultaneous L+R click | all |
| :tap | bool | Tap-to-click | touchpad |
| :dwt | bool | Disable touchpad while typing | touchpad |
| :click-method | string | "button-areas" or "clickfinger" | touchpad |
| :tap-button-map | string | "left-right-middle" or "left-middle-right" | touchpad |
Keyboard Properties
| Property | Type | Description |
|---|---|---|
| :repeat-delay | int | Key repeat delay in milliseconds (default 200) |
| :repeat-rate | int | Key repeat rate in Hz (default 25) |
| :xkb-layouts | string | Comma-separated XKB layout names, e.g. "us,ru" |
| :xkb-variants | string | Comma-separated XKB variants, parallel to layouts, e.g. "dvorak," or ",ergol" (empty entry = layout default) |
| :xkb-options | string | XKB options, e.g. "ctrl:nocaps" |
Multiple layouts enable switching. Layout is tracked globally; all windows share the same active layout. During prefix key sequences (C-x, M-x, etc.), the compositor temporarily resets to the base layout so Emacs keybindings work regardless of the active layout.
Why keyboard config is not per-device
Pointer settings (acceleration, scroll, tap) are per-device because libinput applies them at the hardware level: each libinput_device has its own configuration. Keyboard settings work differently:
- XKB layout/options are per-seat, not per-device. The Wayland protocol sends a single wl_keyboard.keymap to each client. All physical keyboards on the seat share the same XKB state. A compositor could maintain separate XKB states internally, but there is no way to communicate per-device keymaps to clients.
- Repeat rate/delay are also per-seat. The Wayland protocol sends a single wl_keyboard.repeat_info event. Clients use these values to implement key repeat themselves. There is no mechanism to vary them by input source.
This is a fundamental constraint of the Wayland protocol, not a limitation of EWM, libinput, or seatd/logind. Every Wayland compositor (Sway, niri, GNOME, KDE) has the same restriction.
Settings take effect immediately when set via setopt (or :custom in use-package). New devices receive the current configuration on hotplug.
Key Interception
The compositor intercepts keys from two sources, redirecting them to Emacs even when a Wayland application has focus.
Super-key bindings
All bindings in ewm-mode-map are automatically intercepted. Default bindings:
| Key | Command | Description |
|---|---|---|
| s-<left> | windmove-left | Focus window left |
| s-<right> | windmove-right | Focus window right |
| s-<down> | windmove-down | Focus window below |
| s-<up> | windmove-up | Focus window above |
| s-c | kill-ring-save | Copy (Ctrl+C in surfaces) |
| s-v | yank | Paste (Ctrl+V in surfaces) |
| s-d | ewm-launch-app | Launch XDG application |
| s-t | tab-new | New workspace tab |
| s-w | tab-close | Close workspace tab |
| s-f | ewm-toggle-fullscreen | Toggle surface fullscreen |
| s-l | ewm-lock-session | Lock session |
| s-1..s-9 | ewm-tab-select-or-return | Switch to tab N (repeat to go back) |
Standard Emacs commands work as expected with Wayland surfaces:
- C-x b - switch between apps and regular buffers
- C-x 2, C-x 3 - split windows (surfaces follow)
- C-x 0, C-x 1 - close/maximize windows
Override default bindings with use-package:
(use-package ewm
:bind (:map ewm-mode-map
("s-d" . consult-buffer)
("s-<return>" . vterm)))
Launching applications
Bind keys to launch apps using lambdas or named functions:
(use-package ewm
:bind (:map ewm-mode-map
("s-<return>" . (lambda () (interactive)
(start-process "alacritty" nil "alacritty")))))
Or define a named command for reuse:
(defun alacritty () (interactive) (start-process "alacritty" nil "alacritty"))
(use-package ewm
:bind (:map ewm-mode-map
("s-<return>" . alacritty)))
For interactive app selection, use ewm-launch-app (bound to s-d by default), which presents XDG desktop applications via completing-read.
Prefix keys
Keys that start command sequences. Focus redirects to Emacs and returns after the sequence completes:
(setopt ewm-intercept-prefixes '("C-x" "C-u" "C-h" "M-x" "M-:"))
Use setopt (or :custom in use-package) so changes reach the running compositor.
Focus-Follows-Mouse
(setopt ewm-focus-follows-mouse t)
Moving the mouse pointer over a Wayland surface automatically focuses it. Also sets mouse-autoselect-window and focus-follows-mouse so Emacs windows within a frame follow the pointer too. Can be toggled at runtime via customize-variable.
Mouse-Follows-Focus
(setopt ewm-mouse-follows-focus t)
Warps pointer to the center of a newly focused window. Skipped when focus was triggered by mouse or the pointer is already inside the target window.
Cursor Auto-Hide
Hide the mouse cursor when it is not in active use. Two independent triggers, both off by default.
;; Hide after 5 seconds of no pointer motion or button events. (setopt ewm-cursor-auto-hide 5)
;; Hide immediately on key press; pointer motion restores it. (setopt ewm-cursor-hide-when-typing t)
ewm-cursor-auto-hide takes a number of seconds, or nil to disable. Only pointer motion and button events restore the cursor and reset the timer; scroll and keyboard input do not, so reading or scrolling through a long document does not flash the cursor back.
ewm-cursor-hide-when-typing hides the cursor on the next key press. Pointer motion restores it. Useful when the cursor sits over the text you are editing.
Both can be enabled together. Hiding is purely visual: clients still receive pointer events, and any custom cursor surface set by a focused client is preserved across hide/show transitions.
Output Configuration
Configure display modes and positions via ewm-output-config:
(setopt ewm-output-config
'(("DP-1" :width 2560 :height 1440 :scale 1.0)
("eDP-1" :width 1920 :height 1200 :scale 1.25 :x 0 :y 0)))
Each entry is keyed by connector name (e.g., DP-1, eDP-1, HDMI-A-1).
Properties
| Property | Type | Description |
|---|---|---|
| :width | integer | Horizontal resolution in pixels |
| :height | integer | Vertical resolution in pixels |
| :refresh | number | Refresh rate in Hz |
| :custom | boolean | Generate a CVT mode for :width/:height/:refresh instead of matching an advertised one (see below) |
| :modeline | string | Explicit X11 modeline; takes precedence over the mode fields (see below) |
| :x | integer | Horizontal position in global coordinate space |
| :y | integer | Vertical position in global coordinate space |
| :scale | float | Fractional scale (e.g. 1.25, 1.5, 2.0) |
| :transform | integer | 0=Normal 1=90 2=180 3=270 4=Flipped 5-7=Flipped+rot |
| :enabled | boolean | Whether the output is enabled (default t) |
Custom Modes
By default :width/:height/:refresh select one of the modes the monitor advertises. To run a resolution the monitor does not advertise — for example a lower resolution on a HiDPI panel for games — add :custom t to generate a CVT mode instead. :refresh is required.
(setopt ewm-output-config
'(("eDP-1" :width 1024 :height 640 :refresh 60 :custom t)))
For full control over the timings, give an explicit X11 :modeline string. It takes precedence over the mode fields when set:
"CLOCK HDISP HSS HSE HTOTAL VDISP VSS VSE VTOTAL +hsync +vsync"
(setopt ewm-output-config
'(("DP-1" :modeline "173.0 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync")))
CLOCK is the pixel clock in MHz; the sync polarities are +hsync/-hsync and +vsync/-vsync. Tools like cvt and gtf print modelines in this form.
Idle Timeout
EWM implements the ext-idle-notify-v1 Wayland protocol, so external idle daemons like swayidle and hypridle work out of the box.
For simple cases, EWM has a built-in idle timeout that can blank the screen or run a command:
;; Blank monitors after 5 minutes (setopt ewm-idle 300)
;; Run a lock screen after 5 minutes (setopt ewm-idle '(300 . "swaylock -f -c 333333"))
Any keyboard or pointer input wakes the screen immediately and restarts the timer. The idle timer is also reset on session unlock and resume from suspend.
Set to nil (default) to disable.
Appearance
Visual settings for how EWM renders surfaces and transitions.
Cursor Theme
Set the cursor theme and size in the environment that starts Emacs/EWM:
export XCURSOR_THEME=Adwaita export XCURSOR_SIZE=24
This must happen before Emacs starts. EWM runs as an Emacs dynamic module, so Emacs is already a GTK process before EWM Lisp or the Rust compositor code runs. Some toolkit cursor state can be initialized during Emacs startup, before ewm-cursor-theme or ewm-cursor-size can apply anything.
EWM also exposes runtime settings:
(setopt ewm-cursor-theme "Adwaita") (setopt ewm-cursor-size 24)
These configure EWM's own named cursor rendering and the environment inherited by clients launched after EWM applies the setting. They are useful for runtime changes, but they are not a replacement for the session environment because they happen after Emacs has already started.
Unfocused Window Alpha
Dim non-focused application toplevels so the active window stands out.
;; Render unfocused windows at 70% opacity. (setopt ewm-unfocused-alpha 0.7)
A float in 0.0..=1.0; 1.0 (default) disables the effect. The alpha is per layout entry, so when a surface is mirrored across multiple Emacs windows only the active mirror stays opaque.
Emacs frames, layer surfaces, and fullscreen toplevels are never dimmed.
Workspace Transition Animations
EWM plays a horizontal slide animation when switching workspace tabs. Enabled by default.
;; Disable slide animations entirely. (setopt ewm-animations-enabled nil)