;;; exwm.el --- Emacs X Window Manager -*- lexical-binding: t -*- ;; Copyright (C) 2015-2024 Free Software Foundation, Inc. ;; Author: Chris Feng ;; Maintainer: Adrián Medraño Calvo , Steven Allen , Daniel Mendler ;; Version: 0.30 ;; Package-Requires: ((emacs "27.1") (xelb "0.19") (compat "29.1")) ;; Keywords: unix ;; URL: https://github.com/emacs-exwm/exwm ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;;; Commentary: ;; Overview ;; -------- ;; EXWM (Emacs X Window Manager) is a full-featured tiling X window manager ;; for Emacs built on top of [XELB](https://github.com/emacs-exwm/xelb). ;; It features: ;; + Fully keyboard-driven operations ;; + Hybrid layout modes (tiling & stacking) ;; + Dynamic workspace support ;; + ICCCM/EWMH compliance ;; Optional features: ;; + RandR (multi-monitor) support ;; + System tray ;; + Input method ;; + Background setting support ;; + XSETTINGS server ;; Installation & configuration ;; ---------------------------- ;; Here are the minimal steps to get EXWM working: ;; 1. Install XELB and EXWM, and make sure they are in `load-path'. ;; 2. In '~/.emacs', add following lines (please modify accordingly): ;; ;; (require 'exwm) ;; (setq exwm-input-global-keys `(([?\s-r] . exwm-reset))) ;; (exwm-enable) ;; ;; 3. Link or copy the file 'xinitrc' to '~/.xinitrc'. ;; 4. Launch EXWM in a console (e.g. tty1) with ;; ;; xinit -- vt01 ;; ;; You should additionally hide the menu-bar, tool-bar, etc to increase the ;; usable space. Please check the wiki (https://github.com/emacs-exwm/exwm/wiki) ;; for more detailed instructions on installation, configuration, usage, etc. ;; References: ;; + dwm (http://dwm.suckless.org/) ;; + i3 wm (https://i3wm.org/) ;; + Also see references within each required library. ;;; Code: (require 'server) (require 'exwm-core) (require 'exwm-workspace) (require 'exwm-layout) (require 'exwm-floating) (require 'exwm-manage) (require 'exwm-input) (declare-function x-get-atom-name "C source code" (VALUE &optional FRAME)) (defgroup exwm nil "Emacs X Window Manager." :tag "EXWM" :group 'applications :prefix "exwm-") (defcustom exwm-init-hook nil "Normal hook run when EXWM has just finished initialization." :type 'hook) (defcustom exwm-exit-hook nil "Normal hook run just before EXWM exits." :type 'hook) (defcustom exwm-update-class-hook nil "Normal hook run when window class is updated." :type 'hook) (defcustom exwm-update-title-hook nil "Normal hook run when window title is updated." :type 'hook) (defcustom exwm-blocking-subrs ;; `x-file-dialog' and `x-select-font' are missing on some Emacs builds, for ;; example on the X11 Lucid build. '(x-file-dialog x-popup-dialog x-select-font message-box message-or-box) "Subrs (primitives) that would normally block EXWM." :type '(repeat function)) (defcustom exwm-replace 'ask "Whether to replace existing window manager." :type '(radio (const :tag "Ask" ask) (const :tag "Replace by default" t) (const :tag "Do not replace" nil))) (defconst exwm--server-name "server-exwm" "Name of the subordinate Emacs server.") (defvar exwm--server-timeout 1 "Number of seconds to wait for the subordinate Emacs server to exit. After this time, the server will be killed.") (defvar exwm--server-process nil "Process of the subordinate Emacs server.") (defvar exwm--client-message-functions nil "Alist of form ((MESSAGE . MESSAGE-HANDLER)...). Set during `exwm-init'.") (defun exwm-reset () "Reset the state of the selected window (non-fullscreen, line-mode, etc)." (interactive) (exwm--log) (with-current-buffer (window-buffer) (when (derived-mode-p 'exwm-mode) (when (exwm-layout--fullscreen-p) (exwm-layout-unset-fullscreen)) ;; Force refresh (exwm-layout--refresh) (call-interactively #'exwm-input-grab-keyboard)))) ;;;###autoload (defun exwm-restart () "Restart EXWM." (interactive) (exwm--log) (when (exwm--confirm-kill-emacs "Restart" 'no-check) (let* ((attr (process-attributes (emacs-pid))) (args (cdr (assq 'args attr))) (ppid (cdr (assq 'ppid attr))) (pargs (cdr (assq 'args (process-attributes ppid))))) (cond ((= ppid 1) ;; The parent is the init process. This probably means this ;; instance is an emacsclient. Anyway, start a control instance ;; to manage the subsequent ones. (call-process (car command-line-args)) (kill-emacs)) ((string= args pargs) ;; This is a subordinate instance. Return a magic number to ;; inform the parent (control instance) to start another one. (kill-emacs ?R)) (t ;; This is the control instance. Keep starting subordinate ;; instances until told to exit. ;; Run `server-force-stop' if it exists. (run-hooks 'kill-emacs-hook) (with-temp-buffer (while (= ?R (shell-command-on-region (point) (point) args)))) (kill-emacs)))))) (defun exwm--update-desktop (xwin) "Update _NET_WM_DESKTOP. Argument XWIN contains the X window of the `exwm-mode' buffer." (exwm--log "#x%x" xwin) (with-current-buffer (exwm--id->buffer xwin) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:ewmh:get-_NET_WM_DESKTOP :window xwin))) desktop) (when reply (setq desktop (slot-value reply 'value)) (cond ((and desktop (= desktop 4294967295.)) (unless (or (not exwm--floating-frame) (eq exwm--frame exwm-workspace--current) (and exwm--desktop (= desktop exwm--desktop))) (exwm-layout--show xwin (frame-root-window exwm--floating-frame))) (setq exwm--desktop desktop)) ((and desktop (< desktop (exwm-workspace--count)) (if exwm--desktop (/= desktop exwm--desktop) (/= desktop (exwm-workspace--position exwm--frame)))) (exwm-workspace-move-window desktop xwin)) (t (exwm-workspace--set-desktop xwin))))))) (defun exwm--update-window-type (id &optional force) "Update `exwm-window-type' from _NET_WM_WINDOW_TYPE. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place if `exwm-window-type' is unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (unless (and exwm-window-type (not force)) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:ewmh:get-_NET_WM_WINDOW_TYPE :window id)))) (when reply ;nil when destroyed (setq exwm-window-type (append (slot-value reply 'value) nil))))))) (defun exwm--update-class (id &optional force) "Update `exwm-instance-name' and `exwm-class' from WM_CLASS. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place if any of `exwm-instance-name' or `exwm-class' is unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (unless (and exwm-instance-name exwm-class-name (not force)) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:icccm:get-WM_CLASS :window id)))) (when reply ;nil when destroyed (setq exwm-instance-name (slot-value reply 'instance-name) exwm-class-name (slot-value reply 'class-name)) (when (and exwm-instance-name exwm-class-name) (run-hooks 'exwm-update-class-hook))))))) (defun exwm--update-utf8-title (id &optional force) "Update `exwm-title' from _NET_WM_NAME. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place if `exwm-title' is unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (when (or force (not exwm-title)) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:ewmh:get-_NET_WM_NAME :window id)))) (when reply ;nil when destroyed (setq exwm-title (slot-value reply 'value)) (when exwm-title (setq exwm--title-is-utf8 t) (run-hooks 'exwm-update-title-hook))))))) (defun exwm--update-ctext-title (id &optional force) "Update `exwm-title' from WM_NAME. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place if `exwm-title' is unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (unless (or exwm--title-is-utf8 (and exwm-title (not force))) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:icccm:get-WM_NAME :window id)))) (when reply ;nil when destroyed (setq exwm-title (slot-value reply 'value)) (when exwm-title (run-hooks 'exwm-update-title-hook))))))) (defun exwm--update-title (id) "Update _NET_WM_NAME or WM_NAME. Argument ID contains the X window of the `exwm-mode' buffer." (exwm--log "#x%x" id) (exwm--update-utf8-title id) (exwm--update-ctext-title id)) (defun exwm--update-transient-for (id &optional force) "Update `exwm-transient-for' from WM_TRANSIENT_FOR. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place if `exwm-title' is unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (unless (and exwm-transient-for (not force)) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:icccm:get-WM_TRANSIENT_FOR :window id)))) (when reply ;nil when destroyed (setq exwm-transient-for (slot-value reply 'value))))))) (defun exwm--update-normal-hints (id &optional force) "Update normal hints from WM_NORMAL_HINTS. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place all of `exwm--normal-hints-x exwm--normal-hints-y', `exwm--normal-hints-width exwm--normal-hints-height', `exwm--normal-hints-min-width exwm--normal-hints-min-height' and `exwm--normal-hints-max-width exwm--normal-hints-max-height' are unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (unless (and (not force) (or exwm--normal-hints-x exwm--normal-hints-y exwm--normal-hints-width exwm--normal-hints-height exwm--normal-hints-min-width exwm--normal-hints-min-height exwm--normal-hints-max-width exwm--normal-hints-max-height ;; FIXME: other fields )) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:icccm:get-WM_NORMAL_HINTS :window id)))) (when (and reply (slot-value reply 'flags)) ;nil when destroyed (with-slots (flags x y width height min-width min-height max-width max-height base-width base-height ;; win-gravity ) reply (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:USPosition)) (setq exwm--normal-hints-x x exwm--normal-hints-y y)) (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:USSize)) (setq exwm--normal-hints-width width exwm--normal-hints-height height)) (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PMinSize)) (setq exwm--normal-hints-min-width min-width exwm--normal-hints-min-height min-height)) (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PMaxSize)) (setq exwm--normal-hints-max-width max-width exwm--normal-hints-max-height max-height)) (unless (or exwm--normal-hints-min-width (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PBaseSize))) (setq exwm--normal-hints-min-width base-width exwm--normal-hints-min-height base-height)) ;; (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PWinGravity)) ;; (setq exwm--normal-hints-win-gravity win-gravity)) (setq exwm--fixed-size (and exwm--normal-hints-min-width exwm--normal-hints-min-height exwm--normal-hints-max-width exwm--normal-hints-max-height (/= 0 exwm--normal-hints-min-width) (/= 0 exwm--normal-hints-min-height) (= exwm--normal-hints-min-width exwm--normal-hints-max-width) (= exwm--normal-hints-min-height exwm--normal-hints-max-height))))))))) (defun exwm--update-hints (id &optional force) "Update hints from WM_HINTS. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place if both of `exwm--hints-input' and `exwm--hints-urgency' are unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (unless (and (not force) exwm--hints-input exwm--hints-urgency) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:icccm:get-WM_HINTS :window id)))) (when (and reply (slot-value reply 'flags)) ;nil when destroyed (with-slots (flags input initial-state) reply (when flags (unless (= 0 (logand flags xcb:icccm:WM_HINTS:InputHint)) (setq exwm--hints-input (when input (= 1 input)))) (unless (= 0 (logand flags xcb:icccm:WM_HINTS:StateHint)) (setq exwm-state initial-state)) (unless (= 0 (logand flags xcb:icccm:WM_HINTS:UrgencyHint)) (setq exwm--hints-urgency t)))) (when (and exwm--hints-urgency (not (eq exwm--frame exwm-workspace--current))) (unless (frame-parameter exwm--frame 'exwm-urgency) (set-frame-parameter exwm--frame 'exwm-urgency t) (setq exwm-workspace--switch-history-outdated t)))))))) (defun exwm--update-protocols (id &optional force) "Update `exwm--protocols' from WM_PROTOCOLS. Argument ID contains the X window of the `exwm-mode' buffer. When FORCE is nil the update only takes place if `exwm--protocols' is unset." (exwm--log "#x%x" id) (with-current-buffer (exwm--id->buffer id) (unless (and exwm--protocols (not force)) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:icccm:get-WM_PROTOCOLS :window id)))) (when reply ;nil when destroyed (setq exwm--protocols (append (slot-value reply 'value) nil))))))) (defun exwm--update-struts-legacy (id) "Update struts of X window ID from _NET_WM_STRUT." (exwm--log "#x%x" id) (let ((pair (assq id exwm-workspace--id-struts-alist)) reply struts) (unless (and pair (< 4 (length (cdr pair)))) (setq reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:ewmh:get-_NET_WM_STRUT :window id))) (when reply (setq struts (slot-value reply 'value)) (if pair (setcdr pair struts) (push (cons id struts) exwm-workspace--id-struts-alist)) (exwm-workspace--update-struts)) ;; Update workareas. (exwm-workspace--update-workareas) ;; Update workspaces. (dolist (f exwm-workspace--list) (exwm-workspace--set-fullscreen f))))) (defun exwm--update-struts-partial (id) "Update struts of X window ID from _NET_WM_STRUT_PARTIAL." (exwm--log "#x%x" id) (let ((reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:ewmh:get-_NET_WM_STRUT_PARTIAL :window id))) struts pair) (when reply (setq struts (slot-value reply 'value) pair (assq id exwm-workspace--id-struts-alist)) (if pair (setcdr pair struts) (push (cons id struts) exwm-workspace--id-struts-alist)) (exwm-workspace--update-struts)) ;; Update workareas. (exwm-workspace--update-workareas) ;; Update workspaces. (dolist (f exwm-workspace--list) (exwm-workspace--set-fullscreen f)))) (defun exwm--update-struts (id) "Update struts of X window ID from _NET_WM_STRUT_PARTIAL or _NET_WM_STRUT." (exwm--log "#x%x" id) (exwm--update-struts-partial id) (exwm--update-struts-legacy id)) (defun exwm--on-PropertyNotify (data _synthetic) "Handle PropertyNotify event. DATA contains unmarshalled PropertyNotify event data." (let ((obj (make-instance 'xcb:PropertyNotify)) atom id buffer) (xcb:unmarshal obj data) (setq id (slot-value obj 'window) atom (slot-value obj 'atom)) (exwm--log "atom=%s(%s)" (x-get-atom-name atom exwm-workspace--current) atom) (setq buffer (exwm--id->buffer id)) (if (not (buffer-live-p buffer)) ;; Properties of unmanaged X windows. (cond ((= atom xcb:Atom:_NET_WM_STRUT) (exwm--update-struts-legacy id)) ((= atom xcb:Atom:_NET_WM_STRUT_PARTIAL) (exwm--update-struts-partial id))) (with-current-buffer buffer (cond ((= atom xcb:Atom:_NET_WM_WINDOW_TYPE) (exwm--update-window-type id t)) ((= atom xcb:Atom:WM_CLASS) (exwm--update-class id t)) ((= atom xcb:Atom:_NET_WM_NAME) (exwm--update-utf8-title id t)) ((= atom xcb:Atom:WM_NAME) (exwm--update-ctext-title id t)) ((= atom xcb:Atom:WM_TRANSIENT_FOR) (exwm--update-transient-for id t)) ((= atom xcb:Atom:WM_NORMAL_HINTS) (exwm--update-normal-hints id t)) ((= atom xcb:Atom:WM_HINTS) (exwm--update-hints id t)) ((= atom xcb:Atom:WM_PROTOCOLS) (exwm--update-protocols id t)) ((= atom xcb:Atom:_NET_WM_USER_TIME)) ;ignored (t (exwm--log "Unhandled: %s(%d)" (x-get-atom-name atom exwm-workspace--current) atom))))))) (defun exwm--on-net-number-of-desktops (_id data) "Handle _NET_NUMBER_OF_DESKTOPS_ message with DATA." (let ((current (exwm-workspace--count)) (requested (elt data 0))) ;; Only allow increasing/decreasing the workspace number by 1. (cond ((< current requested) (make-frame)) ((and (> current requested) (> current 1)) (let ((frame (car (last exwm-workspace--list)))) (delete-frame frame)))))) (defun exwm--on-net-current-desktop (_id data) "Handle _NET_CURRENT_DESKTOP message with DATA." (exwm-workspace-switch (elt data 0))) (defun exwm--on-net-active-window (id _data) "Handle _NET_ACTIVE_WINDOW message with ID." (let ((buffer (exwm--id->buffer id)) window) (if (buffer-live-p buffer) ;; Either an `exwm-mode' buffer (an X window) or a floating frame. (with-current-buffer buffer (when (eq exwm--frame exwm-workspace--current) (if exwm--floating-frame (select-frame exwm--floating-frame) (setq window (get-buffer-window nil t)) (unless window ;; State change: iconic => normal. (setq window (frame-selected-window exwm--frame)) (set-window-buffer window (current-buffer))) ;; Focus transfer. (select-window window)))) ;; A workspace. (dolist (f exwm-workspace--list) (when (eq id (frame-parameter f 'exwm-outer-id)) (x-focus-frame f t)))))) (defun exwm--on-net-close-window (id _data) "Handle _NET_CLOSE_WINDOW message with ID." (let ((buffer (exwm--id->buffer id))) (when (buffer-live-p buffer) (exwm--defer 0 #'kill-buffer buffer)))) (defun exwm--on-net-wm-moveresize (id data) "Handle _NET_WM_MOVERESIZE message with ID and DATA." (let ((direction (elt data 2)) (buffer (exwm--id->buffer id))) (unless (and buffer (not (buffer-local-value 'exwm--floating-frame buffer))) (cond ((= direction xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_KEYBOARD) ;; FIXME ) ((= direction xcb:ewmh:_NET_WM_MOVERESIZE_MOVE_KEYBOARD) ;; FIXME ) ((= direction xcb:ewmh:_NET_WM_MOVERESIZE_CANCEL) (exwm-floating--stop-moveresize)) ;; In case it's a workspace frame. ((and (not buffer) (catch 'break (dolist (f exwm-workspace--list) (when (or (eq id (frame-parameter f 'exwm-outer-id)) (eq id (frame-parameter f 'exwm-id))) (throw 'break t))) nil))) (t ;; In case it's a floating frame, ;; move the corresponding X window instead. (unless buffer (catch 'break (dolist (pair exwm--id-buffer-alist) (with-current-buffer (cdr pair) (when (and exwm--floating-frame (or (eq id (frame-parameter exwm--floating-frame 'exwm-outer-id)) (eq id (frame-parameter exwm--floating-frame 'exwm-id)))) (setq id exwm--id) (throw 'break nil)))))) ;; Start to move it. (exwm-floating--start-moveresize id direction)))))) (defun exwm--on-net-request-frame-extents (id _data) "Handle _NET_REQUEST_FRAME_EXTENTS message with ID." (let ((buffer (exwm--id->buffer id)) top btm) (if (or (not buffer) (not (buffer-local-value 'exwm--floating-frame buffer))) (setq top 0 btm 0) (setq top (window-header-line-height) btm (window-mode-line-height))) (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_FRAME_EXTENTS :window id :left 0 :right 0 :top top :bottom btm))) (xcb:flush exwm--connection)) (defun exwm--on-net-wm-desktop (id data) "Handle _NET_WM_DESKTOP message with ID and DATA." (let ((buffer (exwm--id->buffer id))) (when (buffer-live-p buffer) (exwm-workspace-move-window (elt data 0) id)))) (defun exwm--on-net-wm-state (id data) "Handle _NET_WM_STATE message with ID and DATA." (let ((action (elt data 0)) (props (list (elt data 1) (elt data 2))) (buffer (exwm--id->buffer id)) props-new) ;; only support _NET_WM_STATE_FULLSCREEN / _NET_WM_STATE_ADD for frames (when (and (not buffer) (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props) (= action xcb:ewmh:_NET_WM_STATE_ADD)) (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_WM_STATE :window id :data (vector xcb:Atom:_NET_WM_STATE_FULLSCREEN))) (xcb:flush exwm--connection)) (when buffer ;ensure it's managed (with-current-buffer buffer ;; _NET_WM_STATE_FULLSCREEN (when (or (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props) (memq xcb:Atom:_NET_WM_STATE_ABOVE props)) (cond ((= action xcb:ewmh:_NET_WM_STATE_ADD) (unless (exwm-layout--fullscreen-p) (exwm-layout-set-fullscreen id)) (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new)) ((= action xcb:ewmh:_NET_WM_STATE_REMOVE) (when (exwm-layout--fullscreen-p) (exwm-layout-unset-fullscreen id))) ((= action xcb:ewmh:_NET_WM_STATE_TOGGLE) (if (exwm-layout--fullscreen-p) (exwm-layout-unset-fullscreen id) (exwm-layout-set-fullscreen id) (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new))))) ;; _NET_WM_STATE_DEMANDS_ATTENTION ;; FIXME: check (may require other properties set) (when (memq xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION props) (when (= action xcb:ewmh:_NET_WM_STATE_ADD) (unless (eq exwm--frame exwm-workspace--current) (set-frame-parameter exwm--frame 'exwm-urgency t) (setq exwm-workspace--switch-history-outdated t))) ;; xcb:ewmh:_NET_WM_STATE_REMOVE? ;; xcb:ewmh:_NET_WM_STATE_TOGGLE? ) (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_WM_STATE :window id :data (vconcat props-new))) (xcb:flush exwm--connection))))) (defun exwm--on-wm-protocols (_id data) "Handle WM_PROTOCOLS message with DATA." (let ((type (elt data 0))) (cond ((= type xcb:Atom:_NET_WM_PING) (setq exwm-manage--ping-lock nil)) (t (exwm--log "Unhandled WM_PROTOCOLS of type: %d" type))))) (defun exwm--on-wm-change-state (id data) "Handle WM_CHANGE_STATE message with ID and DATA." (let ((buffer (exwm--id->buffer id))) (when (and (buffer-live-p buffer) (= (elt data 0) xcb:icccm:WM_STATE:IconicState)) (with-current-buffer buffer (if exwm--floating-frame (call-interactively #'exwm-floating-hide) (bury-buffer)))))) (defun exwm--on-ClientMessage (raw-data _synthetic) "Handle ClientMessage event. RAW-DATA contains unmarshalled ClientMessage event data." (let* ((obj (let ((m (make-instance 'xcb:ClientMessage))) (xcb:unmarshal m raw-data) m)) (type (slot-value obj 'type)) (id (slot-value obj 'window)) (data (slot-value (slot-value obj 'data) 'data32)) (fn (alist-get type exwm--client-message-functions))) (if (not fn) (exwm--log "Unhandled: %s(%d)" (x-get-atom-name type exwm-workspace--current) type) (exwm--log "atom=%s(%s) id=#x%x data=%s" (x-get-atom-name type exwm-workspace--current) type (or id 0) data) (funcall fn id data)))) (defun exwm--on-SelectionClear (data _synthetic) "Handle SelectionClear events. DATA contains unmarshalled SelectionClear event data." (exwm--log) (let ((obj (make-instance 'xcb:SelectionClear)) owner selection) (xcb:unmarshal obj data) (setq owner (slot-value obj 'owner) selection (slot-value obj 'selection)) (when (and (eq owner exwm--wmsn-window) (eq selection xcb:Atom:WM_S0)) (exwm-exit)))) (defun exwm--on-delete-terminal (terminal) "Handle terminal being deleted without Emacs being killed. This function is Hooked to `delete-terminal-functions'. TERMINAL is the terminal being (or that has been) deleted. This may happen when invoking `save-buffers-kill-terminal' within an emacsclient session." (when (eq terminal exwm--terminal) (exwm-exit))) (defun exwm--init-icccm-ewmh () "Initialize ICCCM/EWMH support." (exwm--log) ;; Handle PropertyNotify event (xcb:+event exwm--connection 'xcb:PropertyNotify #'exwm--on-PropertyNotify) ;; Handle relevant client messages (xcb:+event exwm--connection 'xcb:ClientMessage #'exwm--on-ClientMessage) ;; Handle SelectionClear (xcb:+event exwm--connection 'xcb:SelectionClear #'exwm--on-SelectionClear) ;; Set _NET_SUPPORTED (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_SUPPORTED :window exwm--root :data (vector ;; Root windows properties. xcb:Atom:_NET_SUPPORTED xcb:Atom:_NET_CLIENT_LIST xcb:Atom:_NET_CLIENT_LIST_STACKING xcb:Atom:_NET_NUMBER_OF_DESKTOPS xcb:Atom:_NET_DESKTOP_GEOMETRY xcb:Atom:_NET_DESKTOP_VIEWPORT xcb:Atom:_NET_CURRENT_DESKTOP ;; xcb:Atom:_NET_DESKTOP_NAMES xcb:Atom:_NET_ACTIVE_WINDOW ;; xcb:Atom:_NET_WORKAREA xcb:Atom:_NET_SUPPORTING_WM_CHECK ;; xcb:Atom:_NET_VIRTUAL_ROOTS ;; xcb:Atom:_NET_DESKTOP_LAYOUT ;; xcb:Atom:_NET_SHOWING_DESKTOP ;; Other root window messages. xcb:Atom:_NET_CLOSE_WINDOW ;; xcb:Atom:_NET_MOVERESIZE_WINDOW xcb:Atom:_NET_WM_MOVERESIZE ;; xcb:Atom:_NET_RESTACK_WINDOW xcb:Atom:_NET_REQUEST_FRAME_EXTENTS ;; Application window properties. xcb:Atom:_NET_WM_NAME ;; xcb:Atom:_NET_WM_VISIBLE_NAME ;; xcb:Atom:_NET_WM_ICON_NAME ;; xcb:Atom:_NET_WM_VISIBLE_ICON_NAME xcb:Atom:_NET_WM_DESKTOP ;; xcb:Atom:_NET_WM_WINDOW_TYPE ;; xcb:Atom:_NET_WM_WINDOW_TYPE_DESKTOP xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK xcb:Atom:_NET_WM_WINDOW_TYPE_TOOLBAR xcb:Atom:_NET_WM_WINDOW_TYPE_MENU xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY xcb:Atom:_NET_WM_WINDOW_TYPE_SPLASH xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG xcb:Atom:_NET_WM_WINDOW_TYPE_DROPDOWN_MENU xcb:Atom:_NET_WM_WINDOW_TYPE_POPUP_MENU xcb:Atom:_NET_WM_WINDOW_TYPE_TOOLTIP xcb:Atom:_NET_WM_WINDOW_TYPE_NOTIFICATION xcb:Atom:_NET_WM_WINDOW_TYPE_COMBO xcb:Atom:_NET_WM_WINDOW_TYPE_DND xcb:Atom:_NET_WM_WINDOW_TYPE_NORMAL ;; xcb:Atom:_NET_WM_STATE ;; xcb:Atom:_NET_WM_STATE_MODAL ;; xcb:Atom:_NET_WM_STATE_STICKY ;; xcb:Atom:_NET_WM_STATE_MAXIMIZED_VERT ;; xcb:Atom:_NET_WM_STATE_MAXIMIZED_HORZ ;; xcb:Atom:_NET_WM_STATE_SHADED ;; xcb:Atom:_NET_WM_STATE_SKIP_TASKBAR ;; xcb:Atom:_NET_WM_STATE_SKIP_PAGER xcb:Atom:_NET_WM_STATE_HIDDEN xcb:Atom:_NET_WM_STATE_FULLSCREEN ;; xcb:Atom:_NET_WM_STATE_ABOVE ;; xcb:Atom:_NET_WM_STATE_BELOW xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION ;; xcb:Atom:_NET_WM_STATE_FOCUSED ;; xcb:Atom:_NET_WM_ALLOWED_ACTIONS xcb:Atom:_NET_WM_ACTION_MOVE xcb:Atom:_NET_WM_ACTION_RESIZE xcb:Atom:_NET_WM_ACTION_MINIMIZE ;; xcb:Atom:_NET_WM_ACTION_SHADE ;; xcb:Atom:_NET_WM_ACTION_STICK ;; xcb:Atom:_NET_WM_ACTION_MAXIMIZE_HORZ ;; xcb:Atom:_NET_WM_ACTION_MAXIMIZE_VERT xcb:Atom:_NET_WM_ACTION_FULLSCREEN xcb:Atom:_NET_WM_ACTION_CHANGE_DESKTOP xcb:Atom:_NET_WM_ACTION_CLOSE ;; xcb:Atom:_NET_WM_ACTION_ABOVE ;; xcb:Atom:_NET_WM_ACTION_BELOW ;; xcb:Atom:_NET_WM_STRUT xcb:Atom:_NET_WM_STRUT_PARTIAL ;; xcb:Atom:_NET_WM_ICON_GEOMETRY ;; xcb:Atom:_NET_WM_ICON xcb:Atom:_NET_WM_PID ;; xcb:Atom:_NET_WM_HANDLED_ICONS ;; xcb:Atom:_NET_WM_USER_TIME ;; xcb:Atom:_NET_WM_USER_TIME_WINDOW xcb:Atom:_NET_FRAME_EXTENTS ;; xcb:Atom:_NET_WM_OPAQUE_REGION ;; xcb:Atom:_NET_WM_BYPASS_COMPOSITOR ;; Window manager protocols. xcb:Atom:_NET_WM_PING ;; xcb:Atom:_NET_WM_SYNC_REQUEST ;; xcb:Atom:_NET_WM_FULLSCREEN_MONITORS ;; Other properties. xcb:Atom:_NET_WM_FULL_PLACEMENT))) ;; Create a child window for setting _NET_SUPPORTING_WM_CHECK (let ((new-id (xcb:generate-id exwm--connection))) (setq exwm--guide-window new-id) (xcb:+request exwm--connection (make-instance 'xcb:CreateWindow :depth 0 :wid new-id :parent exwm--root :x -1 :y -1 :width 1 :height 1 :border-width 0 :class xcb:WindowClass:InputOnly :visual 0 :value-mask xcb:CW:OverrideRedirect :override-redirect 1)) ;; Set _NET_WM_NAME. Must be set to the name of the window manager, as ;; required by wm-spec. (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_WM_NAME :window new-id :data "EXWM")) (dolist (i (list exwm--root new-id)) ;; Set _NET_SUPPORTING_WM_CHECK (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_SUPPORTING_WM_CHECK :window i :data new-id)))) ;; Set _NET_DESKTOP_VIEWPORT (we don't support large desktop). (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_DESKTOP_VIEWPORT :window exwm--root :data [0 0])) (xcb:flush exwm--connection)) (defun exwm--wmsn-acquire (replace) "Acquire the WM_Sn selection. REPLACE specifies what to do in case there already is a window manager. If t, replace it, if nil, abort and ask the user if `ask'." (exwm--log "%s" replace) (with-slots (owner) (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:GetSelectionOwner :selection xcb:Atom:WM_S0)) (when (/= owner xcb:Window:None) (when (eq replace 'ask) (setq replace (yes-or-no-p "Replace existing window manager? "))) (when (not replace) (user-error "Other window manager detected"))) (let ((new-owner (xcb:generate-id exwm--connection))) (xcb:+request exwm--connection (make-instance 'xcb:CreateWindow :depth 0 :wid new-owner :parent exwm--root :x -1 :y -1 :width 1 :height 1 :border-width 0 :class xcb:WindowClass:CopyFromParent :visual 0 :value-mask 0 :override-redirect 0)) (xcb:+request exwm--connection (make-instance 'xcb:ewmh:set-_NET_WM_NAME :window new-owner :data "EXWM: exwm--wmsn-window")) (xcb:+request-checked+request-check exwm--connection (make-instance 'xcb:SetSelectionOwner :selection xcb:Atom:WM_S0 :owner new-owner :time xcb:Time:CurrentTime)) (with-slots (owner) (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:GetSelectionOwner :selection xcb:Atom:WM_S0)) (unless (eq owner new-owner) (error "Could not acquire ownership of WM selection"))) ;; Wait for the other window manager to terminate. (when (/= owner xcb:Window:None) (let (reply) (cl-dotimes (i exwm--wmsn-acquire-timeout) (setq reply (xcb:+request-unchecked+reply exwm--connection (make-instance 'xcb:GetGeometry :drawable owner))) (when (not reply) (cl-return)) (message "Waiting for other window manager to quit... %ds" i) (sleep-for 1)) (when reply (error "Other window manager did not release selection in time")))) ;; announce (let* ((cmd (make-instance 'xcb:ClientMessageData :data32 (vector xcb:Time:CurrentTime xcb:Atom:WM_S0 new-owner 0 0))) (cm (make-instance 'xcb:ClientMessage :window exwm--root :format 32 :type xcb:Atom:MANAGER :data cmd)) (se (make-instance 'xcb:SendEvent :propagate 0 :destination exwm--root :event-mask xcb:EventMask:NoEvent :event (xcb:marshal cm exwm--connection)))) (xcb:+request exwm--connection se)) (setq exwm--wmsn-window new-owner)))) ;;;###autoload (cl-defun exwm-init (&optional frame) "Initialize EXWM. FRAME, if given, indicates the X display EXWM should manage." (interactive) (exwm--log "%s" frame) (if frame ;; The frame might not be selected if it's created by emacslicnet. (select-frame-set-input-focus frame) (setq frame (selected-frame))) (when (not (eq 'x (framep frame))) (message "[EXWM] Not running under X environment") (cl-return-from exwm-init)) (when exwm--connection (exwm--log "EXWM already running") (cl-return-from exwm-init)) (condition-case err (progn (exwm-enable 'undo) ;never initialize again (setq exwm--terminal (frame-terminal frame)) (setq exwm--connection (xcb:connect)) (set-process-query-on-exit-flag (slot-value exwm--connection 'process) nil) ;prevent query message on exit (setq exwm--root (slot-value (car (slot-value (xcb:get-setup exwm--connection) 'roots)) 'root)) ;; Initialize ICCCM/EWMH support (xcb:icccm:init exwm--connection t) (xcb:ewmh:init exwm--connection t) (setq exwm--client-message-functions (list (cons xcb:Atom:_NET_NUMBER_OF_DESKTOPS #'exwm--on-net-number-of-desktops) (cons xcb:Atom:_NET_CURRENT_DESKTOP #'exwm--on-net-current-desktop) (cons xcb:Atom:_NET_ACTIVE_WINDOW #'exwm--on-net-active-window) (cons xcb:Atom:_NET_CLOSE_WINDOW #'exwm--on-net-close-window) (cons xcb:Atom:_NET_REQUEST_FRAME_EXTENTS #'exwm--on-net-request-frame-extents) (cons xcb:Atom:_NET_WM_DESKTOP #'exwm--on-net-wm-desktop) (cons xcb:Atom:_NET_WM_STATE #'exwm--on-net-wm-state) (cons xcb:Atom:WM_PROTOCOLS #'exwm--on-wm-protocols) (cons xcb:Atom:WM_CHANGE_STATE #'exwm--on-wm-change-state))) ;; Try to register window manager selection. (exwm--wmsn-acquire exwm-replace) (when (xcb:+request-checked+request-check exwm--connection (make-instance 'xcb:ChangeWindowAttributes :window exwm--root :value-mask xcb:CW:EventMask :event-mask xcb:EventMask:SubstructureRedirect)) (error "Other window manager is running")) ;; Disable some features not working well with EXWM (setq use-dialog-box nil confirm-kill-emacs #'exwm--confirm-kill-emacs) (advice-add 'save-buffers-kill-terminal :before-while #'exwm--confirm-kill-terminal) ;; Clean up if the terminal is deleted. (add-hook 'delete-terminal-functions 'exwm--on-delete-terminal) (exwm--lock) (exwm--init-icccm-ewmh) (exwm-layout--init) (exwm-floating--init) (exwm-manage--init) (exwm-workspace--init) (exwm-input--init) (exwm--unlock) (exwm-workspace--post-init) (exwm-input--post-init) (run-hooks 'exwm-init-hook) ;; Manage existing windows (exwm-manage--scan)) (user-error) ((quit error) (exwm-exit) ;; Rethrow error (warn "[EXWM] EXWM fails to start (%s: %s)" (car err) (cdr err))))) ;;;###autoload (defun exwm-exit () "Exit EXWM." (interactive) (exwm--log) (run-hooks 'exwm-exit-hook) (setq confirm-kill-emacs nil) ;; Exit modules. (when exwm--connection (exwm-input--exit) (exwm-manage--exit) (exwm-workspace--exit) (exwm-floating--exit) (exwm-layout--exit) (xcb:flush exwm--connection) (xcb:disconnect exwm--connection)) (setq exwm--connection nil) (setq exwm--terminal nil) (exwm--log "Exited")) ;;;###autoload (defun exwm-enable (&optional undo) "Enable/Disable EXWM. Optional argument UNDO may be either of the following symbols: - `undo' prevents reinitialization. - `undo-all' attempts to revert all hooks and advice." (exwm--log "%s" undo) (pcase undo (`undo ;prevent reinitialization (remove-hook 'window-setup-hook #'exwm-init) (remove-hook 'after-make-frame-functions #'exwm-init)) (`undo-all ;attempt to revert everything (remove-hook 'window-setup-hook #'exwm-init) (remove-hook 'after-make-frame-functions #'exwm-init) (remove-hook 'kill-emacs-hook #'exwm--server-stop) (dolist (i exwm-blocking-subrs) (advice-remove i #'exwm--server-eval-at))) (_ ;enable EXWM (setq frame-resize-pixelwise t ;mandatory; before init window-resize-pixelwise t) ;; Ignore unrecognized command line arguments. This can be helpful ;; when EXWM is launched by some session manager. (push #'vector command-line-functions) ;; In case EXWM is to be started from a graphical Emacs instance. (add-hook 'window-setup-hook #'exwm-init t) ;; In case EXWM is to be started with emacsclient. (add-hook 'after-make-frame-functions #'exwm-init t) ;; Manage the subordinate Emacs server. (add-hook 'kill-emacs-hook #'exwm--server-stop) (dolist (i exwm-blocking-subrs) (advice-add i :around #'exwm--server-eval-at))))) (defun exwm--server-stop () "Stop the subordinate Emacs server." (exwm--log) (when exwm--server-process (when (process-live-p exwm--server-process) (cl-loop initially (signal-process exwm--server-process 'TERM) while (process-live-p exwm--server-process) repeat (* 10 exwm--server-timeout) do (sit-for 0.1))) (delete-process exwm--server-process) (setq exwm--server-process nil))) (defun exwm--server-eval-at (function &rest args) "Wrapper of `server-eval-at' used to advice subrs. FUNCTION is the function to be evaluated, ARGS are the arguments." ;; Start the subordinate Emacs server if it's not alive (exwm--log "%s %s" function args) (unless (server-running-p exwm--server-name) (when exwm--server-process (delete-process exwm--server-process)) (setq exwm--server-process (start-process exwm--server-name nil (car command-line-args) ;The executable file "-d" (frame-parameter nil 'display) "-Q" (concat "--fg-daemon=" exwm--server-name) "--eval" ;; Create an invisible frame "(make-frame '((window-system . x) (visibility)))")) (while (not (server-running-p exwm--server-name)) (sit-for 0.001))) (server-eval-at exwm--server-name `(progn (select-frame (car (frame-list))) (let ((result ,(nconc (list (make-symbol (subr-name function))) args))) (pcase (type-of result) ;; Return the name of a buffer (`buffer (buffer-name result)) ;; We blindly convert all font objects to their XLFD names. This ;; might cause problems of course, but it still has a chance to ;; work (whereas directly passing font objects would merely ;; raise errors). ((or `font-entity `font-object `font-spec) (font-xlfd-name result)) ;; Passing following types makes little sense ((or `compiled-function `finalizer `frame `hash-table `marker `overlay `process `window `window-configuration)) ;; Passing the name of a subr (`subr (make-symbol (subr-name result))) ;; For other types, return the value as-is. (t result)))))) (defun exwm--confirm-kill-terminal (&optional _) "Confirm before killing terminal." ;; This is invoked instead of `save-buffers-kill-emacs' (C-x C-c) on client ;; frames. (if (exwm--terminal-p) (exwm--confirm-kill-emacs "Kill terminal") t)) (defun exwm--confirm-kill-emacs (prompt &optional force) "Confirm before exiting Emacs. PROMPT a reason to present to the user. If FORCE is nil, ask the user for confirmation. If FORCE is the symbol `no-check', ask if there are unsaved buffers. If FORCE is any other non-nil value, force killing of Emacs." (exwm--log) (when (cond ((and force (not (eq force 'no-check))) ;; Force killing Emacs. t) ((or (eq force 'no-check) (not exwm--id-buffer-alist)) ;; Check if there's any unsaved file. (pcase (catch 'break (let ((kill-emacs-query-functions (append kill-emacs-query-functions (list (lambda () (throw 'break 'break)))))) (save-buffers-kill-emacs))) (`break (y-or-n-p prompt)) (x x))) (t (yes-or-no-p (format "[EXWM] %d X window(s) will be destroyed. %s?" (length exwm--id-buffer-alist) prompt)))) ;; Run `kill-emacs-hook' (`server-force-stop' excluded) before Emacs ;; frames are unmapped so that errors (if any) can be visible. (if (memq #'server-force-stop kill-emacs-hook) (progn (setq kill-emacs-hook (delq #'server-force-stop kill-emacs-hook)) (run-hooks 'kill-emacs-hook) (setq kill-emacs-hook (list #'server-force-stop))) (run-hooks 'kill-emacs-hook) (setq kill-emacs-hook nil)) ;; Exit each module, destroying all resources created by this connection. (exwm-exit) ;; Set the return value. t)) (provide 'exwm) ;;; exwm.el ends here