面向产品经理的Emacs教程:5. 改变Emacs的样貌

· 3630字 · 8分钟

1 前言 🔗

在过去的四节课里,我们简单了解了什么是Emacs,什么是Org mode,如何通过Org mode来管理配置文件,以及配置安装包管理器。虽然有点枯燥,但这些都是后面起飞的基础。

2 改变一下样貌 🔗

刚刚装好的纯净的Emacs,不能说丑,但是跟我们系统里的其他程序比,字太小,窗口也挺小的,看上去就比较古老,不够现代。人常说,工欲善其事,必先利其器。

那么我们就先来改变一下Emacs的一些样貌吧。

2.1 给Emacs挑一个好看的主题 🔗

主题是最快能获得视觉反馈的设置,Emacs社区有无数漂亮的主题,可以按照个人的意愿自行安装,这里,我以 Protesilaos Stavrou 大佬的 ef-themes 为例,来安装一个好看的主题包(为什么说是主题包,而不是主题,是因为这个插件里包含了数个很漂亮的主题)。

2.1.1 创建 init-ui.el 标题行 🔗

按照模块化配置的思想,我们将所有Emacs样貌、交互相关的配置都放入 ~/.emacs.d/lisp/init-ui.el 里,这样的话,我们可以建一个Org mode的标题行叫 init-ui.el ,设置好 tangle 参数,并创建好文件头和文件尾两个标题行。后续我们的 UI 相关设置都插入到这两个标题行之间即可。

* init-ui.el
:PROPERTIES:
:HEADER-ARGS: :tangle lisp/init-ui.el :mkdirp yes
:END:

** init-ui.el 文件头
#+BEGIN_SRC emacs-lisp
;;; init-ui.el --- UI settings -*- lexical-binding: t -*-
;;; Commentary:

;;; Code:

#+END_SRC

** init-ui.el 文件尾
#+BEGIN_SRC emacs-lisp

(provide 'init-ui)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; init-ui.el ends here
#+END_SRC

我们除了添加完 init-ui.el 标题行之外,我们还需要在主配置文件 init.el 里,加载我们的模块化配置文件 init-ui.el ,这样才能将所有UI相关的配置生效。下面这段配置代码放在 emacs-config.org 文件的 init.el 这个标题行下面:

** 加载模块化配置

#+BEGIN_SRC emacs-lisp
;; 将lisp目录放到加载路径的前面以加快启动速度
(let ((dir (locate-user-emacs-file "lisp")))
  (add-to-list 'load-path (file-name-as-directory dir)))

;; 加载各模块化配置
;; 不要在`*message*'缓冲区显示加载模块化配置的信息
(with-temp-message ""
  (require 'init-ui)                    ; 加载UI交互的模块化配置文件
  )
#+END_SRC

2.1.2 配置 ef-themes 🔗

init-ui.el 文件头init-ui.el 文件尾 之间,创建一个新的标题行 ef主题 ,然后进行如下的配置即可(这里,我的配置是每次启动时随机挑选一款主题,你也可以固定挑选某个主题,如果需要固定挑选某个主题,参照下面的注释):

** ef主题

[[https://protesilaos.com/emacs/ef-themes][ef themes]] 是我非常喜欢的一个主题包。

#+BEGIN_SRC emacs-lisp
(use-package ef-themes
  :ensure t
  :bind ("C-c t" . ef-themes-toggle)
  :init
  ;; set two specific themes and switch between them
  (setq ef-themes-to-toggle '(ef-summer ef-winter))
  ;; set org headings and function syntax
  (setq ef-themes-headings
        '((0 . (bold 1))
          (1 . (bold 1))
          (2 . (rainbow bold 1))
          (3 . (rainbow bold 1))
          (4 . (rainbow bold 1))
          (t . (rainbow bold 1))))
  (setq ef-themes-region '(intense no-extend neutral))
  ;; Disable all other themes to avoid awkward blending:
  (mapc #'disable-theme custom-enabled-themes)

  ;; Load the theme of choice:
  ;; The themes we provide are recorded in the `ef-themes-dark-themes',
  ;; `ef-themes-light-themes'.

  ;; 如果你不喜欢随机主题,也可以直接固定选择一个主题,如下:
  ;; (ef-themes-select 'ef-summer)

  ;; 随机挑选一款主题,如果是命令行打开Emacs,则随机挑选一款黑色主题
  (if (display-graphic-p)
      (ef-themes-load-random)
    (ef-themes-load-random 'dark))

  :config
  ;; auto change theme, aligning with system themes.
  (defun my/apply-theme (appearance)
    "Load theme, taking current system APPEARANCE into consideration."
    (mapc #'disable-theme custom-enabled-themes)
    (pcase appearance
      ('light (if (display-graphic-p) (ef-themes-load-random 'light) (ef-themes-load-random 'dark)))
      ('dark (ef-themes-load-random 'dark))))

  (if (eq system-type 'darwin)
      ;; only for emacs-plus
      (add-hook 'ns-system-appearance-change-functions #'my/apply-theme)
    (ef-themes-select 'ef-summer)
    )
  )
#+END_SRC

2.1.3 运行 M-x org-babel-tangle 并重启生效 🔗

此时,emacs 配置文件夹里的目录架构大概是下面这样的:

/Users/randolph/.emacs.d
├── README.org
├── early-init.el
├── elpa
│   ├── archives
│   ├── diminish-20220909.847
│   ├── ef-themes-0.10.0
│   ├── ef-themes-0.10.0.signed
│   ├── gnupg
│   ├── quelpa-20221220.1136
│   └── quelpa-use-package-20201022.746
├── emacs-config.org
├── init.el
├── init.el~
└── lisp
    └── init-ui.el

2.1.4 效果 🔗

安装完主题之后的效果如下,可以看到我们的代码块有背景底色了,跟其他的文本区分开了。

2.2 给Emacs挑一个好看的字体 🔗

这里以我个人很喜欢的 Sarasa Mono SC Nerd 中文字体和 Source Code Pro 英文字体为例(这两个字体需要提前下载并安装,直接点击相关链接按提示下载安装即可),通过 fontaine 这个插件来配置中英文字体,以及确保等宽显示(具体配置不用懂先,只管copy paste,如果需要换成你喜欢的字体,只需要替换下面配置里的对应字体字符串即可):

** 字体设置

[[https://protesilaos.com/emacs/fontaine][fontaine]] 插件可以根据需要高度定制字体。

#+BEGIN_QUOTE
这篇文章可以作为字体设置的参考:
[[http://xahlee.info/emacs/emacs/emacs_list_and_set_font.html]]
#+END_QUOTE

#+BEGIN_SRC emacs-lisp
(use-package fontaine
  :ensure t
  :when (display-graphic-p)
  ;; :hook (kill-emacs . fontaine-store-latest-preset)
  :config
  (setq fontaine-latest-state-file
        (locate-user-emacs-file "etc/fontaine-latest-state.eld"))
  (setq fontaine-presets
        '((regular
           :default-height 140
           :default-weight regular
           :fixed-pitch-height 1.0
           :variable-pitch-height 1.0
           )
          (large
           :default-height 180
           :default-weight normal
           :fixed-pitch-height 1.0
           :variable-pitch-height 1.05
           )
          (t
           :default-family "Source Code Pro"
           :fixed-pitch-family "Source Code Pro"
           :variable-pitch-family "Source Code Pro"
           :italic-family "Source Code Pro"
           :variable-pitch-weight normal
           :bold-weight normal
           :italic-slant italic
           :line-spacing 0.1)
          ))
  ;; (fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular))
  (fontaine-set-preset 'regular)

  ;; set emoji font
  (set-fontset-font
   t
   (if (version< emacs-version "28.1")
       '(#x1f300 . #x1fad0)
     'emoji)
   (cond
    ((member "Noto Emoji" (font-family-list)) "Noto Emoji")
    ((member "Symbola" (font-family-list)) "Symbola")
    ((member "Apple Color Emoji" (font-family-list)) "Apple Color Emoji")
    ((member "Noto Color Emoji" (font-family-list)) "Noto Color Emoji")
    ((member "Segoe UI Emoji" (font-family-list)) "Segoe UI Emoji")
    ))

  ;; set Chinese font
  (dolist (charset '(kana han symbol cjk-misc bopomofo))
    (set-fontset-font
     (frame-parameter nil 'font)
     charset
     (font-spec :family
                (cond
                 ((eq system-type 'darwin)
                  (cond
                   ((member "Sarasa Mono SC Nerd" (font-family-list)) "Sarasa Mono SC Nerd")
                   ((member "PingFang SC" (font-family-list)) "PingFang SC")
                   ((member "WenQuanYi Zen Hei" (font-family-list)) "WenQuanYi Zen Hei")
                   ((member "Microsoft YaHei" (font-family-list)) "Microsoft YaHei")
                   ))
                 ((eq system-type 'gnu/linux)
                  (cond
                   ((member "Sarasa Mono SC Nerd" (font-family-list)) "Sarasa Mono SC Nerd")
                   ((member "WenQuanYi Micro Hei" (font-family-list)) "WenQuanYi Micro Hei")
                   ((member "WenQuanYi Zen Hei" (font-family-list)) "WenQuanYi Zen Hei")
                   ((member "Microsoft YaHei" (font-family-list)) "Microsoft YaHei")
                   ))
                 (t
                  (cond
                   ((member "Sarasa Mono SC Nerd" (font-family-list)) "Sarasa Mono SC Nerd")
                   ((member "Microsoft YaHei" (font-family-list)) "Microsoft YaHei")
                   )))
                )))

  ;; set Chinese font scale
  (setq face-font-rescale-alist `(
                                  ("Symbola"             . 1.3)
                                  ("Microsoft YaHei"     . 1.2)
                                  ("WenQuanYi Zen Hei"   . 1.2)
                                  ("Sarasa Mono SC Nerd" . 1.2)
                                  ("PingFang SC"         . 1.16)
                                  ("Lantinghei SC"       . 1.16)
                                  ("Kaiti SC"            . 1.16)
                                  ("Yuanti SC"           . 1.16)
                                  ("Apple Color Emoji"   . 0.91)
                                  ))
  )
#+END_SRC

#+CAPTION: 测试中英文字体对齐
#+NAME: 测试中英文字体对齐
| 中文  |   |
| abcd |   |

配置生效后的截图如下:

2.3 修改Emacs的一些默认设置 🔗

2.3.1 改变默认启动窗口大小 🔗

在Mac上打开Emacs时,默认的窗口比较小,我们把它调大一些:

** 调整启动窗口大小

#+BEGIN_SRC emacs-lisp
;; 设置窗口大小,仅仅在图形界面需要设置
(when (display-graphic-p)
  (let ((top    0)                                     ; 顶不留空
        (left   (/ (x-display-pixel-width) 10))        ; 左边空10%
        (height (round (* 0.8                          ; 窗体高度为0.8倍的显示高度
                          (/ (x-display-pixel-height)
                             (frame-char-height))))))
    (let ((width  (round (* 2.5 height))))             ; 窗体宽度为2.5倍高度
      (setq default-frame-alist nil)
      (add-to-list 'default-frame-alist (cons 'top top))
      (add-to-list 'default-frame-alist (cons 'left left))
      (add-to-list 'default-frame-alist (cons 'height height))
      (add-to-list 'default-frame-alist (cons 'width width)))))
#+END_SRC

注: default-frame-alist 里的宽度和高度的单位,均为字符宽度和高度,即整个窗体多少字符高,多少字符宽,而不是像素。

2.3.2 一些零散设置项 🔗

下面是一些零散的小设置项,能让体验更佳,如设置光标不闪烁,设置不响铃等,具体,这一块照抄就可以啦:

** 其他UI零散设置项

#+begin_src emacs-lisp
;; 禁用一些GUI特性
(setq use-dialog-box nil)               ; 鼠标操作不使用对话框
(setq inhibit-default-init t)           ; 不加载 `default' 库
(setq inhibit-startup-screen t)         ; 不加载启动画面
(setq inhibit-startup-message t)        ; 不加载启动消息
(setq inhibit-startup-buffer-menu t)    ; 不显示缓冲区列表

;; 草稿缓冲区默认文字设置
(setq initial-scratch-message (concat ";; Happy hacking, "
                                      (capitalize user-login-name) " - Emacs ♥ you!\n\n"))

;; 设置缓冲区的文字方向为从左到右
(setq bidi-paragraph-direction 'left-to-right)
;; 禁止使用双向括号算法
;; (setq bidi-inhibit-bpa t)

;; 设置自动折行宽度为80个字符,默认值为70
(setq-default fill-column 80)

;; 设置大文件阈值为100MB,默认10MB
(setq large-file-warning-threshold 100000000)

;; 以16进制显示字节数
(setq display-raw-bytes-as-hex t)
;; 有输入时禁止 `fontification' 相关的函数钩子,能让滚动更顺滑
(setq redisplay-skip-fontification-on-input t)

;; 禁止响铃
(setq ring-bell-function 'ignore)

;; 禁止闪烁光标
(blink-cursor-mode -1)

;; 在光标处而非鼠标所在位置粘贴
(setq mouse-yank-at-point t)

;; 拷贝粘贴设置
(setq select-enable-primary nil)        ; 选择文字时不拷贝
(setq select-enable-clipboard t)        ; 拷贝时使用剪贴板

;; 鼠标滚动设置
(setq scroll-step 2)
(setq scroll-margin 2)
(setq hscroll-step 2)
(setq hscroll-margin 2)
(setq scroll-conservatively 101)
(setq scroll-up-aggressively 0.01)
(setq scroll-down-aggressively 0.01)
(setq scroll-preserve-screen-position 'always)

;; 对于高的行禁止自动垂直滚动
(setq auto-window-vscroll nil)

;; 设置新分屏打开的位置的阈值
(setq split-width-threshold (assoc-default 'width default-frame-alist))
(setq split-height-threshold nil)

;; TAB键设置,在Emacs里不使用TAB键,所有的TAB默认为4个空格
(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)

;; yes或no提示设置,通过下面这个函数设置当缓冲区名字匹配到预设的字符串时自动回答yes
(setq original-y-or-n-p 'y-or-n-p)
(defalias 'original-y-or-n-p (symbol-function 'y-or-n-p))
(defun default-yes-sometimes (prompt)
  "automatically say y when buffer name match following string"
  (if (or
	   (string-match "has a running process" prompt)
	   (string-match "does not exist; create" prompt)
	   (string-match "modified; kill anyway" prompt)
	   (string-match "Delete buffer using" prompt)
	   (string-match "Kill buffer of" prompt)
	   (string-match "still connected.  Kill it?" prompt)
	   (string-match "Shutdown the client's kernel" prompt)
	   (string-match "kill them and exit anyway" prompt)
	   (string-match "Revert buffer from file" prompt)
	   (string-match "Kill Dired buffer of" prompt)
	   (string-match "delete buffer using" prompt)
       (string-match "Kill all pass entry" prompt)
       (string-match "for all cursors" prompt)
	   (string-match "Do you want edit the entry" prompt))
	  t
    (original-y-or-n-p prompt)))
(defalias 'yes-or-no-p 'default-yes-sometimes)
(defalias 'y-or-n-p 'default-yes-sometimes)

;; 设置剪贴板历史长度300,默认为60
(setq kill-ring-max 200)

;; 在剪贴板里不存储重复内容
(setq kill-do-not-save-duplicates t)

;; 设置位置记录长度为6,默认为16
;; 可以使用 `counsel-mark-ring' or `consult-mark' (C-x j) 来访问光标位置记录
;; 使用 C-x C-SPC 执行 `pop-global-mark' 直接跳转到上一个全局位置处
;; 使用 C-u C-SPC 跳转到本地位置处
(setq mark-ring-max 6)
(setq global-mark-ring-max 6)

;; 设置 emacs-lisp 的限制
(setq max-lisp-eval-depth 10000)        ; 默认值为 800
(setq max-specpdl-size 10000)           ; 默认值为 1600

;; 启用 `list-timers', `list-threads' 这两个命令
(put 'list-timers 'disabled nil)
(put 'list-threads 'disabled nil)

;; 在命令行里支持鼠标
(xterm-mouse-mode 1)

;; 退出Emacs时进行确认
(setq confirm-kill-emacs 'y-or-n-p)

;; 在模式栏上显示当前光标的列号
(column-number-mode t)
#+end_src

2.3.3 编码设置 🔗

在Emacs世界里,统一使用 utf-8 编码,省得出现各种不可预料的问题。

** 编码设置

统一使用 UTF-8 编码。

#+begin_src emacs-lisp
;; 配置所有的编码为UTF-8,参考:
;; https://thraxys.wordpress.com/2016/01/13/utf-8-in-emacs-everywhere-forever/
(setq locale-coding-system 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(set-selection-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-language-environment 'utf-8)
(set-clipboard-coding-system 'utf-8)
(set-file-name-coding-system 'utf-8)
(set-buffer-file-coding-system 'utf-8)
(prefer-coding-system 'utf-8)
(modify-coding-system-alist 'process "*" 'utf-8)
(when (display-graphic-p)
  (setq x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING)))
#+end_src

2.4 给Emacs的模式栏美化一下 🔗

我们通过 doom-modelineminions 插件来进行模式栏的美化,前者能改变模式栏的样式,好看洋气,后者能让模式栏变得清爽,将次要模式隐藏起来。

** 模式栏设置
*** doom-modeline插件

[[https://github.com/seagle0128/doom-modeline][doom-modeline]] 是一个模式栏美化插件。

#+begin_src emacs-lisp
(use-package doom-modeline
  :ensure t
  :hook (after-init . doom-modeline-mode)
  :custom
  (doom-modeline-irc nil)
  (doom-modeline-mu4e nil)
  (doom-modeline-gnus nil)
  (doom-modeline-github nil)
  (doom-modeline-buffer-file-name-style 'truncate-upto-root) ; : auto
  (doom-modeline-persp-name nil)
  (doom-modeline-unicode-fallback t)
  (doom-modeline-enable-word-count nil))
#+end_src

*** minions插件
[[https://github.com/tarsius/minions][minions]] 插件能让模式栏变得清爽,将次要模式隐藏起来。

#+BEGIN_SRC emacs-lisp
(use-package minions
  :ensure t
  :hook (after-init . minions-mode))
#+END_SRC

2.5 在模式栏上显示按键详情 🔗

我们可以通过 keycast mode 插件在模式栏上展示所有的按键详情(如按键对应的函数):

** keycast按键展示
[[https://github.com/tarsius/keycast][keycast mode]] 插件可以在模式栏上展示所有的按键,以及对应的函数。

#+BEGIN_SRC emacs-lisp
(use-package keycast
  :ensure t
  :hook (after-init . keycast-mode)
  ;; :custom-face
  ;; (keycast-key ((t (:background "#0030b4" :weight bold))))
  ;; (keycast-command ((t (:foreground "#0030b4" :weight bold))))
  :config
  ;; set for doom-modeline support
  ;; With the latest change 72d9add, mode-line-keycast needs to be modified to keycast-mode-line.
  (define-minor-mode keycast-mode
    "Show current command and its key binding in the mode line (fix for use with doom-mode-line)."
    :global t
    (if keycast-mode
        (progn
          (add-hook 'pre-command-hook 'keycast--update t)
          (add-to-list 'global-mode-string '("" keycast-mode-line "  ")))
      (remove-hook 'pre-command-hook 'keycast--update)
      (setq global-mode-string (delete '("" keycast-mode-line "  ") global-mode-string))
      ))

  (dolist (input '(self-insert-command
                   org-self-insert-command))
    (add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))

  (dolist (event '(mouse-event-p
                   mouse-movement-p
                   mwheel-scroll))
    (add-to-list 'keycast-substitute-alist `(,event nil)))

  (setq keycast-log-format "%-20K%C\n")
  (setq keycast-log-frame-alist
        '((minibuffer . nil)))
  (setq keycast-log-newest-first t)
  )
#+END_SRC

具体效果如下图(当我们按下 C-x C-f 时,模式栏上会出现快捷键以及对应的函数 find-file ,非常方便):

3 结语 🔗

至此,我们对Emacs的样貌改造就到一段落了,我们已经可以看到一个拥有好看的配色、和谐的字体的Emacs了,是不是觉得很开心呢?

经过了今天的课程,你的Emacs配置文件大概会像下面这样:第五课的配置文件

你也可以在 这里 查看最新的配置文件。

  1. 在后续的课程里,我将不再贴相关的配置文件,不再贴Org mode的代码块,如果有相关配置代码,仅贴Emacs-lisp的代码块。
  2. 后面每一堂课后,我都会保存一份org配置文件的快照,以 emacs-config-lx.org 的方式来命名,其中 x 代表的第 x 课,与博客标题里的序号是对应的。