还以为更新了呢
莫急,正在录视频中,这次会分享一下我是怎么从 vterm
切换至 ansi-term
的,以及我是如何使用 eshell
, shell-mode
的
期待新介绍,后面会讲 wdired 吗?最近在网上看到这么一篇 Working with multiple files in dired - Mastering Emacs 感觉放到前面讲过的 dired 里很不错。
Proced
,类似top,
直接M-x proced
即可,全平台可用,推荐配置:
(setq-default proced-auto-update-flag t ; 自动刷新
proced-auto-update-interval 3 ) ; 默认为5秒一次
viper-mode
Yet another vim emulator,没有visual-mode,ex命令支持不好,会干扰minibuffer的keymap(这个很要命),而且代码耦合严重,想自己改点都很麻烦.
但比evil轻一个量级,和很多内建mode配合得不错,对启动时间敏感又有点空闲时间的人可以花点时间调教一下.
建议看看官方指南
我觉得和内建mode配合得不错,是不是可以把它的代码给 port 到 evil-mode ?
Changelog:
- 2020-12-14 term mode 相关应用
term mode 相关应用
Emacs 下有几个类似终端模拟器(其实有些不算是),内置的有这 3 个: shell-mode
, term-mode
, eshell
。
如果你不喜欢用 M-x compile 来编译,习惯在 shell-mode
, term-mode
, eshell
下直接使用 gcc 或者 make 来编译, 那么你可能需要compilation-shell-minor-mode
。它可以识别报错,令错误可以点击,快速打开报错文件。自然在调用 M-x compile 的就是用的 compilation-mode
了。
term-mode
term-mode
算是一个完整的终端模拟器,与外部的终端模拟器相比除了刷新速度慢、色彩显示较差之外就没有其他差别了。因此如果是在 Linux/MacOS 平台下且只在本地使用,是比较推荐 term-mode
的。term-mode
分别可以通过 term
和 ansi-term
命令启动,唯一的区别是由 term
命令启用的终端模拟器下面 C-x 是直接被终端给捕获了,想要在这个模式下使用 C-x C-f 来打开文件还需要再额外地做设置,而且重复地使用 term
命令只会打开一个 buffer。如果你想多次调用分别打开多个 buffer 的话推荐使用 ansi-term
命令。对我个人而言我是更喜欢 ansi-term
命令。
;; 可以使用这种方式将需要的按键解绑
(use-package term
:ensure nil
:bind (:map term-raw-map
("M-:" . nil)
("M-x" . nil)))
当然在 term-mode
里使用 htop
, git
, fzf
, neofetch
这种类似工具是没啥大问题的,但是使用 vim
的话就有点拉胯了。一是显示效果非常差,代码高亮都无法显示;二是也不推荐在 Emacs 里使用 vim
, 编辑文件直接 C-x C-f 就好。
这里不得不提一下, term-mode
里两种模式,一个是 char-mode
, 另一个是 line-mode
。 在 char-mode
下输入任意一个字符都会直接转发至当前的进程,而 line-mode
下则只会遇到 \n
的时候才会将以前的内容一起转发。就拿 htop
这个命令来说,在 char-mode
下按一下 q
会直接退出,按一下 C-n
会移动光标,但是一旦切换到 line-mode
下后就完全变了,连续地按 q
不会退出,直到你按下 Enter 键。
term-mode
还有一个非常大的优点是与 Emacs 生态的结合。其中一个是可以快速地跳转到上一次的 prompt 处(一般的终端模拟器都没有这个功能),想要启用这功能需要配置 term-prompt-regexp
变量,而它的默认非常不友好,竟然是一个 ^
表示跳转到开头。建议修改成如下配置,毕竟我们要在 term-mode
里使用 shell, 这个配置也是它的注释里所推荐使用的:
(setq term-prompt-regexp "^[^#$%>\n]*[#$%>] *")
这样就可以使用 C-c C-p 和 C-c C-n 来上下跳转 prompt 了。
另外一点是目录同步,如果你在 term-mode
下进入了 /tmp
目录,那么在 Emacs 按 C-x C-f
就会尝试打开此目录下的文件。如果你是 bash 用户那么这个甚至不需要你配置,其他 shell 用户就必须要在对应 shell 的配置里增加如下配置:
# 这是 zsh 需要做的修改
#
# INSIDE_EMACS 则是 Emacs 在创建 term/shell/eshell 时都会带上的环境变量
# 通常 shell/tramp 会将 TERM 环境变量设置成
# dumb,所以这里要将他们排除。
#
# shell 下的目录同步不采用这种方式
function precmd() {
if [[ -n "$INSIDE_EMACS" && "$TERM" != "dumb" ]]; then
echo -e "\033AnSiTc" "$(pwd)"
echo -e "\033AnSiTh" $(hostname -f)
echo -e "\033AnSiTu" "$LOGNAME"
fi
}
其实它就是在每条命令执行前将自己当前的目录告诉了 term-mode
, 然后 term-mode
再设置 default-directory
变量。
另外一种方式则是依赖 Linux 的 procfs
, 可以获得 term-mode
启动的 shell 进程 pid,然后通过读 /proc/pid/cwd/
来获取当前路径。
(defun term-directory-sync ()
"Synchronize current working directory."
(interactive)
(when term-process
(let* ((pid (process-id term-process))
(dir (file-truename (format "/proc/%d/cwd/" pid))))
(setq default-directory dir))))
;; term-process 则是在 term-mode-hook 中通过
;;
;; (get-buffer-process (current-buffer))
;;
;; 获得
;; 注:以上这种方式对于 vterm 同样适用,因为 vterm 直接暴露了 vterm--process 使用
;; 起用更加方便
如果你嫌 term-mode
的刷新速度太慢、颜色显示太差,可以使用 vterm
, 但是它的目录同步方式完全与 term-mode
的不同,这点需要注意。
shell-mode
shell-mode
它实际上不算是一个终端模拟器,它只是简单包装了一下 shell, 所以只能执行一些简单的命令, htop
这种存在复杂交互的应用就不行了。它也支持上下跳转到 prompt 处,而且它的默认值足够通用,如果不适用的话用户再自己配置一下 shell-prompt-pattern
. 通过 C-c C-p 和 C-c C-n 来上下跳转 prompt.
自然 shell-mode
也支持目录同步,不过它的同步方式与 term-mode
不同。 term-mode
是要求 shell 主动告诉 Emacs,而 shell-mode
是启用了 shell-dirtrack-mode
使用正则匹配如 cd
, pushd
等各种可能改变当前目录的各种命令来达到的。
与 term-mode
相比而言它实在是没啥多大优势,但是如果你是在通过 tramp
编辑一个远程的文件,想在远程机器上运行一些命令,可以直接 M-x shell 登录远端的机器,而 term-mode
则不会识别这种情况,仍是创建一个本地的终端环境。在有 tramp
的情况下, shell-mode
下路径显示在 cd
改变了当前工作目录之后会显示出错, PR 的机会又来了!
在 shell-mode
里没法像终端模拟器那样通过 M-. 来直接输入上一命令的最后一个参数,但是多数 shell 都实现提供了一个内部变量 $_
支持。
echo hello
echo $_
# 输出如下
#
# hello
# hello
另外一个独到的地方是它可以当做 sh 文件的 REPL。例如你在编写这样的一个 sh 脚本:
echo 'hello, world'
可以直接输入 C-c C-n (sh-send-line-or-region-and-step
) 将当前行发送至 shell 执行。
eshell
eshell
则是完全由 elisp 实现的 shell,正因为它是 elisp 实现的,所以在所有地方下都可以使用(推荐 Windows 用户使用)。当然也因为它是 elisp 实现的,所以速度上会稍微慢一点。此外如果你是在远程编辑文件,那么使用 eshell
可以直接编辑远程文件,因为它是完全用 elisp 实现的,可以共享当前 Emacs 的状态,自然 tramp 也是可以直接共享的,当然目录同步的更不在话下了。
它的语法与与 bash/zsh 的语法不完全一致,例如一个 for 循环
# 在 bash/zsh 里这样写的
for i in *.el; do
rm $i
done
# 在 eshell 里则是这样写的
for i in *.el {
rm $i
}
在 eshell
里执行类似 htop
, git diff
这样的命令也是可以的,但是它不是直接支持,而是间接调用 term-mode
来完成。只需要将对应的命令加入至 eshell-visual-commands
, eshell-visual-subcommands
和 eshell-visual-options
中即可,建议配置:
(use-package em-term
:ensure nil
:custom
(eshell-visual-commands '("top" "htop" "less" "more" "bat"))
(eshell-visual-subcommands '(("git" "help" "lg" "log" "diff" "show")))
(eshell-visual-options '(("git" "--help" "--paginate"))))
因为 eshell
不能复用其他 shell 的插件,所以 eshell
有自己的生态,可以考虑使用 eshell-git-prompt
等包。
eshell
还有一个最大的缺点是补全系统。其他 shell 都有自己的 bash-completion
, zsh-completion
包,但是 eshell 却没有,但是它只提供了一个基础的补全功能模块 pcomplete
. 通过它我们也可以完成基础命令的补全,但是如果想要全部实现的话还是得费一番功夫的。基于这个痛点,Emacs 社区有相应的增强包 emacs-fish-completion, 在补全时将对应的命令发送给 fish 然后再截获、解析它的输出。以这种形式扩展的 pcomplete 不用再重复走 bash-completion
, zsh-completion
的路。
因为 eshell
与 shell-mode
都使用了 pcomplete, 所以这两者都能够享受到由此带来的补全效果。
默认情况下,在 eshell
里 C-d 只会删除字符不会在当前输入为空时退出、 M-. 不会自动插入上一命令的最后一个参数,这可能会令习惯使用外部 shell 的用户非常不习惯,可以通过如下配置将它们带回来。
;; eshell 自己有实现的一个比较好的 C-d 函数,但是它默认没有开启
;; 这里显式地将这个函数导出。
(use-package em-rebind
:ensure nil
:commands eshell-delchar-or-maybe-eof)
;; Emacs 28 可以直接定义在 eshell-mode-map 里,但是 27 的话需要将相关的键绑定定义在
;; eshell-first-time-mode-hook 这个 hook 里
(use-package esh-mode
:ensure nil
:bind (:map eshell-mode-map
("C-d" . eshell-delchar-or-maybe-eof)
("M-." . eshell-yank-last-arg))
:config
(defun eshell-yank-last-arg ()
"Insert the last arg of the previous command."
(interactive)
(insert "$_")
(pcomplete-expand))
)
关于 $_
的说明可以看 eshell 文档的 Expansion 节。 eshell 这样设计也与其他 shell 保持一致。
最近正在琢磨 Emacs 里的终端怎么用比较好,这次更新太涨姿势了!
施工完成了!
官方文档靠后的一些章节实用性比较高,比如
- Dealing with Emacs Trouble
- Abbrevs
- Merging Files with Emerge
- Customizing VC
- Maintaining Large Programs
里面的内容也很充实,就是感觉文字有些多(看不动。。),需要个班长来给我们划重点,感觉楼主可以胜任。
学习了,不过 !$
按起来有点不太方便,!!
还可以接受。
记下了!
扫了一遍,发现只是蜻蜓点水了一下……
我搞了好久才发现char-mode用term-raw-map
,line-mode用term-mode-map
,之前怎么bind key都不对劲。
对的,实际上 line-mode 不通用,只使用 char-mode 就好了。
我感觉 term-line-mode
可以改造一下,或者抄一下做一个新的 mode,启用的时候把 term 变成一个只读的普通 buffer,这样就可以用普通的快捷键浏览和复制东西了。
这个与 tmux 的那个浏览模式很像,我觉得可以再加个 copy-mode。如果是用 evil 的话那这个就显得有点多余了
evil-normal-state的时候char-mode会阻止光标移出当前命令的编辑区,要用line-mode才行。
(general-define-key
:states '(insert emacs)
:keymaps 'term-raw-map
"<escape>" (lambda! (term-line-mode) (evil-motion-state)))
(general-define-key
:states '(normal motion)
:keymaps 'term-mode-map
"a" (lambda! (evil-insert-state) (term-char-mode)))
mumu 的视频在哪呢?
参照楼主的教学,写了一个简单的配置。亮点:
-
my-term
命令:和term
差不多,但不会在开启时要你选一下 shell 程序(还有一些细节,具体看代码)。 -
my-term-yank
:可以直接在 char mode 下粘贴文本,比如用来粘贴复制来的命令或者路径。 -
my-term-browse-mode
:把终端变成一个只读的、可以浏览的、键位和平时一样的 buffer,方便我们浏览一个很长的输出或者复制东西。
用法:把下面的代码复制到你的配置中,阅读并修改 “Example config" 部分。用 M-x my-term
启动终端。
(require 'term)
;;; term.el tweaks
(defvar my-term-escape-keys
'("M-x")
"Escape keys for `my-term'.
Notice that if you enable minor modes in the term, keys defined
by them override `term-raw-map', so they also behave like they
are escaped.")
(defun my-term-remove-escape-char ()
"Undo changed by `term-set-escape-char'."
(when term-escape-char
(define-key term-raw-map term-escape-char 'term-send-raw)
(setq term-escape-char nil)))
(defun my-term-setup-escape-keys ()
"Set keys in `my-term-escape-keys' as escape chars in term."
(dolist (key my-term-escape-keys)
(define-key term-raw-map (kbd key) nil)))
(with-eval-after-load 'term
;; In term.el, `C-c' is explicitely escaped, in the top level scope, using
;; `term-set-escape-char'. It's a useless API because it undoes its previous
;; call, so you can't escape multiple chars (`ansi-term' works around it
;; using a hack). We undo it here because we have `my-term-escape-keys'
;; where you can specify more than 1 escaped keys.
(my-term-remove-escape-char)
(my-term-setup-escape-keys))
;;; `my-term' command
(defvar my-term-shell-program nil
"Shell program for `my-term'.
We need this because we may use a lightweight shell like dash for
inferior shell, but want bash/zsh for interactive shell.")
(defun my-term ()
"Start a terminal emulator in a new buffer.
This is like `term' but with several tweaks to make you happier."
(interactive)
(let ((prog (or my-term-shell-program
explicit-shell-file-name
(getenv "SHELL")
shell-file-name
(read-file-name "Shell executable: "
"/" nil t)))
(buf (generate-new-buffer "*term*")))
;; If the user calls `ansi-term' before, undo its call to
;; `term-set-escape-char'.
(when term-escape-char
(my-term-remove-escape-char)
(my-term-setup-escape-keys))
(with-current-buffer buf
(term-mode)
(term-exec buf (buffer-name) prog nil nil)
(term-char-mode))
(pop-to-buffer buf)))
;;; Helpers
(defvar my-term-browse-mode-map
(make-sparse-keymap)
"Keymap for `my-term-browse-mode'.")
(defun my-term-browse-mode ()
"Turn the terminal buffer into a read-only normal buffer."
(interactive)
;; Workaround: Without this code, there's a bug: Press `C-p' in char mode to
;; browse history, then `C-n' to go back, then `my-term-browse-mode', then
;; `C-n', you'll find a newline is produced. Call `term-char-mode', that
;; newline is sent to the shell. This is not a problem with
;; `my-term-browse-mode', since `term-line-mode' also has it.
(let ((inhibit-read-only t))
(save-excursion
(goto-char (point-max))
(while (eq (char-before) ?\n)
(delete-char -1))))
;; Idea: We could put a `read-only' property to the region before
;; `process-mark', so current input could be edited, but I think there's
;; little benefit.
(setq buffer-read-only t)
(remove-hook 'pre-command-hook #'term-set-goto-process-mark t)
(remove-hook 'post-command-hook #'term-goto-process-mark-maybe t)
(use-local-map my-term-browse-mode-map))
(defun my-term-yank ()
"Paste recent kill into terminal, in char mode."
(interactive)
(when-let ((text (current-kill 0))
;; Remove newlines at the beginning/end.))
(text (string-trim text "\n+" "\n+")))
(when (or (not (string-match-p "\n" text))
(y-or-n-p "You are pasting a multiline string. Continue? "))
(term-send-raw-string text))))
;;; Example config
;; Set this to your shell program, or delete this if you just want to use the
;; inferior (reads "default") shell.
(setq my-term-shell-program "/bin/bash")
;; Keys you don't want them to be captured by the terminal.
(setq my-term-escape-keys '("C-c" "C-x" "M-x"))
;; Call this after you set `my-term-escape-keys'
(my-term-setup-escape-keys)
;; Bind keys to use in char mode. Make sure you have the prefix key (`C-c' in
;; this example) in `my-term-escape-keys'.
(define-key term-raw-map (kbd "C-c b") 'my-term-browse-mode)
(define-key term-raw-map (kbd "C-M-v") 'my-term-yank)
;; Bind keys to use in browse mode.
(define-key my-term-browse-mode-map (kbd "C-c b") 'term-char-mode)
实际录到一半不想录了)