1 ;;; external-dict.el --- Query external dictionary like goldendict, Bob.app etc -*- lexical-binding: t; -*-
3 ;; Authors: stardiviner <numbchild@gmail.com>
4 ;; Package-Requires: ((emacs "25.1"))
5 ;; Package-Version: 1.0
6 ;; Keywords: wp processes
7 ;; homepage: https://repo.or.cz/external-dict.el.git
8 ;; SPDX-License-Identifier: GPL-2.0-or-later
10 ;; You should have received a copy of the GNU General Public License
11 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
17 ;; (global-set-key (kbd "C-x d") 'external-dict-dwim)
18 ;; If invoke with [C-u] prefix, then it will raise the main window.
22 (declare-function ns-do-applescript
"nsfns.m" t
)
23 (require 'url
) ; for `url-retrieve-synchronously'
24 (require 'url-http
) ; for `url-http-end-of-headers'
27 (defgroup external-dict nil
28 "Use external dictionary in Emacs."
29 :prefix
"external-dict-"
32 (defcustom external-dict-cmd
36 ((executable-find "goldendict")
37 '(:dict-program
"goldendict" :command-p t
))))
40 ((and (file-exists-p "/Applications/Bob.app")
41 (not (string-empty-p (shell-command-to-string "pidof Bob"))))
42 '(:dict-program
"Bob.app" :command-p nil
))
43 ((and (file-exists-p "/Applications/Easydict.app")
44 (not (string-empty-p (shell-command-to-string "pidof Easydict"))))
45 '(:dict-program
"Easydict.app" :command-p nil
))
46 ((file-exists-p "/Applications/GoldenDict.app")
47 '(:dict-program
"GoldenDict.app" :command-p t
))
48 (t '(:dict-program
"Dictionary.app" :command-p t
)))))
49 "Specify external dictionary command."
51 :group
'external-dict
)
53 (defcustom external-dict-read-cmd
56 (cl-case (plist-get external-dict-cmd
:dict-program
)
60 ((executable-find "festival") "festival")
61 ((executable-find "espeak") "espeak")))))
63 (pcase (plist-get external-dict-cmd
:dict-program
)
65 ("GoldenDict.app" "say")
66 ("Dictionary.app" "say"))))
67 "Specify external tool command to read the query word.
68 If the value is nil, let dictionary handle it without invoke the command.
69 If the value is a command string, invoke the command to read the word."
72 :group
'external-dict
)
74 (defcustom external-dict-read-query t
75 "Whether read the query text."
78 :group
'external-dict
)
80 (defun external-dict--get-text ()
81 "Get word or text from region selected, `thing-at-point', or interactive input."
84 (let ((text (buffer-substring-no-properties (mark) (point))))
86 `(:type
:text
:text
,text
)))
87 ((and (thing-at-point 'word
)
88 (not (string-blank-p (substring-no-properties (thing-at-point 'word
)))))
89 (let ((word (substring-no-properties (thing-at-point 'word
))))
90 `(:type
:word
, :text
,word
)))
91 (t (let ((word (read-string "[external-dict.el] Query word: ")))
92 `(:type
:word
:text
,word
)))))
95 (defun external-dict-read-word (word)
96 "Auto pronounce the query WORD."
98 (when external-dict-read-query
100 (pcase external-dict-read-cmd
102 (shell-command (concat "say " (shell-quote-argument word
))))
104 (shell-command (concat "festival --tts " (shell-quote-argument word
))))
106 (shell-command (concat "espeak " (shell-quote-argument word
)))))))
108 ;;; [ macOS Dictionary.app ]
111 (defun external-dict-Dictionary.app
(word)
112 "Query WORD at point or region selected or input with macOS Dictionary.app."
116 (buffer-substring-no-properties (mark) (point)))
117 ((not (string-blank-p (substring-no-properties (thing-at-point 'word
))))
118 (substring-no-properties (thing-at-point 'word
)))
119 (t (read-string "[external-dict.el] Query word in macOS Dictionary.app: ")))))
121 (shell-command (format "open dict://\"%s\"" word
))
122 (external-dict-read-word word
))
126 (defun external-dict-goldendict--ensure-running ()
127 "Ensure goldendict program is running."
128 (unless (string-match "goldendict" (shell-command-to-string "ps -C 'goldendict' | sed -n '2p'"))
129 (start-process-shell-command
135 (defun external-dict-goldendict (word)
136 "Query WORD at point or region selected with goldendict.
137 If you invoke command with `RAISE-MAIN-WINDOW' prefix \\<universal-argument>,
138 it will raise external dictionary main window."
139 (interactive (list (plist-get (external-dict--get-text) :text
)))
140 (external-dict-goldendict--ensure-running)
141 (let ((goldendict-cmd (cl-case system-type
142 (gnu/linux
(executable-find "goldendict"))
143 (darwin (or (executable-find "GoldenDict") (executable-find "goldendict")))
144 (t (plist-get external-dict-cmd
:dict-program
)))))
145 (if current-prefix-arg
147 (call-process goldendict-cmd nil nil nil
))
149 ;; pass the selection to shell command goldendict.
150 ;; use Goldendict API: "Scan Popup"
151 (call-process goldendict-cmd nil nil nil word
))
152 (external-dict-read-word word
)
155 ;;; alias for `external-dict-cmd' property `:dict-program' name under macOS.
156 (defalias 'external-dict-GoldenDict.app
'external-dict-goldendict
)
161 (defun external-dict-Bob.app-translate
(text)
162 "Translate TEXT in Bob.app."
163 (let ((path "translate")
164 (action "translateText")
167 (format "use scripting additions
168 use framework \"Foundation\"
169 on toJson(recordValue)
170 (((current application's NSString)'s alloc)'s initWithData:((current application's NSJSONSerialization)'s dataWithJSONObject:recordValue options:1 |error|:(missing value)) encoding:4) as string
173 set theRecord to {|path|: \"%s\", body: {action: \"%s\", |text|: \"%s\", windowLocation: \"center\", inputBoxState: \"alwaysUnfold\"}}
174 set theParameter to toJson(theRecord)
175 tell application id \"com.hezongyidev.Bob\" to request theParameter
179 (defun external-dict-Bob.app-dictionary
(word)
180 "Query WORD in macOS Bob.app."
183 "tell application \"Bob\"
187 (external-dict-read-word word
))
189 (defun external-dict-Bob.app
()
190 "Translate text with Bob.app on macOS."
192 (let* ((return-plist (external-dict--get-text))
193 (type (plist-get return-plist
:type
))
194 (text (plist-get return-plist
:text
)))
197 (external-dict-Bob.app-dictionary text
))
199 (external-dict-Bob.app-translate text
)))))
203 (defun external-dict-Easydict.app--http-api
(text &optional target-language service-type apple-dictionary-names
&rest args
)
204 "Translate TEXT in Easydict local HTTP server translate API.
206 - TARGET-LANGUAGE: specify translated text target language.
207 - SERVICE-TYPE: specify service type available in Easydict.
208 - ARGS: extra arguments like appleDictionaryNames vector in HTTP API."
209 (if-let ((ping-connection (ignore-errors (open-network-stream "ping-localhost" "*ping localhost*" "localhost" 8080))))
210 (let* ((url-request-method "POST")
211 (url-request-extra-headers '(("Content-Type" .
"application/json")))
212 ;; POST JSON data `url-request-data'
213 (service-type (or service-type
214 (completing-read "[Easydict] serviceType: " '("AppleDictionary" "Apple" "CustomOpenAI"))))
215 (target-language (or target-language
216 (completing-read "[Easydict] targetLanguage: " '("zh-Hans" "en"))))
217 (apple-dictionary-names (when (string-equal service-type
"AppleDictionary")
218 (or apple-dictionary-names
219 (completing-read-multiple "[Easydict] multiple appleDictionaryNames (separated by ,) : "
220 ;; TODO: auto read a list of dictionaries in ~/Library/Dictionaries/
221 '("简明英汉字典" "牛津高阶英汉双解词典" "现代汉语规范词典" "汉语成语词典" "现代汉语同义词典" "大辞海"
222 "Oxford Dictionary of English" "New Oxford American Dictionary" "Oxford Thesaurus of English"
223 "Oxford American Writer’s Thesaurus")))))
224 (url-request-data (encode-coding-string (json-encode
226 ,(cons 'targetLanguage target-language
)
227 ,(cons 'serviceType service-type
)
228 ,@(when apple-dictionary-names
229 (list (cons 'appleDictionaryNames apple-dictionary-names
)))))
231 (with-current-buffer (url-retrieve-synchronously
232 (let ((host "localhost")
234 (api (if (member service-type
'("CustomOpenAI" "Ollama"))
237 (format "http://%s:%s/%s" host port api
)))
238 (let* ((result-alist (json-read-from-string
239 (buffer-substring-no-properties (1+ url-http-end-of-headers
) (point-max))))
240 (translated-text (decode-coding-string (alist-get 'translatedText result-alist
) 'utf-8
)))
241 (delete-process ping-connection
)
242 (message translated-text
))))
243 (user-error "[external-dict] Easydict local HTTP server is not available. Please enable it in settings")))
245 (defun external-dict-Easydict.app--http-api-translate-service-apple-dictionary
(text)
246 "Translate TEXT in Easydict in translate API with service Apple Dictionary."
247 (external-dict-Easydict.app--http-api text nil
"AppleDictionary"))
249 (defun external-dict-Easydict.app--http-api-translate-service-apple
(text)
250 "Translate TEXT in Easydict in translate API with service Apple."
251 (external-dict-Easydict.app--http-api text nil
"Apple"))
253 (defun external-dict-Easydict.app--http-api-translate-service-custom-openai
(text)
254 "Translate TEXT in Easydict in translate API with service CustomOpenAI."
255 (external-dict-Easydict.app--http-api text nil
"CustomOpenAI"))
258 ;; (external-dict-Easydict.app--http-api "good ending")
259 ;; (external-dict-Easydict.app--http-api "世界")
260 ;; (external-dict-Easydict.app--http-api-translate-service-apple-dictionary "world")
262 (defun external-dict-Easydict.app--macos-url-call-query
(word)
263 "Query WORD in Easydict.app on macOS system through URL scheme."
265 :name
"external-dict Easydict.app"
266 :command
(list "open" (format "easydict://query?text=%s" (url-encode-url word
)))))
269 (defun external-dict-Easydict.app
()
270 "Translate text with Easydict.app on macOS.
271 Easydict.app URL scheme easydict://query?text=good%20girl
272 You can open the URL scheme with shell command:
273 $ open \"easydict://query?text=good%20girl\""
275 (let* ((return-plist (external-dict--get-text))
276 (type (plist-get return-plist
:type
))
277 (text (plist-get return-plist
:text
)))
280 (external-dict-Easydict.app--macos-url-call-query text
)
281 ;; (external-dict-Easydict.app--http-api-translate-service-apple-dictionary text)
282 (external-dict-read-word text
))
285 (if-let ((ping-connection (ignore-errors (open-network-stream "ping-localhost" "*ping localhost*" "localhost" 8080))))
287 (external-dict-Easydict.app--http-api text
)
288 (delete-process ping-connection
))
290 (external-dict-Easydict.app--macos-url-call-query text
))))))
293 (defun external-dict-dwim ()
294 "Query current word at point or region selected with external dictionary."
296 (let ((dict-program (plist-get external-dict-cmd
:dict-program
)))
297 (call-interactively (intern (format "external-dict-%s" dict-program
)))))
301 (provide 'external-dict
)
303 ;;; external-dict.el ends here