从 Emacs 向外部终端 Konsole 发送命令 (C-x C-e 到终端)

由于包括 Emacs 在内的众多编辑器和 IDE 的内嵌终端都在边缘情况有各种各样的问题,我都是拉起一个真正的终端来用,正好少一个 buffer 也不用调整 Emacs 的 buffer 布局。(cURl 的作者 daniel 也是用 Emacs + 外部终端写代码的)

对我来说,拉起一个外部终端只有一个场景满足不了,那就是 REPL,准确来说是用 C-x C-e 执行一小段代码。

研究了一下,发现 KDE 的 konsole 有一套从上古时代就支持的 dbus API。只需几行 elisp 就可以直接向 Konsole 发送命令。

演示

Peek 2023-11-15 04-56

原理

Konsole 启动会创建 dbus service → org.kde.konsole

每个创建的窗口对应一个 object path → /Windows/{1,2,3,4...} 每个窗口中的标签对应一个 object path → /Sessions/{1,2,3,4,5...}

主要用到的 method → org.kde.konsole.SessionsendTextrunCommand

代码

最终会暴露出几个命令

  • konsole-new-session-for-buffer 用当前 buffer 的目录打开一个新的标签
  • (konsole-run-cmd-in-active-session str) 发送 str 到 Konsole 活跃的标签
  • konsole-run-line 发送当前行
;;; -*- lexical-binding: t; -*-
(require 'dbus)

;; dbus object paths
(defvar konsole-window-path "/Windows/1")
(defvar konsole-mainwindow-path "/konsole/MainWindow_1")

;; Internals

(defun konsole--get-default-profile ()
  (dbus-call-method
   :session
   "org.kde.konsole"
   konsole-window-path
   "org.kde.konsole.Window"
   "defaultProfile"))

(defun konsole--get-active-session ()
  (dbus-call-method
   :session
   "org.kde.konsole"
   konsole-window-path
   "org.kde.konsole.Window"
   "currentSession"))

(defun konsole--raise ()
  "raise window, which may not work on Wayland."
  (dbus-call-method
   :session
   "org.kde.konsole"
   konsole-mainwindow-path
   "org.qtproject.Qt.QMainWindow"
   "raise")
  )

;; public

(defun konsole-run-cmd-in-active-session (cmd)
  (dbus-call-method
   :session
   "org.kde.konsole"
   (concat "/Sessions/" (number-to-string (konsole--get-active-session)))
   "org.kde.konsole.Session"
   "runCommand"
   cmd))

(defun konsole-run-cmd-between (begin end)
  (konsole-run-cmd-in-active-session
   (buffer-substring-no-properties begin end)))

;; interactive uses

(defun konsole-new-session-for-buffer ()
  "Create a new tab and set its path to current buffer's path"
  (interactive)
  (dbus-call-method
   :session
   "org.kde.konsole"
   konsole-window-path
   "org.kde.konsole.Window"
   "newSession"
   (konsole--get-default-profile)
   (file-name-directory (buffer-file-name)))
  (konsole--raise))

(defun konsole-run-region (begin end)
  (interactive "r")
  (konsole-run-cmd-in-active-session
   (buffer-substring-no-properties begin end)))

(defun konsole-run-line ()
  (interactive)
  (konsole-run-cmd-in-active-session
   (buffer-substring-no-properties
    (line-beginning-position)
    (line-end-position))))

例子, 发送上一个 sexp

(defun +eval-last-sexp-in-konsole ()
  (interactive)
  (save-excursion
    (let* ((end (point))
           (begin (progn
                    (backward-sexp)
                    (point))))
      (konsole-run-cmd-between begin end))))

通过 OCaml 的 tuareg mode 获取指针附近的代码并发送

(defun +eval-tuareg-phrase-in-konsole ()
  (interactive)
  (let ((phrase (tuareg-discover-phrase)))
    (unless phrase
      (user-error "Expression after the point is not well braced"))
    (let ((begin (car phrase))
          (end (cadr phrase)))
      (konsole-run-cmd-between begin end))))

4 个赞

另外这个功能用 Kitty 实现可能会更简单一些,它有专门的远程控制命令,不需要 dbus

https://sw.kovidgoyal.net/kitty/overview/#remote-control


iTerm2 有 Python API + AppleScript 和 Coprocesses

有异曲同工的帖子 用 iTerm2 Python API 从 Emacs 给 iTerm2 发送命令

https://iterm2.com/documentation-coprocesses.html

1 个赞

在emacs 里给 kitty 发命令

(shell-command-to-string "kitten @ send-text --match cmdline:cat emacs")

会遇到下面的错误 “Error: open /dev/tty: no such device or address”,请问你遇到过吗

默认状态下 kitty 的 kitten 好像只能 kitty 进程内部通讯。要从外部控制需手动指定一个传送命令的 socket 地址

启动的时候加上 --listen-to

kitty -o allow_remote_control=yes --listen-on unix:/tmp/mykitty

发送到同一地址 --to

(shell-command-to-string "kitten @ --to unix:/tmp/mykitty send-text ls\r")

https://sw.kovidgoyal.net/kitty/remote-control/#remote-control-via-a-socket

确实是,非常感谢。

可以在 kitty.conf 文件中添加下面配置,默认使能 socket 远程控制

allow_remote_control yes
listen_on unix:/tmp/mykitty

kitty 会自动在 /tmp/mykitty 名称后添加 kitty 的 pid,可以用下面的命令从 emacs 发送到 kitty

(defun my--send-region-to-kitty ()
  (interactive)
  (shell-command-on-region
   (if (use-region-p) (region-beginning) (point-min))
   (if (use-region-p) (region-end) (point-max))
   "kitty @ --to unix:/tmp/mykitty-`pgrep kitty` send-text --stdin"))