vterm 里打开 dired 或者编辑文件应该怎么做?

问一个比较SB的问题:

现在 emacs 中的主力终端换成 vterm 了,在 vterm 里用什么办法能打开当前目录的 dired buffer呢?或者用当前 emacs 进程打开文件编辑呢?(除了用 emacsclient)

readme里有详细说明

## Message passing

`vterm` can read and execute commands. At the moment, a command is
passed by providing a specific escape sequence. For example, to evaluate
``` elisp
(message "Hello!")

use

printf "\e]51;Emessage \"Hello\!\"\e\\"

The commands that are understood are defined in the setting vterm-eval-cmds.

As split-string-and-unquote is used the parse the passed string, double quotes and backslashes need to be escaped via backslash. For instance, bash can replace strings internally.

vterm_cmd() {
    printf "\e]51;E"
    local r
    while [[ $# -gt 0 ]]; do
        r="${1//\\/\\\\}"
        r="${r//\"/\\\"}"
        printf '"%s" ' "$r"
        shift
    done
    printf "\e\\"
}

However if you are using dash and need a pure POSIX implementation:

vterm_cmd() {
    printf "\e]51;E"
    while [ $# -gt 0 ]; do
        printf '"%s" ' "$(printf "%s" "$1" | sed -e 's|\\|\\\\|g' -e 's|"|\\"|g')"
        shift
    done
    printf "\e\\"
}

Now we can write shell functions to call the ones defined in vterm-eval-cmds.

find_file() {
    vterm_cmd find-file "$(realpath "$@")"
}

say() {
    vterm_cmd message "%s" "$*"
}

This can be used inside vterm as

find_file name_of_file_in_local_directory

As an example, say you like having files opened below the current window. You could add the command to do it on the lisp side like so:

(push (list "find-file-below"
            (lambda (path)
              (if-let* ((buf (find-file-noselect path))
                        (window (display-buffer-below-selected buf nil)))
                  (select-window window)
                (message "Failed to open file: %s" path))))
      vterm-eval-cmds)

Then add the command in your .bashrc file.

open_file_below() {
    vterm_cmd find-file-below "$(realpath "$@")"
}

Then you can open any file from inside your shell.

open_file_below ~/Documents
1赞

或许可以尝试命令行提示符获得当前路径,然后直接用 C-x C-f。我试了试在 M-x ansi-term 下这么处理:

(defun ansi-term-find-file ()
  (interactive)
  (let ((default-directory
          (or
           ;; 对命令行提示符格式有要求:绝对路径 + 空格开头,如
           ;; ~/.emacs.d/lisp $ 
           (save-excursion
             (goto-char (line-beginning-position))
             (and-let* ((f (thing-at-point 'filename))
                        ((file-exists-p f))
                        ((file-directory-p f))
                        (f (expand-file-name f))
                        (d (file-name-as-directory f)))
               d))
           default-directory)))
    (call-interactively #'helm-find-files)))

(bind-key "C-x C-f" #'ansi-term-find-file term-mode-map)
(bind-key "C-x C-f" #'ansi-term-find-file term-raw-map)

用 emacsclient 也挺方便,开了 Emacs Server 就 OK 了。

1赞

直接 C-x dC-x C-f 不就是打开 vterm 所处的路径吗?

说的好有道理,可是有些人就喜欢敲 vi filename 在emacs中打开:joy:

还是不完全一样,比如vterm下随便cd到别处,但vterm buffer 的 default-directory还是初始化时的路径

1赞

这种要改 vterm 代码,终端模拟器一般会在路径变化的时候发出信号,这时候需要添加回调改变 emacs buffer default-directory 的值

2赞

楼上二位准确地说出了我的痛点

人家本来就支持啊:

Directory tracking

vterm supports directory tracking . If this feature is enabled, the default directory in Emacs and the current working directory in vterm are synced. As a result, interactive functions that ask for a path or a file (e.g., dired or find-file ) will do so starting from the current location.

1赞

默认没打开?

首页那个文档, 你找到这一段, 接着往下看, 有开启方法

M-x shel 可以,M-x term 不行。可以想办法通知 Emacs 更新工作路径,比如利用 shell 更新提示符时调用 emacsclient,不过应该比较折腾,可能还会提高 shell 的延迟。我上面的方式是直接从 PROMPT 中猜,优点是简单,缺点是必须用特定格式的 PROMPT,而且不能有命令在运行。

抱歉没仔细看文档 :joy:。而且这个设置不是在 emacs 里设(在 bashrc 里设):

vterm_prompt_end(){
    printf "\e]51;A$(whoami)@$(hostname):$(pwd)\e\\"
}
PS1=$PS1'$(vterm_prompt_end)'

亲测可行,感谢各位提供帮助,特别是 @netjune 指出了文档中已有解决的方法。(本来是勾选 @netjune 的回复作为答案,但是为了方便后面的同学,所以直接把这个粘贴了代码段的楼层作为答案。)

1赞

evil 用户还得再等等。

我大致看了一下,vterm.el 目前只能发送按键信息给 vterm-module:

;; elisp
(vterm--update vterm--term key shift meta ctrl)

;; c
env->copy_string_contents(env, args[1], (char *)key, &len);
VTermModifier modifier = VTERM_MOD_NONE;
if (env->is_not_nil(env, args[2]))
  modifier = modifier | VTERM_MOD_SHIFT;
if (env->is_not_nil(env, args[3]))
  modifier = modifier | VTERM_MOD_ALT;
if (env->is_not_nil(env, args[4]))
  modifier = modifier | VTERM_MOD_CTRL;

也就是说无法通知 vterm 删除一个 region,所以 evil normal 状态那些快捷操作都不行:

2赞

如果可以,实在是不想动.zshrc。因为用的grml-zsh-config,会重置PROMPT变量,导致在.zshrc里设置之后会被覆盖。于是vterm推荐的那种根据prompt来识别的方案就会失效。

一个想法:

但是为什么不通过shell-dirtrack-mode来实现目录同步呢?这是通过监测cd, pushd, popd这种会更改目录的命令来实现的。这个后面是依托于comint-mode (command-interpreter-in-a-buffer) 来实现的。如果vterm-mode继承了comint-mode,是不是就可以令shell-dirtrack-mode生效了?


以上想法已经验证失败。

目前解法是在C-x C-f的时候同步一下目录。因为用到了procfs,所以是linux限定。 不清楚mac能不能用。

(use-package vterm
  :ensure t
  :custom
  (vterm-kill-buffer-on-exit t)
  (vterm-clear-scrollback-when-clearing t)
  :bind (:map vterm-mode-map
         ("C-x C-f" . (lambda (&rest _)
                        (interactive)
                        (my/vterm-directory-sync)
                        (call-interactively 'counsel-find-file))))
  :config
  ;; Directory synchronization (linux-only)
  (defun my/vterm-directory-sync ()
    "Synchronize current working directory."
    (when vterm--process
      (let* ((pid (process-id vterm--process))
             (dir (file-truename (format "/proc/%d/cwd" pid))))
        (setq-local default-directory dir))))
  )

演示动画

理论上 你可以这样用, 但是这样有个副作用, 无法正确定位PROMPT的结束位置, vterm–at-prompt-p vterm-beginning-of-line 等函数会有bug

        autoload -U add-zsh-hook
        add-zsh-hook -Uz chpwd (){
            vterm_printf "51;A$(whoami)@$(hostname):$(pwd)"
        }