dict-line.el 一个离线的查词工具.

Emacs 是有一个完全由 emacs-lisp 实现的 sdcv.el … :wave:

我设想的是这样,dict-line可以获取光标附近的单词,这点是goldendict等软件,在emacs这个界面里暂时做不到的,所以,我希望dict-line能把获取到的单词放在系统的剪贴板里,然后goldendict有快捷键可以从剪贴板取词,因此,可以另外用一个程序在检测到剪贴板里是英语单词时,或者dict-line 去触发这样的快捷键,这样就不需要goldendict的命令行接口什么的了。

好吧, 你赢了…
我感觉这样的体验很不好, 也很奇怪

没事,我这人的爱好就是从多个角度琢磨一个问题,并没有抬杠的意思,如有冒犯请多包涵。

哦, 我没那个意思, 怪我没表达清楚 :grinning:
你这个goldendic用法需要启动外部程序goldendic窗口, 启动goldendic后窗口的焦点就不是Emacs了, 所以体验不好

我是用欧路词典和notepad2来尝试,欧路词典的划词翻译的快捷键是 Ctrl+F7,欧路词典启动后,然后我在 notepad2 里输入英语单词,并选中整个单词,然后按 Ctrl+F7 ,出现翻译的浮动框,而这时焦点还在 notepad2 里。所以应该说我不是启动词典,这样会失去焦点,而是使用词典的划词翻译功能。

我试了试,让 kimi 来帮我实现这个代码:

(provide 'call-external-dictionary-mode)

;; 定义一个 minor mode
(define-minor-mode call-external-dictionary-mode
  "Toggle Call External Dictionary mode.
With ARG, turn Call External Dictionary mode on if ARG is positive, off otherwise.
Call External Dictionary mode is a minor mode that automatically copies the word at point to the clipboard."
  :lighter "ExtDict"
  :keymap (let ((map (make-sparse-keymap)))
            map)
  ;; 当模式启用时,添加钩子
  (if call-external-dictionary-mode
      (add-hook 'post-command-hook 'copy-word-to-clipboard nil t)
    ;; 当模式禁用时,移除钩子
    (remove-hook 'post-command-hook 'copy-word-to-clipboard t)))

;; 定义一个函数,复制当前单词到系统剪贴板
(defun copy-word-to-clipboard ()
  "Copy the word at point to the system clipboard."
  (interactive)
  (let ((word (thing-at-point 'word)))
    (when word
      ;; 根据操作系统执行不同的剪贴板复制操作
      (cond
       ((eq system-type 'windows-nt) ; Windows系统
        (w32-set-clipboard-data word 'CF_UNICODETEXT))
       ((memq system-type '(gnu/linux darwin)) ; Linux或macOS系统
       ;; 使用xclip命令复制单词到剪贴板
       (call-process "xclip" nil nil nil "--selection" "clipboard" nil word)
       (message "Word '%s' copied to clipboard." word))))))

;; 提供一个命令来手动启用或禁用 call-external-dictionary-mode
(defun toggle-call-external-dictionary-mode ()
  "Toggle call-external-dictionary-mode."
  (interactive)
  (call-external-dictionary-mode (if call-external-dictionary-mode -1 1)))

然后在配置文件中:

(require 'call-external-dictionary-mode)
(call-external-dictionary-mode 1)
(global-set-key (kbd "C-c w") 'toggle-call-external-dictionary-mode)

可以做到在 windows 下,把鼠标点到哪个单词,就复制到系统剪贴板,然后欧路词典设置剪贴板取词,就出现单词翻译的浮动框。

然而有两个毛病,一是只能在 windows 下起作用,linux 下的的代码虽然有,但是不起作用,而且我已经安装了 xclip 并且测试下来,xclip 生效,在 emacs 中复制单词,欧路也能跳出来,现在就是 emacs 自己不能做到把单词取出来,所以不能复制到系统剪贴板。二是在 windows 下打开 emacs 后,还是要手动切换到让模式生效,才能使用。

你看下你的 Emacs 有没有绑定了 Ctrl + F7 ??
我记得外部软件的全局按键映射优先级比较高的, 比如我的 Emacs 绑定了 ctrl + alt + num5 外部其他软件也绑定这个按键, 在 Emacs 软件内调用这个按键绑定时, 触发的是外部软件的而不是 Emacs 的, 相当与 Emacs 的按键被截止了没有按下这个组合键.

还是说在 Emacs 选中文本后可以使用 Ctrl + F7 按键, 但是没有办法将文本内容复制给系统?

噢噢, 我理解了你的用途了, 也明白了你的困境…

emacs 不给我重新设定 Ctrl+F7 的机会,怎么都是报错 F7 不是一个字符

C-<f7> 默认是为空的, 你应该绑定到别处去了
我建议不要把时间浪费在这些地方上了, 你直接复制内容再打开其他软件用吧, 你想想你浪费的时间都可以进行多少次这样的操作了

就是啊,所以我上面贴的代码就是自动复制然后正好触发欧路的剪贴板取词。

花了点时间,在 linux 和 windows 下都实现了使用外部欧路词典的剪贴板取词功能,并且可以用 C-c C-d 进行切换,linux 下默认是 posframe 优先,windows 下默认是欧路词典优先,因为欧路词典是剪贴板取词,这样会把剪贴板弄乱,所以可以在不需要的时候,切换到 posframe ,代码贴一下,抛砖引玉。

;; -*- coding: utf-8; -*-

;;; dict-line --- View dict in Emacs.  -*- lexical-binding: t; -*-

;; Copyright (C) 2024 Free Software Foundation, Inc.
;; License: GPL-3.0-or-later

;; Author: ISouthRain
;; Version: 0.6
;; Package-Requires: ((emacs "24.2") (async "1.8") (posframe "1.0.0"))
;; Keywords: dict sdcv
;; URL: https://github.com/ISouthRain/dict-line

;;; Commentary:
;;
;; This package is quickly view git blame information of the current file line in Emacs in real time.

;;; Code:
(require 'async)
(require 'posframe)

(defgroup dict-line nil
  "Emacs dictionary lookup on cursor movement."
  :group 'tools)

(defcustom dict-line-dict-directory "~/my-dict/"
  "The directory where .ts dictionary files are stored."
  :type 'directory
  :group 'dict-line)

(defcustom dict-line-dict-personal-file "~/my-dict/my-dict.ts"
  "Personal dict file"
  :type 'string
  :group 'dict-line)

(defcustom dict-line-audio nil
  "Toggle play audio file."
  :type 'boolean
  :group 'dict-line)

(defcustom dict-line-audio-root-dir "~/my-dict/my-audio/"
  "The directory where audio files are stored."
  :type 'directory
  :group 'dict-line)

(defcustom dict-line-audio-play-program "mplayer"
  "Play audio file program.
List: `mplayer`, `mpg123`, `mpv`"
  :type 'string
  :group 'dict-line)

(defcustom dict-line-audio-play-program-arg "-volume 100"
  "Audio play program arguments.
Default example: play volume 100%"
  :type 'string
  :group 'dict-line)

(defcustom dict-line-idle-time 0.5
  "Idle time in seconds before triggering dictionary lookup."
  :type 'number
  :group 'dict-line)

(defcustom dict-line-toggle-display-method-key "C-c C-d"
  "Key binding for toggling the display method in `dict-line-mode`."
  :type 'string
  :group 'dict-line)

(defvar dict-line-word nil
  "dict-line point word.")

(defvar dict-line-dict nil
  "dict-line result dict txt.")

(defvar dict-line--current-buffer nil
  "dict-line word current buffer name.")

(defvar dict-line--posframe-buffer "*dict-line-posframe*"
  "dict-line show dict txt buffer.")

(defcustom dict-line-posframe-location #'posframe-poshandler-point-bottom-left-corner
  "The location function for displaying the dict-line posframe.
Choose from a list of `posframe` position handlers to control where
the posframe appears relative to the frame, window, or point.
Source for `posframe-show` (2) POSHANDLER:
1.  posframe-poshandler-frame-center
2.  posframe-poshandler-frame-top-center
3.  posframe-poshandler-frame-top-left-corner
4.  posframe-poshandler-frame-top-right-corner
5.  posframe-poshandler-frame-top-left-or-right-other-corner
6.  posframe-poshandler-frame-bottom-center
7.  posframe-poshandler-frame-bottom-left-corner
8.  posframe-poshandler-frame-bottom-right-corner
9.  posframe-poshandler-window-center
10. posframe-poshandler-window-top-center
11. posframe-poshandler-window-top-left-corner
12. posframe-poshandler-window-top-right-corner
13. posframe-poshandler-window-bottom-center
14. posframe-poshandler-window-bottom-left-corner
15. posframe-poshandler-window-bottom-right-corner
16. posframe-poshandler-point-top-left-corner
17. posframe-poshandler-point-bottom-left-corner
18. posframe-poshandler-point-bottom-left-corner-upward
19. posframe-poshandler-point-window-center
20. posframe-poshandler-point-frame-center"
  :type '(choice (const nil)
                 function)
  :group 'dict-line)


(defcustom dict-line-display #'dict-line--message
  "dict-line to display function."
  :type '(choice (const nil)
                 function)
  :group 'dict-line)

(defun dict-line--message ()
  "dict-line display function."
  (dict-line--dict-convert)
  (message dict-line-dict))

(defun dict-line--posframe ()
  "Show translation in the posframe"
  (dict-line--dict-convert)
  (when (posframe-workable-p)
    (posframe-show dict-line--posframe-buffer
                   :string dict-line-dict
                   :max-width 30
                   :left-fringe 5
                   :right-fringe 5
                   :poshandler dict-line-posframe-location
                   :border-width 5;; 外边框大小
                   :border-color "#ed98cc" ;; 边框颜色
                   )
    )
  )

(defun dict-line--posframe-delete ()
  "Delete the posframe associated with BUFFER if it exists."
  (when (eq dict-line-display #'dict-line--posframe)
    (posframe-hide dict-line--posframe-buffer))
  )

(defun dict-line--dict-convert ()
  "dict-line convert dict txt."
  (setq dict-line-dict (replace-regexp-in-string "\\\\\\\\n" "\n" dict-line-dict))
  (setq dict-line-dict (replace-regexp-in-string "\"," "\" " dict-line-dict))
  (setq dict-line-dict (substring dict-line-dict 1 -2))
  )

(defun dict-line-toggle-display-method ()
  "Toggle between using posframe and clipboard for displaying dictionary results."
  (interactive)
  (setq dict-line-use-clipboard (not dict-line-use-clipboard))
  (message "Dict-line display method toggled: %s" (if dict-line-use-clipboard "clipboard" "posframe")))

(defun dict-line--copy-word-to-clipboard (word)
  "Copy WORD to the system clipboard."
  ;(if (not word)
   ;   (message "No word to copy to clipboard.")
   ; (message "Copying word to clipboard: %s" word)  ; Debug message
    (cond
     ((eq system-type 'windows-nt)
      (w32-set-clipboard-data word 'CF_UNICODETEXT))
     ((memq system-type '(gnu/linux darwin))
    ;  (if (stringp word)
          (let ((command (format "echo '%s' | xsel --clipboard --input" word)))
            (shell-command command)))))
     ;       (message "Copied to clipboard: %s" word))))
     ;(t (message "Unsupported operating system.")))))

(defun dict-line--get-dict-async ()
  "Check the word under cursor and look it up in the dictionary asynchronously."
  (let ((word (if (use-region-p) ;; Check if there is a selected area
                  (buffer-substring-no-properties (region-beginning) (region-end)) ;; Use selected text
                (thing-at-point 'word t))) ;; Otherwise use the word under the cursor
        (buffer (get-buffer (buffer-name)))
        (dir dict-line-dict-directory)) ;; Extract dictionary directory
    (when word
      (message "Word at point: %s" word) ;; Debug message
      (setq dict-line-word word)
      (setq dict-line--current-buffer (get-buffer (buffer-name)))
      (if dict-line-use-clipboard
          (dict-line--copy-word-to-clipboard word)
      (async-start
       `(lambda ()
          (let ((dict-files (directory-files ,dir t "\\.ts$"))
                (dicts nil))
            (while (and dict-files (not dicts))
              (with-temp-buffer
                (insert-file-contents (car dict-files))
                (goto-char (point-min))
                (when (search-forward (concat "\"" ,word "\":") nil t)
                  (setq dicts (buffer-substring-no-properties (point) (line-end-position)))))
              (setq dict-files (cdr dict-files)))
            dicts))
       ;; Callback
       (lambda (dicts)
         (when dicts
           (setq dict-line-dict dicts)
           (with-current-buffer (get-buffer-create dict-line--current-buffer)
             (when (functionp dict-line-display)
               (funcall dict-line-display)))
           )
         ;; Play audio
         (when dict-line-audio
           (let* ((first-letter (upcase (substring dict-line-word 0 1))) ;; Get the first letter of the word
                  (audio-file (concat dict-line-audio-root-dir first-letter "/" dict-line-word ".mp3"))
                  (program dict-line-audio-play-program)
                  (args (append (split-string dict-line-audio-play-program-arg) (list audio-file)))) ;; Combine arguments
             (when (file-exists-p audio-file)
               (let ((process (apply #'start-process "dict-line" nil program args)))
                 ;; Automatically terminate playback after x seconds
                 (run-at-time "1 sec" nil
                              (lambda (proc)
                                (when (process-live-p proc)
                                  (kill-process proc)))
                              process))))
           ))
         ))
      ))
  )

;;;###autoload
(defun dict-line-word-save-from-echo ()
  "Extract the word under the cursor, prompt the user to enter information, and then save 'word': 'Input information' to the last line of the specified file."
  (interactive)
  (let* ((word (thing-at-point 'word t))
         (input (read-string (format "Enter information for '%s': " word)))
         (entry (format "\"%s\":\"%s\"," word input)))
    (when (and word input)
      (with-temp-buffer
        (insert-file-contents dict-line-dict-personal-file)
        (goto-char (point-max))
        (insert (concat "\n" entry))
        (write-region (point-min) (point-max) dict-line-dict-personal-file))
      (message "Save %s to %s" entry dict-line-dict-personal-file))))

;; TODO not completed
;;;###autoload
(define-minor-mode dict-line-mode
  "Minor mode to look up words under the cursor asynchronously."
  :lighter " DictLine "
  :group 'dict-line
  :keymap (let ((map (make-sparse-keymap)))
            (define-key map (kbd dict-line-toggle-display-method-key)
              #'dict-line-toggle-display-method)
            map)
  (if dict-line-mode
      (progn
        ;; Start the idle timer for asynchronous word lookup
        (run-with-idle-timer dict-line-idle-time t #'dict-line--get-dict-async)
        ;; Add hook to delete posframe after each command
        (add-hook 'post-command-hook #'dict-line--posframe-delete))
    ;; Cancel all timers for dict-line--get-dict-async
    (cancel-function-timers #'dict-line--get-dict-async)
    ;; Remove the hook for deleting posframe
    (remove-hook 'post-command-hook #'dict-line--posframe-delete))
  )

;;;###autoload
(define-minor-mode global-dict-line-mode
  "Toggle Dict Line mode in all buffers."
  :global t
  :lighter " DictLine"
  :keymap (let ((map (make-sparse-keymap)))
            (define-key map (kbd dict-line-toggle-display-method-key)
              #'dict-line-toggle-display-method)
            map)
  (if global-dict-line-mode
      (dict-line-mode 1)
    (dict-line-mode -1)))

;; 在用户配置文件中启用 dict-line-mode
(global-dict-line-mode 1)

(provide 'dict-line)

这是配置文件中的代码:

(require 'dict-line)
(dict-line-mode "🗺️")
;; 全局启用 dict-line-mode
(global-dict-line-mode 1)
(use-package diminish :ensure t)
(setq dict-line-dict-directory (concat slkshareemacs-dir "/dict/dict_data/"))
(setq dict-line-dict-personal-file (concat slkshareemacs-dir "/dict/Mydict.ts"))
(setq dict-line-audio-root-dir (concat slkshareemacs-dir "/dict/pronunciations/"))
(setq dict-line-audio t)
(setq dict-line-display #'dict-line--posframe);; 显示依赖 posframe.el
(setq dict-line-audio-play-program-arg "-volume 65")
(if (eq system-type 'windows-nt)
 (setq dict-line-use-clipboard t)
(setq dict-line-use-clipboard nil))
(setq dict-line-toggle-display-method-key "C-c C-d")

linux 上要安装 xsel 。

另外,windows 下的欧路可以直接开启鼠标自动取词,对 emacs 的窗口有效,但是它分割字符串有点困难,它不会按横杠分隔单词。linux 下的欧路不能鼠标自动取词。欧路有浏览器插件,我在想要是能知道它和浏览器之间的接口是咋样的,说不定能在 emacs 中模仿一下。

linux 下的 goldendict-ng ,可以直接在 Emacs 的窗口中取词,只要双击一个单词,emacs 可以选中这个单词,然后 goldendict-ng 就能弹窗翻译,当然要在它的图标上选中翻译弹窗,也可以是剪贴板取词。我觉得都很方便。

顺带一提,一直在用的 sdcv.el 也是很好用的

1 个赞

哈哈, 你用的开心就好, 每个人需求不同, Emacs 就是让大家随心所欲的 :grinning: