cashmere

cashmere

Emacs Window Manager

Setting up EWM

If you have a flake.nix based setup, good for you!

If not, RTFM.

My personal setup

Nix

I also use the emacs-overlay. However, instead of linking my Emacs configuration from Nix as in the example, I still load it at runtime.

programs.ewm = {
  enable = true;
  emacsPackage = pkgs.emacs-git-pgtk.pkgs.withPackages (epkgs: [
    epkgs.mu4e
    epkgs.offlineimap
    epkgs.pdf-tools
    epkgs.nushell-ts-mode
    config.programs.ewm.ewmPackage
    (epkgs.treesit-grammars.with-grammars (grammars: [
      grammars.tree-sitter-nu
    ]))
  ]);
};

Emacs configuration

EWM comes with the default tiling window manager keybinds out of the box, which you can of course override. I was able to move around the workspaces like in any other WM. However, this will not be your typical EXWM experience as far as I can remember.

I suggest using my EWM setup as a reference. It's also very advisable to have a good amount of packages, as they may interfere with your startup. In my case, emacs-dashboard was interfering with startup, and it took me 2-3 days to get behind that issue. Of course, my fix is included. And being good FOSS citizens, I will report this bug upstream.

(with-eval-after-load 'ewm
  (require 'ewm-transient)

  ;; ── Bugfix: guard against nil output in pointer functions ──
  ;; ewm-input--pointer-in-window-p crashes with "stringp, nil" when
  ;; called on a frame without ewm-output (e.g. the initial daemon frame
  ;; during startup when dashboard does switch-to-buffer).
  (defun ewm-input--pointer-in-window-p (window)
    "Return non-nil if pointer is inside WINDOW. Safe for non-EWM frames."
    (let* ((frame (window-frame window))
           (output (frame-parameter frame 'ewm-output)))
      (when output
        (let* ((output-offset (ewm--get-output-offset output))
               (edges (window-inside-pixel-edges window))
               (left (+ (car output-offset) (nth 0 edges)))
               (top (+ (cdr output-offset) (nth 1 edges)))
               (right (+ (car output-offset) (nth 2 edges)))
               (bottom (+ (cdr output-offset) (nth 3 edges)))
               (pointer (ewm-get-pointer-location))
               (px (car pointer))
               (py (cdr pointer)))
          (and (<= left px right)
               (<= top py bottom))))))

  (defun ewm-input--warp-pointer-to-window (window)
    "Warp pointer to center of WINDOW. Safe for non-EWM frames."
    (unless (minibufferp (window-buffer window))
      (let* ((frame (window-frame window))
             (output (frame-parameter frame 'ewm-output)))
        (when (and output (not (ewm-input--pointer-in-window-p window)))
          (let* ((output-offset (ewm--get-output-offset output))
                 (edges (window-inside-pixel-edges window))
                 (x (+ (car output-offset) (/ (+ (nth 0 edges) (nth 2 edges)) 2)))
                 (y (+ (cdr output-offset) (/ (+ (nth 1 edges) (nth 3 edges)) 2))))
            (ewm-warp-pointer (float x) (float y)))))))

  ;; ── Settings ─────────────────────────────────────────────
  (setq ewm-mouse-follows-focus t)
  (setq ewm-input-config '((keyboard :repeat-delay 200 :repeat-rate 25)))
  (setq ewm-idle '(300 . "swaylock -f -c 333333"))

  ;; Per-host output config
  (pcase (system-name)
    ("md"
     (setq ewm-output-config
           '(("DP-1" :x 0 :y 0 :refresh 144))))
    ("sportmacher"
     (setq ewm-output-config
           '(("DSI-1" :width 800 :height 1280 :transform 3 :scale 1.0)))))

  ;; ── Programm-Launcher ──────────────────────────────────────
  (define-key ewm-mode-map (kbd "s-<return>")
    (lambda () (interactive) (start-process "kitty" nil "kitty")))
  (define-key ewm-mode-map (kbd "M-s-7")
    (lambda (command)
      (interactive (list (read-shell-command "$ ")))
      (start-process-shell-command command nil command)))
  (define-key ewm-mode-map (kbd "s-SPC")
    (lambda () (interactive) (start-process "vicinae" nil "vicinae" "toggle")))
  (define-key ewm-mode-map (kbd "s-b")
    (lambda () (interactive) (start-process "helium" nil "helium-browser")))
  (define-key ewm-mode-map (kbd "s-p")
    (lambda () (interactive) (start-process "vicinae" nil "vicinae" "vicinae://extensions/tinkerbells/pass/pass")))
  (define-key ewm-mode-map (kbd "s-v")
    (lambda () (interactive) (start-process "vicinae" nil "vicinae" "vicinae://extensions/vicinae/clipboard/history")))
  (define-key ewm-mode-map (kbd "s-s")
    (lambda () (interactive) (start-process "vicinae" nil "vicinae" "vicinae://extensions/vicinae/power")))
  (define-key ewm-mode-map (kbd "s-d")
    (lambda () (interactive) (start-process "vicinae" nil "vicinae" "vicinae://extensions/cashmere/deepl-improve/deepl-improve")))
  (define-key ewm-mode-map (kbd "M-s")
    (lambda () (interactive) (start-process "snipe" nil "snipe")))
  (define-key ewm-mode-map (kbd "s-<print>")
    (lambda () (interactive)
      (start-process-shell-command "screenshot" nil "grim -g \"$(slurp)\" - | wl-copy")))
  (define-key ewm-mode-map (kbd "<print>")
    (lambda () (interactive)
      (start-process-shell-command "screenshot-pb" nil
        "grim -g \"$(slurp)\" - | ssh pb@pb.cashmere.rs -- -ext png 2>/dev/null | sed 's/\\x1b\\[[0-9;]*m//g' | grep -oP 'https://\\S+/f/\\S+' | sed 's/$/?r=1/' | wl-copy")))

  ;; ── Window Management ────────────────────────────────────
  (define-key ewm-mode-map (kbd "s-h") #'windmove-left)
  (define-key ewm-mode-map (kbd "s-j") #'windmove-down)
  (define-key ewm-mode-map (kbd "s-k") #'windmove-up)
  (define-key ewm-mode-map (kbd "s-l") #'windmove-right)
  (define-key ewm-mode-map (kbd "s-q") #'kill-current-buffer)
  (define-key ewm-mode-map (kbd "s-f") #'ewm-toggle-fullscreen)
  (define-key ewm-mode-map (kbd "s-S-q") #'ewm-stop-module)

  ;; tab switching s-0..s-9 (s-0 = tab 10)
  (dotimes (i 10)
    (define-key ewm-mode-map
      (kbd (format "s-%d" i))
      (let ((n i))
        (lambda () (interactive) (tab-bar-select-tab (if (= n 0) 10 n))))))

  ;; ── Media Keys ───────────────────────────────────────────
  (define-key ewm-mode-map (kbd "<XF86AudioLowerVolume>")
    (lambda () (interactive) (start-process "vol-" nil "pamixer" "-d" "5")))
  (define-key ewm-mode-map (kbd "<XF86AudioRaiseVolume>")
    (lambda () (interactive) (start-process "vol+" nil "pamixer" "-i" "5")))
  (define-key ewm-mode-map (kbd "<XF86AudioMute>")
    (lambda () (interactive) (start-process "mute" nil "pamixer" "--toggle-mute")))
  (define-key ewm-mode-map (kbd "<XF86AudioMicMute>")
    (lambda () (interactive)
      (start-process-shell-command "micmute" nil
        "pactl list sources short | grep input | awk '{print $1}' | xargs -I{} pactl set-source-mute {} toggle")))
  (define-key ewm-mode-map (kbd "<XF86AudioPlay>")
    (lambda () (interactive) (start-process "play" nil "playerctl" "play-pause")))
  (define-key ewm-mode-map (kbd "<XF86AudioMedia>")
    (lambda () (interactive) (start-process "play" nil "playerctl" "play-pause")))
  (define-key ewm-mode-map (kbd "<XF86AudioNext>")
    (lambda () (interactive) (start-process "next" nil "playerctl" "next")))
  (define-key ewm-mode-map (kbd "<XF86AudioPrev>")
    (lambda () (interactive) (start-process "prev" nil "playerctl" "previous")))
  (define-key ewm-mode-map (kbd "<XF86MonBrightnessUp>")
    (lambda () (interactive) (start-process "bri+" nil "brightnessctl" "set" "+5%")))
  (define-key ewm-mode-map (kbd "<XF86MonBrightnessDown>")
    (lambda () (interactive) (start-process "bri-" nil "brightnessctl" "set" "5%-")))

  ;; ── Leader bindings under SPC e ──────────────────────────
  (my-leader
    "e"   '(:ignore t :wk "ewm")
    "e a" '(ewm-launch-app :wk "launch app")
    "e n" '(ewm-next-surface-buffer :wk "next surface")
    "e p" '(ewm-prev-surface-buffer :wk "prev surface")
    "e f" '(ewm-toggle-fullscreen :wk "fullscreen")
    "e t" '(ewm-transient :wk "control panel")
    "e s" '(ewm-screenshot :wk "screenshot")
    "e i" '(ewm-show-state :wk "inspect state")
    "e d" '(ewm-debug-mode :wk "debug")
    "e l" '(ewm-lock-session :wk "lock")
    "e T" '(:ignore t :wk "tab")
    "e T n" '(tab-new :wk "new tab")
    "e T c" '(tab-close :wk "close tab")
    "e T l" '(tab-next :wk "next tab")
    "e T h" '(tab-previous :wk "prev tab")
    "e T r" '(tab-recent :wk "recent tab")))

Review

You run programs inside Emacs frames. You live inside Emacs. Emacs consumes your life. While I didn't enjoy it that much on my main machine, I see a lot of value for my 7-inch laptop with a fake Thinkpad trackpoint:

img

img