Jump to content

EWM

From ArchWiki

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.

Warning Custom modes are not guaranteed to work: the compositor asks the display to run a mode its manufacturer did not advertise, and the kernel may reject it.

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)

See also