《elisp 每日一练》——状态栏显示管理与modeline 动态更新

前面几篇我们陆陆续续的分析了 define-mode-line 中管理开关变量、快捷键相关的内容。我们可以通过 init-valueglobal来设置开关变量的初始值和是否定义为全局启动。通过 keymap 可以定义快捷键的keymap。现在我们来讨论 lighter 关键字的相关内容

背景知识

mode-line-format

lighter 关键字是用来定义当 Minor Mode 激活时在 Mode Line (状态栏) 上显示的文本。在详细描述 lighter 的作用之前,我们要先来讨论 Mode Line 的显示机制

Emacs 的 Mode Line 位于每个窗口底部, 在未经任何美化的Emacs中,它位于 minibuffer 上方显示为灰色,用于显示缓冲区状态和模式信息。它的显示内容由 buffer local 变量 mode-line-format。这是一个由列表、字符串、符号和数字构成的递归数据结构。

我在Windows上打开的 *scratch* buffer 中,它的显示的内容为 c\--- *scratc* All L4 (Lisp Interaction ElDoc)

  • c 表示当前缓冲区的编码系统是c, 通常对应 utf-8 或 undecided 在中文环境下的显示,具体取决于你的 buffer-file-coding-system
  • \ 表示换行符的类型是 Dos 风格,也就是采用 \r\n 来换行,对于unix 类型的换行,它显示为 :,Mac风格的换行符显示为 /
  • 第一个 - 表示当前缓冲区是可写的,对于不可写的通常表现是 %
  • 第二个 - 表示当前缓冲区未被修改,修改之后显示为 *
  • 第三个 - 表示是在本地存储,如果是远程的会显示为 @
  • *scratch* 表示当前buffer 的名称
  • ALL 表示当前窗口显示了整个buffer,如果buffer 内容较多,一个窗口显示不下的话,这里可能是 TOP表示显示的是开头位置,BOTTOM 表示显示的结尾,又或者是一个百分比
  • L4 表示光标在第四行
  • Lisp Interaction ElDoc 表示当前加载的 Major Mode 是 Lisp Interaction 、Minor Mode 是 ElDoc

这些东西其实不用记忆,因为Emacs最大的特点是灵活,你不喜欢这些显示内容完全可以自己修改,显示自己想要的信息。我们可以通过调整 mode-line-format 变量的值来修改显示内容。

我们查看当前 *scratch* buffer 中 mode-line-format 的值如下:

emacs-lisp 复制代码
(%e
 mode-line-front-space
 (:propertize
  (mode-line-mule-info
   mode-line-client
   mode-line-modified
   mode-line-remote
   mode-line-window-dedicated)
  display (min-width (6.0)))
 mode-line-frame-identification
 mode-line-buffer-identification
 "   "
 mode-line-position
 (project-mode-line project-mode-line-format)
 (vc-mode vc-mode)
 "  "
 mode-line-modes
 mode-line-misc-info
 mode-line-end-spaces)

这个例子很好的演示了 mode-line-format 中的几种常见数据类型:

符号

例如 mode-line-front-spacemode-line-buffer-identificationmode-line-positionmode-line-modes 等等都是一个符号,对于符号,Emacs会取出它的值,这些值可以是列表、字符串、或者数字,后面会再把它当作新的结构递归处理。

字符串

它可以是固定的字符串,也可以以 % 开头的格式化内容,例如 %b 会显示buffer 名称、%f 显示对应的文件名。这些内容读者可以查看Emacs中关于 mode-line-format 的文档。我们这个例子中的字符串仅仅是几个空格

列表

整个 mode-line-format 就是一个列表,列表的处理规则取决于第一个元素。如果第一个元素是字符串或者另一个列表,则递归处理所有元素并拼接。

% 构造

这里开头位置是一个 %e ,虽然说字符串中也可以使用 %e ,但是这里显然不在字符串中。这里的 %e 表示当前缓冲区文件名或进程状态的简短表示。

:propertize

:propertize 构造规则是: 先递归处理第一个参数(这里是包含多个符号的列表),然后给结果加上文本属性。这里加的是 display 属性,值为 (min-width (6.0)),意思是这段内容至少占 6 个字符宽度。这解决了编码信息(如 c\)在窗口窄时被截断的问题。

条件构造

条件构造的语法规则是 (symbol then else) , (project-mode-line project-mode-line-format) 就是一个条件构造,如果 project-mode-line 是非 nil 则,处理 project-mode-line-format。后面的 vc-mode 也是同理,它表示,如果当前buffer 开启了 版本管理,就显示版本管理的信息。读者可以打开一个git管理的项目,看看显示效果

根据上面的介绍,我们可以推测出上面 mode-line-format 的值将会如何显示:

  • %e 是文件名/进程状态简短标记
  • mode-line-front-space 前端空白,通常为空或一个空格
  • :propertize 它是一个结构,你可以理解为一组信息。根据后面的符号我们知道它分别显示: 编码、客户端、修改状态、远程、专用窗口标记,并且根据后面的属性定义可以知道,它保证至少 6 字符宽
  • mode-line-frame-identification 帧标识,在我的Emacs scratch buffer中显示为空
  • mode-line-buffer-identification 缓冲区名称
  • mode-line-position 光标所在位置
  • project-mode-line 如果启用 project-mode 则通过 project-mode-line-format 显示项目相关的信息
  • vc-mode 如果开启了版本管理,显示版本管理相关的信息
  • mode-line-modes 显示当前开启的mode 信息
  • mode-line-misc-info 其他信息
  • mode-line-end-spaces 尾部空白,通常用于右对齐或者填充

一般来讲,如果我们希望修改显示的内容,可以修改其中的某个符号。或者我们可以往 mode-line-misc-info ,这个是官方预留给我们用来自定义显示内容的接口。但是我们修改之后Emacs不会立马启用,需要调用 (force-mode-line-update) 让本次变更在下次重绘时生效。

lighter

emacs-lisp 复制代码
(define-minor-mode my/focus-mode
  ""
  :lighter "Be Focus")

展开之后,我们发现实际上与 Mode Line 显示相关的内容主要操作是:

emacs-lisp 复制代码
(add-minor-mode 'my/focus-mode '"Be Focus"
		    (if (boundp 'my/focus-mode-map) my/focus-mode-map)
		    nil nil)

通过将 :lighter 中指定的内容传入 add-minor-mode 作为参数

然后在切换状态的函数 my/focus-mode 最后调用 (force-mode-line-update) 刷新显示状态。

我们再进入 add-minor-mode 中查看关于 Mode Line 显示信息的相关内容:

emacs-lisp 复制代码
(when name
    (let ((existing (assq toggle minor-mode-alist)))
      (if existing
	  (setcdr existing (list name))
	(let ((tail minor-mode-alist) found)
	  (while (and tail (not found))
	    (if (eq after (caar tail))
		(setq found tail)
	      (setq tail (cdr tail))))
	  (if found
	      (let ((rest (cdr found)))
		(setcdr found nil)
		(nconc found (list (list toggle name)) rest))
	    (push (list toggle name) minor-mode-alist))))))

我们来分析一下,它是如何将我们的内容显示到 mode line 上的。这段代码与昨天我们分析 keymap 时的行为类似,它是通过修改 minor-mode-alist 来修改 mode line 的显示内容。我们发现它在插入时仍然与 after 有关,默认它会尝试插入到 after 之后。

我们前面介绍过,默认的 mode-line-format 中通过 mode-line-modes 显示开启的mode信息。我们查看 mode-line-modes 的值可以找到这样一段代码:

emacs-lisp 复制代码
(:propertize
 (#1= minor-mode-alist)          ; 要显示的 construct
 mouse-face mode-line-highlight  ; 鼠标悬停时高亮
 help-echo "Minor mode
mouse-1: Display minor mode menu
mouse-2: Show help for minor mode
mouse-3: Toggle minor modes"
 local-map
 (keymap
  (header-line keymap
               (down-mouse-3 . #4=(menu-item "Menu Bar" #3#
                                             :filter
                                             bindings--sort-menu-keymap)))
  (mode-line keymap
             (down-mouse-3 . #4#)
             (mouse-2 . mode-line-minor-mode-help)
             (down-mouse-1 . mouse-minor-mode-menu))))

这里我们又遇到了 :propertize 。我们还是一样,将第一个元素 (#1= minor-mode-alist) 作为显示的内容,第二个元素 mouse-face mode-line-highlight 作为属性,表示这部分显示的内容鼠标放在上面要高亮。后面的 help-echo 表示鼠标放在上面弹出的提示信息。

我们来看看 (#1= minor-mode-alist) 表示的含义。前面的 #1= 表示为 #1 。后面可以使用 #1# 来引用。这个内容在前面已经被引用了,所以说这里实际上将 minor-mode-alist 的值标记为 #1 并且进行了显示,后面的内容就是显示的属性。我们可以将鼠标放在上面显示提示信息。

练习

本节的练习,我们围绕着修改 mode line 的显示进行,我们定义一个 minor mode ,它可以统计buffer中文字数量并显示在 mode line 中。

我们先定义一个统计函数

emacs-lisp 复制代码
(defun my/count-words()
  (save-excursion
    (goto-char (point-min))
    (let ((count 0))
      (while (forward-word 1)
	(setq count (1+ count)))
      count)))

我们可以在lighter 中使用 eval 构造,这里我们要在后续动态更新的单词数量,所以我们不能使用 (format "%s" my/count-words) 这种形式。我们在前面看到 lighter 中的内容会被原样传入到 add-minor-mode 中,相当于先格式化一个字符串,然后将字符串作为 name 加入到 minor-mode-alist 作为后续显示。后面实际上我们就无法实时更新了。所以这里最好由 eval 来构造一个结构,每次刷新时由Emacs自己计算。

emacs-lisp 复制代码
(define-minor-mode my/count-words-mode
  "Count Words"
  :lighter (:eval (format " WC:%d" (my/count-words)))
  :init-value t)

这样我们发现,每次我们输入一个新的单词之后,在mode line上都会更新显示。我们每一次输入都伴随着显示的刷新,Emacs每次刷新都会根据 mode-line-format 中的内容实时计算,每次计算时因为我们传入的是一个 eval 构造,所以每次都会调用 my/count-words 显示它的结果。

总结

本章的内容我们先介绍了 mode-line-format ,它是一个列表,Emacs每次更新显示都会解析列表并根据列表计算的值来更新 mode line 显示。

我们通过 define-minor-mode 中的 :lighter 定义的内容会通过调用 add-minor-mode 被追加到 minor-mode-alist 中。

mode-line-format 中存在一个名为 mode-line-modes 的符号,它也是一个列表,内部会通过解析 minor-mode-alist 来实现 minor mode 的名称。

相关推荐
Masimaro2 天前
《elisp 每日一练》——minor mode 开关变量的管理
emacs
Masimaro3 天前
《elisp 每日一练》—— Minor Mode 变量生成机制与状态控制
emacs
Masimaro4 天前
《elisp 每日一练》——文件与目录的回顾与综合练习
emacs
Masimaro11 天前
《elisp 每日一练》——minibuffer输入读取和set-transient-map 临时热键状态机
emacs
Masimaro14 天前
《elisp 每日一练》——Window 树与布局状态恢复
emacs
典典分享指南16 天前
Windows 自动化踩坑实录
python·intellij-idea·pyqt·emacs·visual studio
辰辉创聚17 天前
重组细胞因子全解析:定义、分类与应用指南
人工智能·eclipse·emacs·白细胞介素·重组蛋白·重组细胞因子
Masimaro19 天前
《elisp 每日一练》—— 前缀按键
emacs
带娃的IT创业者22 天前
Emacs 31:一场静默而深刻的范式演进
编辑器·文本编辑器·开源软件·emacs·lisp·软件架构