Emacs 折腾日记(二十七)——终端管理

Masimaro
2025-07-01 / 0 评论 / 1 阅读 / 正在检测是否收录...

Emacs 号称一个伪装成文本编辑器的操作系统,你几乎可以在Emacs中干任何事情。在Emacs中运行终端自然是小菜一碟。
Emacs中有许多其他类型的shell,例如 eshelltermvetrm

term 终端

其中 term 是Emacs中自带的shell,它最终是调用系统中安装的其他shell环境。我们可以直接使用 M-x 调用 term命令,来选择启用一个系统终端。

term

我们可以在这个终端上进行一些操作。但是还有一些执行细节需要优化。

控制term窗口的显示

首先我们看到在启动终端之后,它占用了一整个窗口,这对于我来说并不友好。一般情况下我只是希望在编写代码之后临时的调用终端执行一些命令,例如编译或者运行代码之类的操作。因此我希望它可以放到下方并且可以快速的关闭退出

我们可以通过 shackle 插件来控制窗口的行为。主要有:方向、大小和弹出方式等等。

配置 shackle 插件最重要的是变量 shackle-rules 。它的构成如下:

 CONDITION(:regexp)            :select     :inhibit-window-quit   :size+:align|:other     :same|:popup
  • CONDITION 是相关的条件,表示我们要定义哪一类窗口的行为。这个条件可以是正则表达式,可以是字符串,可以是mode
  • select 控制弹出窗口后是否选中
  • size: 0到1间的数字,控制窗口宽度和高度占整个窗口区域的百分比
  • inhibit-window-quit: 按q退出时不删除这个缓冲区
  • align: 弹出窗口的对齐方式,取值有 ’left, ‘right, ‘below, ‘above
  • other: 如果当前 frame 有多个 window,是否复用另外一个 window
  • popup: 弹出一个新的 window,而不是复用当前 window
  • same: 不弹出 window,复用当前 window
  • ignore: 禁止显示该窗口

我们当前的配置可以这么写

(use-package shackle
  :ensure t
  :hook (after-init . shackle-mode)
  :custom
  (shackle-default-size 0.5)
  (shackle-default-alignment 'below)
  :config
  (setq shackle-rules
    '((term-mode :regexp t
             :select t
             :size 0.3
             :align t
             :popup t
             :quit t))))

这里表示 term 窗口在弹出之后占据下方30%的区域进行显示,并且退出后直接关闭窗口。

这样我们可以看到使用 term 之后窗口显示在下方,并且像普通buffer那样可以使用vim的命令 :q 来退出

shackle

快速切换显示和隐藏状态

对于终端这种临时窗口,我们一般不会让它长时间驻留在buffer中。想要的时候随时调用、不要的时候随时关闭或者隐藏。对于这种快速打开关闭的需求,我们可以使用 popper 插件。它通过赋予任意缓冲区“弹出”状态,让这些缓冲区不打扰你的核心工作区,只需一键即可调用或隐藏,完美适用于那些需要即时访问但又希望不影响视线的场景。

popper-togglepopper-cyclepopper-toggle-type 是三个核心命令。

  • popper-toggle: 快速切换最新一个弹出缓冲区的显示/隐藏状态
  • popper-cycle: 循环切换所有标记为弹出的缓冲区
  • popper-toggle-type: 切换特定类型缓冲区的显示/隐藏状态

另外它需要通过 popper-reference-buffers 来定义特定类型的缓冲区,插件会将符合这些规则的缓冲区标记为 POP并且进行管理。popper-reference-buffers 支持正则表达式来匹配缓冲区名。

我们可以使用下面简单的配置

(use-package popper
  :ensure t
  :hook (after-init . popper-mode)
  :init
  (setq popper-reference-buffers
    '("\\*Messages\\*"
      "\\*Async Shell Command\\*"
      term-mode
      help-mode
      helpful-mode
      "^\\*eshell.*\\*$"
      eshell-mode
      "^\\*shell.*\\*$"
      shell-mode
      ("\\*corfu\\*" . hide)))
  :config
  (my-leader-def
    "/" #'popper-toggle
    "t" #'popper-cycle
    "T" #'popper-toggle-type)
  )

最终它的效果如下:

popper

eshell

eshellterm 最大的不同在于eshell 是Emacs 自己通过elisp实现的一个仿制的终端,而 term 是真实的系统终端,它调用系统中的shell环境。目前来说使用 term 来打开系统终端能应付大部分的情况,但是学习Emacs,eshell似乎是一个绕不开的坎,并且eshell配置好了,同样好用。

eshell里面实现了普通shell里面常用的命令,例如 lscdcp 等等。同时也可以执行elisp代码,例如可以通过find-file 打开一个文件。或者输入简单的算术表达式像 (+ 1 1) 来进行一些数学计算。

封装shell命令到eshell

eshell里面实现了常见的shell 命令,但是我们在后续会经常性的安装一些好用的工具,来简化我们的工作流程,所以在使用eshell的第一件事就是将shell命令封装到eshell

这里我以 autojump 为例。autojump 是一个快速跳转到指定目录的工具,当你通过cd进入过的目录会被记录下来,下一次通过 autojump 不再需要输入全路径,而是直接模糊匹配进行跳转,例如我们多次进入了 ~/.emacs.d/lisp 目录,下一次不管当前目录在哪里只需要输入 j lisp 即可进入。我们希望复刻这种行为到eshell中。

定义eshell 中的命令可以通过定义一个以 eshell/ 开头的函数来实现,函数名后面的名称就是可以输入eshell的命令名称,所以这里我们要定义一个名为 eshell/j 的函数。同理,我们想要使用eshll中定义好的命令例如 cd ,可以直接调用 eshell/cd 函数。

我们还是使用 use-package 来管理关于 eshell 的配置,最终实现的代码如下:

(use-package eshell
  :ensure nil
  :config
  (defun eshell/j (&rest args)
    (let* ((argstr (mapconcat 'identity args " "))
       (target (shell-command-to-string 
            (concat "autojump " argstr))))
      (setq target (replace-regexp-in-string "\n" "" target))
      (if (file-directory-p target)
      (eshell/cd target)
    (error "目录不存在: %s" target)))))

上述代码主要通过 autojump 命令来根据输入的目录名称来匹配一个实际对应的路径全名,然后通过 eshell/cd 来完成跳转操作。为什么不直接执行 j 来实现跳转呢?eshell是emacs模拟的shell并不是真实的shell,它是与Emacs绑定的,外部的shell命令无法改变 eshell的环境包括它当前所在的目录。因此我们需要调用eshell自身的命令来改变它的环境。

最终的效果如下

eshell 封装j命令

eshell 的 alias

在普通的shell中,我们可以通过 alias 定义命令的别名,例如常用的 ll 或者我喜欢将 vivim 改成 nvim。eshell中也可以设置别名

eshell 中使用 alias 来设置别名,它的语法如下:

alias 别名 '命令 [参数] $@*'

参数部分传入 $1$2 等等,代表传入的第一个、第二个参数,或者直接使用 $@* 表示接受输入的所有参数,例如我们定义一下 ll 这个别名

alias ll 'ls -l $@*'  ; 输入 `ll` 等价于 `ls -l`

eshell 中输入的 alias 命令仅仅在当前 eshell 环境中生效,想要永久生效可以将上述命令写入到 ~/.emacs/eshell/alias 文件中。eshell 会通过函数 eshell-read-aliases-list 加载对应文件中定义的别名,保存别名的文件路径被保存在 eshell-aliases-file 中,它默认的值为 ~/.emacs.d/eshell/alias

eshell history

普通shell中有一个 history 命令可以查看保存的历史,并且可以使用方向键的上和下来输入上一条或者下一条命令。eshell 虽然也能这么干,但是eshell 因为是elisp模拟的终端,背靠Emacs这座大山,它可以配合Emacs的相关插件更高效的利用history命令。这里我们可以配合 consult-history + orderless 来模糊匹配命令

(use-package em-history
  :ensure nil
  :defer t
  :custom
  (eshell-history-size 1024)
  (eshell-his-ignoredups t)
  (eshell-save-history-on-exit t))

(use-package esh-mode
  :ensure nil
  :hook (eshell-mode . (lambda ()
             (local-set-key (kbd "C-r") #'consult-history)))
  :bind (:map eshell-mode-map
          ("C-r" . consult-history))
  :config
  (with-eval-after-load 'evil
            (evil-define-key '(normal insert) eshell-mode-map (kbd "C-r") #'consult-history)))

eshell 直接使用 term 命令

上面我们提到,想要调用shell命令,可以通过编写lisp代码封装进行封装,能否直接使用shell命令呢?答案是可以的,eshell中有 eshell-visual-commandseshell-visual-subcommandseshell-visual-options 这么三个变量来共同决定哪些命令需要启用term 终端.

  • eshell-visual-commands 接受一个字符串的列表,我们输入的命令在这个字符串列表中,那么就会启用term终端来执行命令
  • eshell-visual-subcommands 来处理带子命令的复杂命令,在特定子命令下才需终端模拟
  • eshell-visual-options 根据命令选项动态启用终端模拟。某些命令仅在特定选项(如 -i 交互式模式)出现时才需终端支持

三者优先级:eshell-visual-commands > eshell-visual-subcommands > eshell-visual-options。Eshell 按此顺序检查,匹配任意条件即触发终端模拟。

例如我们常见的 git、man、lazygit 等命令无法使用 eshell 中的 elisp 进行模拟,我们可以通过上述函数来封装这些命令。

(use-package em-term
  :ensure nil
  :defer t
  :custom
  (eshell-visual-commands '("top" "htop" "less" "more" "lazygit"))
  (eshell-visual-subcommands '(("git" "help" "lg" "log" "diff" "show")))
  (eshell-visual-options '(("git" "--help" "--paginate")))
  (eshell-destroy-buffer-when-process-dies t))

结合上述对这些函数的说明,来分析一下这段代码。
首先是 eshell-visual-command 中定义的 top、htop、less、more 这些命令需要完成的term终端支持。当运行这些命令时,Eshell 自动切换到term终端,确保交互式界面正常显示(如分页、高亮等)

eshell-visual-subcommands 为特定命令(如 git)的子命令启用图形终端,也就是说我们执行 git help 时会触发切换到term终端的操作,而执行 git status 时不会

eshell-visual-options 当命令携带特定选项时会切换到term 终端。

下面是在eshell 中执行 lazygit 的界面
eshell lazygit

eshell 美化

上面介绍了eshell的一些基本用法,现在来介绍如何将eshell进行简单的美化。首先介绍的是 eshell-git-prompt 。它提供了好多好看的主题

在安装完插件后,可以在eshell中输入 use-theme 来列出它支持的所有主题,或者输入 use-theme name 来设定一个主题。想要持久化,可以通过函数 eshell-git-prompt-use-theme 来指定一个主题

(use-package eshell-git-prompt
  :ensure t
  :after esh-mode
  :config
  (eshell-git-prompt-use-theme 'powerline))

接着介绍 eshell-syntax-highlighting,它是eshell中语法高亮的插件,它是继承自 zsh 中的 zsh-syntax-highlighting

(use-package eshell-syntax-highlighting
  :after eshell-mode
  :ensure t ;; Install if not already installed.
  :config
  ;; Enable in all Eshell buffers.
  (eshell-syntax-highlighting-global-mode +1))

接下来要介绍的插件是 capf-autosuggest 。它是一个自动补全的插件

(use-package capf-autosuggest
  :ensure t
  :hook ((eshell-mode comint-mod) . capf-autosuggest-mode)
  )

这样基本上就把我在终端上常用的一些插件和操作习惯给挪到eshell中了。当然我常用的还有一个名为 autojump 的插件。对应的 eshell 中也有一个名为 eshell-autojump 的插件。但是我已经通过别名简单的实现了一个eshell 上的 j 命令,这个插件的实现原理也是这样的,这里就不介绍这个插件了。

0

评论 (0)

取消