stumpwm.stumpwm/mode-line.lisp
David 49b977945f
Some checks failed
Build and Test / build_and_test (push) Has been cancelled
Merge pull request #1257 from BaalHadad/fixup-some-commands
Rewrite  renumber, toggle-always-on-top and  toggle-always-show
2026-05-29 21:59:00 -04:00

522 lines
19 KiB
Common Lisp

;; Copyright (C) 2006-2008 Shawn Betts
;; Copyright (C) 2016 Joram Schrijver
;;
;; This file is part of stumpwm.
;;
;; stumpwm 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 2, or (at your option)
;; any later version.
;; stumpwm 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 software; see the file COPYING. If not, see
;; <http://www.gnu.org/licenses/>.
(in-package :stumpwm)
(export '(*mode-line-background-color*
*mode-line-border-color*
*mode-line-border-width*
*mode-line-foreground-color*
*mode-line-window-class*
*mode-line-pad-x*
*mode-line-pad-y*
*mode-line-position*
*mode-line-timeout*
*screen-mode-line-format*
*screen-mode-line-formatters*
add-screen-mode-line-formatter
register-ml-on-click-id
enable-mode-line
toggle-mode-line))
;;; Settings
(defvar *mode-line-position* :top
"Specifies where the mode line is displayed. Valid values are :top and :bottom.")
(defvar *mode-line-border-width* 1
"Specifies how thick the mode line's border will be. Integer value.")
(defvar *mode-line-pad-x* 5
"Specifies the number of padding pixels between the text and the side of the mode line. Integer value.")
(defvar *mode-line-pad-y* 1
"The number of padding pixels between the modeline text and the top/bottom of the modeline. Integer value.")
(defvar *mode-line-window-class* "stumpwm-mode-line"
"The WM class of a mode-line window.")
(defvar *mode-line-background-color* "Gray20"
"The mode line background color.")
(defvar *mode-line-foreground-color* "Gray50"
"The mode line foreground color.")
(defvar *mode-line-border-color* "Gray30"
"The mode line border color.")
(defvar *screen-mode-line-format* "[^B%n^b] %W"
"This variable describes what will be displayed on the modeline for each screen.
Turn it on with the function TOGGLE-MODE-LINE or the mode-line command.
It is a list where each element may be a string, a symbol, or a list.
For a symbol its value is used.
For a list of the form (:eval FORM) FORM is evaluated and the
result is used as a mode line element.
If it is a string the string is printed with the following formatting
options:
@table @asis
@item %h
List the number of the head the mode-line belongs to
@item %w
List all windows in the current group windows using @var{*window-format*}
@item %W
List all windows on the current head of the current group using
@var{*window-format*}
@item %g
List the groups using @var{*group-format*}
@item %n
The current group's name
@item %u
Using @var{*window-format*}, return a 1 line list of the urgent windows, space seperated.
@item %v
Using @var{*window-format*}, return a 1 line list of the windows, space
separated. The currently focused window is highlighted with
fmt-highlight. Any non-visible windows are colored the
*hidden-window-color*.
@item %d
Using @var{*time-modeline-string*}, print the time.
@end table
A number of modules have been written that extends the possible
formatting strings. See their documentation for details.")
(defvar *screen-mode-line-formatters* ()
"An alist containing format character format function pairs for
formatting screen mode-lines. functions are passed the mode line.")
(defvar *mode-line-timeout* 60
"The modeline updates after each command, when a new window appears or
an existing one disappears, and on a timer. This variable controls how
many seconds elapse between each update. If this variable is changed
while the modeline is visible, you must toggle the modeline to update
timer.")
(defvar *mode-line-timer* nil
"The timer that updates the modeline")
;;; Formatters
(defun add-screen-mode-line-formatter (character fmt-fun)
"Add a format function to a format character (or overwrite an existing one)."
(setf *screen-mode-line-formatters*
(cons (list character fmt-fun)
(remove character *screen-mode-line-formatters* :key #'first))))
;;; Mode lines
(defvar *mode-lines* ()
"All current mode lines.")
;;; Utilities
(defun screen-mode-lines (screen)
(remove-if (lambda (mode-line)
(not (eq screen (mode-line-screen mode-line))))
*mode-lines*))
(defun head-mode-line (head)
(find head *mode-lines* :key #'mode-line-head))
(defun find-mode-line-by-window (xwin)
(find (xlib:window-id xwin) *mode-lines*
:key (lambda (ml)
(when-let ((win (mode-line-window ml)))
(xlib:window-id win)))))
(defun mode-line-gc (ml)
(ccontext-gc (mode-line-cc ml)))
(defun turn-on-mode-line-timer ()
(when (timer-p *mode-line-timer*)
(cancel-timer *mode-line-timer*))
(setf *mode-line-timer* (run-with-timer *mode-line-timeout*
*mode-line-timeout*
'update-all-mode-lines)))
(defun maybe-cancel-mode-line-timer ()
(unless *mode-lines*
(when (timer-p *mode-line-timer*)
(cancel-timer *mode-line-timer*)
(setf *mode-line-timer* nil))))
;;; Creation
(defun resize-mode-line (mode-line)
(when (eq (mode-line-mode mode-line) :stump)
;; This is a StumpWM mode-line
(setf (xlib:drawable-height (mode-line-window mode-line))
(+ (* 2 *mode-line-pad-y*)
(nth-value 1 (rendered-size
(split-string (mode-line-contents mode-line)
(string #\Newline))
(mode-line-cc mode-line))))))
(with-accessors ((window mode-line-window)
(head mode-line-head)
(position mode-line-position)
(height mode-line-height)
(factor mode-line-factor))
mode-line
(setf (xlib:drawable-width window) (- (frame-width head)
(* 2 (xlib:drawable-border-width
window)))
(xlib:drawable-height window) (min (xlib:drawable-height window)
(truncate (head-height head) 4))
height (+ (xlib:drawable-height window)
(* 2 (xlib:drawable-border-width window)))
factor (- 1 (/ height
(head-height head)))
(xlib:drawable-x window) (head-x head)
(xlib:drawable-y window) (if (eq position :top)
(head-y head)
(- (+ (head-y head)
(head-height head))
height)))))
(defun update-mode-line-color-context (ml)
(let* ((cc (mode-line-cc ml))
(screen (mode-line-screen ml))
(bright (lookup-color screen *mode-line-foreground-color*)))
(adjust-color bright 0.25)
(setf (ccontext-default-bright cc) (alloc-color screen bright))))
(defun make-mode-line-window (screen)
"Create a window suitable for a modeline."
(let ((window
(xlib:create-window
:parent (screen-root screen)
:x 0 :y 0 :width 1 :height 1
:background (alloc-color screen *mode-line-background-color*)
:border (alloc-color screen *mode-line-border-color*)
:border-width *mode-line-border-width*
;; You can click the modeline
:event-mask (xlib:make-event-mask :button-press :exposure)
;; these windows are not controlled by the window manager
:override-redirect :on)))
(xlib:set-wm-class window *mode-line-window-class* *mode-line-window-class*)
window))
(defun make-mode-line-gc (window screen)
(xlib:create-gcontext
:drawable window
:font (when (typep (screen-font screen) 'xlib:font)
(screen-font screen))
:foreground (alloc-color screen *mode-line-foreground-color*)
:background (alloc-color screen *mode-line-background-color*)))
(defun make-mode-line-cc (window screen gc)
(make-ccontext :gc gc
:screen screen
:font (screen-font screen)
:win window
:default-fg (xlib:gcontext-foreground gc)
:default-bg (xlib:gcontext-background gc)))
(defun make-mode-line (screen head format)
(let* ((window (make-mode-line-window screen))
(gc (make-mode-line-gc window screen))
(cc (make-mode-line-cc window screen gc))
(mode-line (%make-mode-line :window window
:screen screen
:head head
:format format
:position *mode-line-position*
:cc cc)))
(prog1 mode-line
(push mode-line *mode-lines*)
(update-mode-line-color-context mode-line)
(resize-mode-line mode-line)
(xlib:map-window window)
(setf (xlib:window-priority window) :below)
(redraw-mode-line mode-line)
(dformat 3 "modeline: ~s~%" mode-line)
(turn-on-mode-line-timer)
(run-hook-with-args *new-mode-line-hook* mode-line))))
;;; Destruction
(defun sync-mode-line (ml)
(dolist (group (screen-groups (mode-line-screen ml)))
(group-sync-head group (mode-line-head ml))))
(defun destroy-mode-line (ml)
(run-hook-with-args *destroy-mode-line-hook* ml)
(xlib:destroy-window (mode-line-window ml))
(when (mode-line-cc ml) ;; mode-lines of dock-type windows do not have a cc
(xlib:free-gcontext (mode-line-gc ml)))
(setf *mode-lines* (remove ml *mode-lines*))
(sync-mode-line ml)
(maybe-cancel-mode-line-timer))
(defun destroy-all-mode-lines ()
(dolist (ml *mode-lines*)
(destroy-mode-line ml)))
;;; Formatting
(defvar *current-mode-line-formatters* nil
"used in formatting modeline strings.")
(defvar *current-mode-line-formatter-args* nil
"used in formatting modeline strings.")
(defgeneric mode-line-format-elt (elt))
(defmethod mode-line-format-elt ((elt string))
(apply 'format-expand
*current-mode-line-formatters*
elt
*current-mode-line-formatter-args*))
(defmethod mode-line-format-elt ((elt symbol))
(if (boundp elt)
(let ((val (symbol-value elt)))
;; ignore T and nil, like emacs.
(unless (typep val 'boolean)
(mode-line-format-elt val)))
(symbol-name elt)))
(defmethod mode-line-format-elt ((elt null))
"")
(defmethod mode-line-format-elt ((elt list))
(etypecase (first elt)
((or string list)
(apply 'concatenate 'string
(mapcar 'mode-line-format-elt elt)))
(symbol
(mode-line-format-elt
(case (first elt)
;; FIXME: silently failing is probably not the best idea.
(:eval (ignore-errors (eval (second elt))))
(t (and (boundp (first elt))
(symbol-value (first elt))
(second elt))))))))
(defun mode-line-format-string (ml)
(mode-line-format-elt (mode-line-format ml)))
(defun redraw-mode-line (ml &optional force)
(when (eq (mode-line-mode ml) :stump)
(setf (mode-line-new-bounds ml) nil)
(let* ((*current-mode-line-formatters* *screen-mode-line-formatters*)
(*current-mode-line-formatter-args* (list ml))
(str
(handler-case (mode-line-format-string ml)
(error (c)
(format nil "Unable to expand mode line format string: ~S" c)))))
(flet ((resize-and-render (string)
(setf (mode-line-contents ml) string)
(resize-mode-line ml)
(render-strings (mode-line-cc ml)
*mode-line-pad-x*
*mode-line-pad-y*
(split-string string (string #\Newline))
()
:ml ml)
(when (mode-line-new-bounds ml)
(setf (mode-line-on-click-bounds ml)
(reverse (mode-line-new-bounds ml))))))
(handler-case
(when (or force (not (string= (mode-line-contents ml) str)))
(resize-and-render str))
(error (c)
(resize-and-render
(format nil "Unable to render mode line: ~S" c))))))))
(defun update-mode-lines (screen)
"Update all mode lines on SCREEN"
(dolist (mode-line (screen-mode-lines screen))
(redraw-mode-line mode-line)))
(defun update-all-mode-lines ()
"Update all mode lines."
(mapc 'redraw-mode-line *mode-lines*))
(defun refresh-colors-for-modeline (screen head)
"Set the colors for the modeline to the currently set values then redraw"
(let* ((ml (head-mode-line head))
(cc (mode-line-cc ml))
(win (mode-line-window ml)))
(setf (ccontext-default-bg cc)
(alloc-color screen *mode-line-background-color*))
(setf (ccontext-default-fg cc)
(alloc-color screen *mode-line-foreground-color*))
(setf (xlib:window-background win)
(alloc-color screen *mode-line-background-color*))
(redraw-mode-line ml t)))
;;; Registering mode line clickable areas
(defvar *mode-line-on-click-functions* nil
"An alist of IDs and and functions, used by :on-click formatter calls")
(defun register-ml-on-click-id (id fn)
"Register FN with ID, to be used by the :on-click mode line color formatter."
(let ((present (assoc id *mode-line-on-click-functions*)))
(if present
(setf (cdr present) fn)
(push (cons id fn) *mode-line-on-click-functions*))))
(defun register-ml-boundaries-with-id (ml xbeg xend ybeg yend id args)
(push (list xbeg xend ybeg yend id args) (mode-line-new-bounds ml)))
(defun mode-line-click-dispatcher (ml code x y)
"A function to hang on the mode line click hook which dispatches the
appropriate mode line click function."
(let ((registered-ids *mode-line-on-click-functions*)
(bounds-list (mode-line-on-click-bounds ml)))
(dformat 3 "In mode line click: x=~A~&~2Tregistered ids: ~S~&~2Tbounds: ~S~&"
x registered-ids bounds-list)
(loop for (xbeg xend ybeg yend id args) in bounds-list
do (when (and (< xbeg x xend)
(< ybeg y yend))
(let ((fn (assoc id registered-ids)))
(when fn
(dformat 3 "Mode line click, calling ~A" (cdr fn))
(apply (cdr fn) code args)))
(loop-finish)))))
(add-hook *mode-line-click-hook* 'mode-line-click-dispatcher)
(flet ((ml-on-click-focus-window (code id &rest rest)
(declare (ignore rest))
(when-let ((window (window-by-id id)))
(case (decode-button-code code)
(:left-button (focus-all window))
(:wheel-up (renumber-window window (1+ (window-number window))))
(:middle-button
(if (typep window 'float-window)
(unfloat-window window (window-group window))
(float-window window (window-group window))))
(:wheel-down (renumber-window window (1- (window-number window))))
(:right-button (destroy-window window)))
(update-mode-lines (current-screen))))
(ml-on-click-switch-to-group (code group &rest rest)
(declare (ignore rest code))
(when-let ((g (find-group (current-screen) group)))
(switch-to-group g)))
(ml-on-click-do-nothing (code &rest rest)
(declare (ignore rest code))
nil))
(register-ml-on-click-id :ml-on-click-focus-window #'ml-on-click-focus-window)
(register-ml-on-click-id :ml-on-click-switch-to-group
#'ml-on-click-switch-to-group)
(register-ml-on-click-id :ml-on-click-do-nothing #'ml-on-click-do-nothing))
;;; External mode lines
(defun move-mode-line-to-head (mode-line head)
(cond ((not (head-mode-line head))
(setf (mode-line-head mode-line) head))
((mode-line-head mode-line)
(rotatef (mode-line-head mode-line)
(mode-line-head (head-mode-line head))))))
(defun update-mode-line-position (mode-line x y)
(let ((head (or (find-if (lambda (h) (and (= x (head-x h))
(>= y (head-y h))
(< y (+ (head-y h)
(head-height h)))))
(screen-heads (mode-line-screen mode-line)))
;; No luck. Just try to find a head without a mode-line
;; already.
(find-if-not #'head-mode-line
(screen-heads (mode-line-screen mode-line))))))
(when head
(unless (eq head (mode-line-head mode-line))
(move-mode-line-to-head mode-line head))
(when (mode-line-head mode-line)
(setf (mode-line-position mode-line)
(if (< y (/ (head-height (mode-line-head mode-line)) 2))
:top
:bottom))))))
(defun place-mode-line-window (screen xwin)
(let ((ml (%make-mode-line
:window xwin
:screen screen
:mode :visible
:position *mode-line-position*)))
(push ml *mode-lines*)
(xlib:reparent-window xwin (screen-root screen) 0 0)
(when (update-mode-line-position ml
(xlib:drawable-x xwin)
(xlib:drawable-y xwin))
(resize-mode-line ml)
(xlib:map-window xwin)
(sync-mode-line ml))))
;;; Toggling
(defun toggle-mode-line (screen head
&optional (format '*screen-mode-line-format*))
"Toggle the state of the mode line for the specified screen"
(check-type format (or symbol list string))
(let ((ml (head-mode-line head)))
(if ml
(case (mode-line-mode ml)
(:visible
;; Hide it.
(setf (mode-line-mode ml) :hidden)
(xlib:unmap-window (mode-line-window ml)))
(:hidden
;; Show it.
(setf (mode-line-mode ml) :visible)
(xlib:map-window (mode-line-window ml))
(setf (xlib:window-priority (mode-line-window ml)) :below))
(:stump
;; Delete it
(destroy-mode-line ml)))
(make-mode-line screen head format))
(dolist (group (screen-groups screen))
(group-sync-head group head))))
(defun enable-mode-line (screen head state &optional format)
"Set the state of SCREEN's HEAD's mode-line. If STATE is T and FORMAT is
specified, then the mode-line's format is updated."
(check-type screen screen)
(check-type head head)
(check-type format (or symbol list string))
(let ((mode-line (head-mode-line head)))
(cond
((and state mode-line)
(when format
(setf (mode-line-format mode-line) format)))
(state
(toggle-mode-line screen head (or format '*screen-mode-line-format*)))
(mode-line
(toggle-mode-line screen head)))))
(defcommand mode-line () ()
"A command to toggle the mode line visibility."
(toggle-mode-line (current-screen) (current-head)))