mirror of
https://github.com/SqrtMinusOne/exwm-modeline.git
synced 2025-12-10 14:35:14 +03:00
308 lines
12 KiB
EmacsLisp
308 lines
12 KiB
EmacsLisp
;;; exwm-modeline.el --- A modeline segment for EXWM workspaces -*- lexical-binding: t -*-
|
|
|
|
;; Copyright (C) 2021-2023 Korytov Pavel
|
|
;; Copyright (C) 2021 Ellis Kenyő
|
|
;; Copyright (C) 2008-2020 Natalie Weizenbaum <nex342@gmail.com>
|
|
|
|
;; Author: Korytov Pavel <thexcloud@gmail.com>
|
|
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
|
|
;; Version: 0.1.3
|
|
;; Package-Requires: ((emacs "27.1") (exwm "0.26"))
|
|
;; Homepage: https://github.com/SqrtMinusOne/exwm-modeline
|
|
;; Published-At: 2021-12-22
|
|
|
|
;; This file is NOT part of GNU Emacs.
|
|
|
|
;; This program 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.
|
|
|
|
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; A modeline segment to display exwm workspaces.
|
|
;;
|
|
;; Features:
|
|
;; - Supports `exwm-randr' to display only of workspaces related to
|
|
;; the the current monitor.
|
|
;; - The segment is clickable.
|
|
;;
|
|
;; Take a look at `exwm-modeline-mode' for more info.
|
|
|
|
;;; Code:
|
|
(require 'exwm)
|
|
(require 'exwm-randr)
|
|
(require 'exwm-workspace)
|
|
(require 'exwm-manage)
|
|
|
|
(defgroup exwm-modeline nil
|
|
"A modeline segment to show EXWM workspaces."
|
|
:group 'mode-line)
|
|
|
|
(defcustom exwm-modeline-dividers '("[" "]" "|")
|
|
"Plist of strings used to create the string shown in the modeline.
|
|
|
|
The first string is the start of the modestring, second is the
|
|
closing of the modestring, and the last is the divider between
|
|
workspaces."
|
|
:group 'exwm-modeline
|
|
:type '(list (string :tag "Open")
|
|
(string :tag "Close")
|
|
(string :tag "Divider")))
|
|
|
|
(defcustom exwm-modeline-short nil
|
|
"When set, display only the current workspace in the modeline."
|
|
:group 'exwm-modeline
|
|
:type 'boolean
|
|
:set (lambda (sym value)
|
|
(set-default sym value)
|
|
(when (bound-and-true-p exwm-modeline-mode)
|
|
(exwm-modeline-update))))
|
|
|
|
(defcustom exwm-modeline-randr t
|
|
"When set, only show workspaces on the current monitor."
|
|
:group 'exwm-modeline
|
|
:type 'boolean
|
|
:set (lambda (sym value)
|
|
(set-default sym value)
|
|
(when (bound-and-true-p exwm-modeline-mode)
|
|
(exwm-modeline-update))))
|
|
|
|
(defcustom exwm-modeline-display-urgent t
|
|
"When set, display the urgent status in the modeline.
|
|
|
|
With that set, the modeline will be updated on every workspace
|
|
switch, so the number of updates is increased significantly."
|
|
:group 'exwm-modeline
|
|
:type 'boolean
|
|
:set (lambda (sym value)
|
|
(set-default sym value)
|
|
(when (bound-and-true-p exwm-modeline-mode)
|
|
(exwm-modeline-update)
|
|
(if value
|
|
(progn
|
|
(advice-add #'exwm--update-hints :after #'exwm-modeline--urgency-advice)
|
|
(add-hook 'exwm-workspace-switch-hook #'exwm-modeline--urgency-advice))
|
|
(advice-remove #'exwm--update-hints #'exwm-modeline--urgency-advice)
|
|
(remove-hook 'exwm-workspace-switch-hook #'exwm-modeline--urgency-advice)))))
|
|
|
|
(defface exwm-modeline-current-workspace
|
|
;; I'd rather :inherit and override warning there, but well
|
|
`((t :foreground ,(face-foreground 'warning) :weight bold))
|
|
"Face for the current workspace."
|
|
:group 'exwm-modeline)
|
|
|
|
(defface exwm-modeline-populated-workspace
|
|
'((t (:inherit success)))
|
|
"Face for any workspace populated with an X window."
|
|
:group 'exwm-modeline)
|
|
|
|
(defface exwm-modeline-empty-workspace
|
|
`((t (:inherit mode-line)))
|
|
"Face for any workspace without an X window."
|
|
:group 'exwm-modeline)
|
|
|
|
(defface exwm-modeline-urgent-workspace
|
|
'((t (:inherit error)))
|
|
"Face for any workspace that is tagged as urgent by X."
|
|
:group 'exwm-modeline)
|
|
|
|
(defun exwm-modeline--urgent-p (frame)
|
|
"Determine if FRAME is tagged as urgent.
|
|
|
|
Always return nil if `exwm-modeline-display-urgent' is not set."
|
|
(when exwm-modeline-display-urgent
|
|
(frame-parameter frame 'exwm-urgency)))
|
|
|
|
(defun exwm-modeline--populated-p (frame)
|
|
"Determine if FRAME has any X windows."
|
|
(cl-loop for item in exwm--id-buffer-alist
|
|
if (eq frame (buffer-local-value 'exwm--frame (cdr item)))
|
|
return t))
|
|
|
|
(defun exwm-modeline--click (event)
|
|
"Process a click EVENT on the modeline segment."
|
|
(interactive "e")
|
|
(when-let
|
|
(target
|
|
(cl-loop with name = (format "%s" (car (posn-string (event-start event))))
|
|
for i from 0 to (1- (length exwm-workspace--list))
|
|
if (string-equal (funcall exwm-workspace-index-map i) name)
|
|
return i))
|
|
(exwm-workspace-switch target)))
|
|
|
|
(defconst exwm-modeline-line-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map [mode-line mouse-1] #'exwm-modeline--click)
|
|
map)
|
|
"A keymap for the modeline segment.")
|
|
|
|
(defun exwm-modeline--format-list (workspace-list)
|
|
"Format the modestring for the current frame.
|
|
|
|
WORKSPACE-LIST is the list of frames to display."
|
|
(cl-loop for frame in workspace-list
|
|
for i from 0 to (length workspace-list)
|
|
for workspace-name = (funcall exwm-workspace-index-map
|
|
(exwm-workspace--position frame))
|
|
with current-frame = (selected-frame)
|
|
if (= i 0) collect (nth 0 exwm-modeline-dividers)
|
|
collect
|
|
(propertize workspace-name
|
|
'face
|
|
(cond ((exwm-modeline--urgent-p frame)
|
|
'exwm-modeline-urgent-workspace)
|
|
((eq frame current-frame)
|
|
'exwm-modeline-current-workspace)
|
|
((exwm-modeline--populated-p frame)
|
|
'exwm-modeline-populated-workspace)
|
|
(t 'exwm-modeline-empty-workspace))
|
|
'local-map (unless (eq frame current-frame)
|
|
exwm-modeline-line-map)
|
|
'mouse-face (unless (eq frame current-frame)
|
|
'mode-line-highlight))
|
|
if (= i (1- (length workspace-list)))
|
|
collect (nth 1 exwm-modeline-dividers)
|
|
else collect (nth 2 exwm-modeline-dividers)))
|
|
|
|
(defun exwm-modeline--randr-workspaces ()
|
|
"Get workspaces on the same monitor as current frame."
|
|
(if-let ((monitor (plist-get exwm-randr-workspace-monitor-plist
|
|
(cl-position (selected-frame)
|
|
exwm-workspace--list)))
|
|
;; This list of frame actually can be wrongly ordered,
|
|
;; hence the second loop. The number of values is quite
|
|
;; small, so it's not like o(n^2) can cause any issues.
|
|
(frames (cl-loop for (key value) on exwm-randr-workspace-monitor-plist
|
|
by 'cddr
|
|
if (string-equal value monitor)
|
|
collect (nth key exwm-workspace--list))))
|
|
(cl-loop for frame in exwm-workspace--list
|
|
if (member frame frames)
|
|
collect frame)
|
|
(cl-loop with indices = (cl-loop
|
|
for (key _) on exwm-randr-workspace-monitor-plist
|
|
by 'cddr collect key)
|
|
for i from 0 to (1- (length exwm-workspace--list))
|
|
for frame in exwm-workspace--list
|
|
unless (member i indices)
|
|
collect frame)))
|
|
|
|
(defun exwm-modeline--format ()
|
|
"Format a modeline string for the current workspace."
|
|
(exwm-modeline--format-list
|
|
(cond ((or exwm-modeline-short exwm--floating-frame)
|
|
(list (selected-frame)))
|
|
(exwm-modeline-randr (exwm-modeline--randr-workspaces))
|
|
(t exwm-workspace--list))))
|
|
|
|
(defun exwm-modeline-update ()
|
|
"Update EXWM modefine for every frame."
|
|
(interactive)
|
|
(cl-loop for frame in exwm-workspace--list
|
|
do (with-selected-frame frame
|
|
(set-frame-parameter nil 'exwm-modeline--string
|
|
(exwm-modeline--format)))))
|
|
|
|
(defun exwm-modeline-segment ()
|
|
"Get a modeline string for the current EXWM frame."
|
|
(frame-parameter nil 'exwm-modeline--string))
|
|
|
|
(defun exwm-modeline--unmanage-advice (&rest _)
|
|
"Update the modeline after unmanaging a window.
|
|
|
|
This function is meant to be advised :after
|
|
`exwm-manage--unmanage-window', because that's when a workspace can
|
|
lose all its X windows and thus may become \"unpopulated\",i.e. the
|
|
face in the segment has to change."
|
|
(exwm-modeline-update))
|
|
|
|
(defun exwm-modeline--urgency-advice (&rest _)
|
|
"Update the modeline after a change in the urgency status.
|
|
|
|
This function is meant to be advised :after `exwm--update-hints' and
|
|
be in the hook `exwm-workspace-switch-hook'.
|
|
|
|
The modeline is updated if `exwm-workspace--switch-history-outdated'
|
|
is set to t, because EXWM sets that variable whenever a window updates
|
|
it urgency status. To avoid running the function too often,
|
|
`exwm-workspace--update-switch-history' is also called, which resets
|
|
the variable.
|
|
|
|
However, one issue with the mentioned variable is that it is also
|
|
set whenever the workspace is switched, so using that advice also
|
|
increases the number of required updates. Optimizing that would
|
|
require more substantial modifications to EXWM code, so in this
|
|
package applying that advice is made optional with the
|
|
`exwm-modeline-display-urgent' variable.
|
|
|
|
Because the first function is very much critical for the normal
|
|
functioning of EXWM, the entire thing is wrapped in
|
|
`with-demoted-errors'."
|
|
(with-demoted-errors "Error in exwm-modeline--urgency-advice: %S"
|
|
(when exwm-workspace--switch-history-outdated
|
|
(exwm-modeline-update)
|
|
(exwm-workspace--update-switch-history))))
|
|
|
|
;;;###autoload
|
|
(define-minor-mode exwm-modeline-mode
|
|
"A mode for displaying EXWM workspaces in the modeline.
|
|
|
|
Make sure to call this after EXWM was initialized, for instance
|
|
in `exwm-init-hook'.
|
|
|
|
By default, the mode displays all the workspaces on the current
|
|
monitor. To display only the current workspace, enable
|
|
`exwm-modeline-short', and to disable the filtering by the
|
|
monitor, disable `exwm-modeline-randr'.
|
|
|
|
If `exwm-modeline-display-urgent' is set, the mode also displays
|
|
if the workspace has a window market as urgent. However, this
|
|
option forces the modeline to update after every workspace
|
|
switch, so it may be wise to disable that in case of performance
|
|
issues.
|
|
|
|
Also, take a look at the `exwm-modeline' group for faces
|
|
customization.
|
|
|
|
This implementation intends to reduce the count of times of
|
|
evaluating the modestring; the rendered modestring is saved as a
|
|
frame parameter, and `exwm-modeline-segment' just returns it.
|
|
|
|
The update itself is done via the `exwm-modeline-update'
|
|
function. You may need to run it manually after updating the
|
|
parameters, but other than that, this mode should cover all the
|
|
cases when the workspace list changes."
|
|
:global t
|
|
(if exwm-modeline-mode
|
|
(progn
|
|
(exwm-modeline-update)
|
|
(add-to-list 'global-mode-string '(:eval (exwm-modeline-segment)))
|
|
(add-hook 'exwm-workspace-list-change-hook #'exwm-modeline-update)
|
|
(add-hook 'exwm-randr-refresh-hook #'exwm-modeline-update)
|
|
(add-hook 'exwm-manage-finish-hook #'exwm-modeline-update)
|
|
(advice-add #'exwm-manage--unmanage-window
|
|
:after #'exwm-modeline--unmanage-advice)
|
|
(when exwm-modeline-display-urgent
|
|
(advice-add #'exwm--update-hints :after #'exwm-modeline--urgency-advice)
|
|
(add-hook 'exwm-workspace-switch-hook #'exwm-modeline--urgency-advice)))
|
|
(setq global-mode-string (delete '(:eval (exwm-modeline-segment))
|
|
global-mode-string))
|
|
(remove-hook 'exwm-workspace-list-change-hook #'exwm-modeline-update)
|
|
(remove-hook 'exwm-randr-refresh-hook #'exwm-modeline-update)
|
|
(remove-hook 'exwm-manage-finish-hook #'exwm-modeline-update)
|
|
(advice-remove #'exwm-manage--unmanage-window
|
|
#'exwm-modeline--unmanage-advice)
|
|
(advice-remove #'exwm--update-hints #'exwm-modeline--urgency-advice)
|
|
(remove-hook 'exwm-workspace-switch-hook #'exwm-modeline--urgency-advice)))
|
|
|
|
(provide 'exwm-modeline)
|
|
;;; exwm-modeline.el ends here
|