add custom option to determine whether read query text
[external-dict.el.git] / external-dict.el
blob3482772bf5b76f67a51cb2b95f9657f76f1f8e00
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/>.
13 ;;; Commentary:
15 ;; Usage:
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.
20 ;;; Code:
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'
25 (require 'json)
27 (defgroup external-dict nil
28 "Use external dictionary in Emacs."
29 :prefix "external-dict-"
30 :group 'dictionary)
32 (defcustom external-dict-cmd
33 (cl-case system-type
34 (gnu/linux
35 (cond
36 ((executable-find "goldendict")
37 '(:dict-program "goldendict" :command-p t))))
38 (darwin
39 (cond
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."
50 :type 'string
51 :group 'external-dict)
53 (defcustom external-dict-read-cmd
54 (cl-case system-type
55 (gnu/linux
56 (cl-case (plist-get external-dict-cmd :dict-program)
57 ("goldendict" nil)
59 (cond
60 ((executable-find "festival") "festival")
61 ((executable-find "espeak") "espeak")))))
62 (darwin
63 (pcase (plist-get external-dict-cmd :dict-program)
64 ("Bob.app" "say")
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."
70 :type 'string
71 :safe #'stringp
72 :group 'external-dict)
74 (defcustom external-dict-read-query t
75 "Whether read the query text."
76 :type 'boolean
77 :safe #'booleanp
78 :group 'external-dict)
80 (defun external-dict--get-text ()
81 "Get word or text from region selected, `thing-at-point', or interactive input."
82 (cond
83 ((region-active-p)
84 (let ((text (buffer-substring-no-properties (mark) (point))))
85 (deactivate-mark)
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)))))
94 ;;;###autoload
95 (defun external-dict-read-word (word)
96 "Auto pronounce the query WORD."
97 (interactive)
98 (when external-dict-read-query
99 (sit-for 1)
100 (pcase external-dict-read-cmd
101 ("say"
102 (shell-command (concat "say " (shell-quote-argument word))))
103 ("festival"
104 (shell-command (concat "festival --tts " (shell-quote-argument word))))
105 ("espeak"
106 (shell-command (concat "espeak " (shell-quote-argument word)))))))
108 ;;; [ macOS Dictionary.app ]
110 ;;;###autoload
111 (defun external-dict-Dictionary.app (word)
112 "Query WORD at point or region selected or input with macOS Dictionary.app."
113 (interactive
114 (list (cond
115 ((region-active-p)
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: ")))))
120 (deactivate-mark)
121 (shell-command (format "open dict://\"%s\"" word))
122 (external-dict-read-word word))
124 ;;; [ Goldendict ]
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
130 "*goldendict*"
131 " *goldendict*"
132 "goldendict")))
134 ;;;###autoload
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
146 (save-excursion
147 (call-process goldendict-cmd nil nil nil))
148 (save-excursion
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)
153 (deactivate-mark))))
155 ;;; alias for `external-dict-cmd' property `:dict-program' name under macOS.
156 (defalias 'external-dict-GoldenDict.app 'external-dict-goldendict)
158 ;;; [ Bob.app ]
160 ;;;###autoload
161 (defun external-dict-Bob.app-translate (text)
162 "Translate TEXT in Bob.app."
163 (let ((path "translate")
164 (action "translateText")
165 (text text))
166 (ns-do-applescript
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
171 end toJson
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
177 path action text))))
179 (defun external-dict-Bob.app-dictionary (word)
180 "Query WORD in macOS Bob.app."
181 (ns-do-applescript
182 (format
183 "tell application \"Bob\"
184 launch
185 translate \"%s\"
186 end tell" word))
187 (external-dict-read-word word))
189 (defun external-dict-Bob.app ()
190 "Translate text with Bob.app on macOS."
191 (interactive)
192 (let* ((return-plist (external-dict--get-text))
193 (type (plist-get return-plist :type))
194 (text (plist-get return-plist :text)))
195 (cond
196 ((eq type :word)
197 (external-dict-Bob.app-dictionary text))
198 ((eq type :text)
199 (external-dict-Bob.app-translate text)))))
201 ;;; [ Easydict.app ]
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
225 `(,(cons 'text text)
226 ,(cons 'targetLanguage target-language)
227 ,(cons 'serviceType service-type)
228 ,@(when apple-dictionary-names
229 (list (cons 'appleDictionaryNames apple-dictionary-names)))))
230 'utf-8)))
231 (with-current-buffer (url-retrieve-synchronously
232 (let ((host "localhost")
233 (port 8080)
234 (api (if (member service-type '("CustomOpenAI" "Ollama"))
235 "streamTranslate"
236 "translate")))
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"))
257 ;;; TEST:
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."
264 (make-process
265 :name "external-dict Easydict.app"
266 :command (list "open" (format "easydict://query?text=%s" (url-encode-url word)))))
268 ;;;###autoload
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\""
274 (interactive)
275 (let* ((return-plist (external-dict--get-text))
276 (type (plist-get return-plist :type))
277 (text (plist-get return-plist :text)))
278 (cond
279 ((eq type :word)
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))
283 ((eq type :text)
284 ;; HTTP API
285 (if-let ((ping-connection (ignore-errors (open-network-stream "ping-localhost" "*ping localhost*" "localhost" 8080))))
286 (progn
287 (external-dict-Easydict.app--http-api text)
288 (delete-process ping-connection))
289 ;; URL scheme
290 (external-dict-Easydict.app--macos-url-call-query text))))))
292 ;;;###autoload
293 (defun external-dict-dwim ()
294 "Query current word at point or region selected with external dictionary."
295 (interactive)
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