1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
|
;;; claude-input.el --- Slot-based, buffered Zhuyin input for chinese-zozy -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Jean Sévère Durand
;; Keywords: i18n, convenience
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Improvements over the `chinese-zozy' Quail package (ZOZY.el), which
;; is left untouched. Everything is done by advising Quail.
;;
;; 1. Slots. A syllable has four slots: initial, medial, final and
;; tone. Typing a symbol whose slot is already taken replaces the
;; old symbol, so the order of typing does not matter. A tone
;; finishes the syllable (its candidates are shown); typing any
;; non-tone symbol afterwards starts a *new* character and leaves
;; the finished one alone.
;;
;; 2. Buffering. All characters of one composition are kept as
;; "cells". Move between cells with the keys in
;; `claude-input-backward-keys' / `claude-input-forward-keys' and
;; edit or delete them. The composition ends with a key in
;; `claude-input-commit-keys' (RET by default).
;;
;; Keys are classified through `quail-keyboard-translate', so the code
;; works with any `quail-keyboard-layout-type'. Slots store the raw
;; keys that the user typed, because `quail-current-key' is raw as
;; well.
;;
;; Word-level selection (stage 2) is not implemented yet.
;;
;; Usage: (require 'claude-input), then select `chinese-zozy'.
;;; Code:
(require 'quail)
(require 'subr-x)
(defgroup claude-input nil
"Slot-based, buffered Zhuyin input."
:group 'quail)
(defcustom claude-input-package "chinese-zozy"
"Name of the Quail package that this file improves."
:type 'string)
(defcustom claude-input-backward-keys (list [left] (kbd "M-b"))
"Key sequences moving to the previous cell inside a composition."
:type '(repeat sexp))
(defcustom claude-input-forward-keys (list [right] (kbd "M-f"))
"Key sequences moving to the next cell inside a composition."
:type '(repeat sexp))
(defcustom claude-input-commit-keys (list (kbd "RET") (kbd "C-j"))
"Key sequences that finish the whole composition."
:type '(repeat sexp))
;;;; Slots
(defconst claude-input--slot-table
(let ((table (make-hash-table :test #'eq)))
;; Keys are in the standard (QWERTY) layout, as used by ZOZY.el.
(dolist (spec '((0 . "1q2wazsxedcrfv5tgbyhn") ; initials
(1 . "ujm") ; medials
(2 . "8ik,9ol.0p;/-") ; finals
(3 . " 6347"))) ; tones
(seq-doseq (c (cdr spec))
(puthash c (car spec) table)))
table)
"Map a standard-layout key to its slot index (0 to 3).")
(defun claude-input--slot-of (char)
"Return the slot index of the raw key CHAR, or nil if it is no symbol.
CHAR is first translated to the standard layout."
(and (characterp char)
(gethash (quail-keyboard-translate char) claude-input--slot-table)))
(defun claude-input--key (slots)
"Return the normalised raw key string built from SLOTS."
(apply #'string (delq nil (append slots nil))))
(defun claude-input--slots-from-key (key)
"Return a fresh slot vector filled with the symbols in KEY."
(let ((slots (make-vector 4 nil)))
(seq-doseq (c key)
(when-let* ((i (claude-input--slot-of c)))
(aset slots i c)))
slots))
;;;; Cells
;; A cell is a vector [SLOTS STR BEG END INDEX]. STR is the text of
;; the cell, BEG and END are markers around that text in the buffer,
;; and INDEX is the selected candidate. The text of the current cell
;; is owned by Quail (`quail-overlay'); its markers are only refreshed
;; when we leave the cell.
(defvar claude-input--cells nil
"Vector of cells of the composition in progress, or nil when inactive.")
(defvar claude-input--index 0
"Index in `claude-input--cells' of the cell being edited.")
(defvar claude-input--slots (make-vector 4 nil)
"Slots of the syllable being edited.")
(defvar claude-input--last-tone-p nil
"Non-nil if the last key typed was a tone.")
(defvar claude-input--overlay nil
"Overlay underlining the whole composition.")
(defsubst claude-input--active-p ()
"Return non-nil if a buffered composition is in progress."
(and claude-input--cells t))
(defsubst claude-input--cell-slots (cell) (aref cell 0))
(defsubst claude-input--cell-str (cell) (aref cell 1))
(defsubst claude-input--cell-beg (cell) (aref cell 2))
(defsubst claude-input--cell-end (cell) (aref cell 3))
(defsubst claude-input--cell-choice (cell) (aref cell 4))
(defun claude-input--make-cell (&optional slots str beg end choice)
"Make a cell. Markers are created if BEG and END are nil."
(let ((b (make-marker)) (e (make-marker)))
;; BEG advances over text inserted at its position, END does not.
;; Together this keeps adjacent cells separated when the text of
;; one of them is replaced.
(set-marker-insertion-type b t)
(set-marker-insertion-type e nil)
(when beg (set-marker b beg))
(when end (set-marker e end))
(vector (or slots (make-vector 4 nil)) (or str "") b e (or choice 0))))
(defun claude-input--current ()
"Return the cell being edited."
(aref claude-input--cells claude-input--index))
(defun claude-input--current-choice ()
"Return the selected candidate index of the current translations."
(if quail-current-translations
(or (car-safe (car-safe quail-current-translations)) 0)
0))
(defun claude-input--save-current ()
"Save the state of the current cell, from Quail's, into the cell."
(let ((cell (claude-input--current))
(beg (overlay-start quail-overlay))
(end (overlay-end quail-overlay)))
(aset cell 0 (copy-sequence claude-input--slots))
(aset cell 1 (or quail-current-str ""))
(aset cell 4 (claude-input--current-choice))
(when beg
(set-marker (claude-input--cell-beg cell) beg)
(set-marker (claude-input--cell-end cell) end))
;; Make Quail forget the text so that it stays in the buffer.
(when end (move-overlay quail-overlay end end))
(claude-input--normalize-cells cell)))
(defun claude-input--normalize-cells (current)
"Order `claude-input--cells' by buffer position and track CURRENT.
Ties (empty cells share a position with their neighbours) are broken by
the old order, so the sort is stable. `claude-input--index' is set to
the index of the cell CURRENT, found by identity. This must be called
after the markers of CURRENT have been refreshed."
(let ((i -1) (last-pos 0) keyed)
(seq-doseq (cell claude-input--cells)
(let ((pos (or (marker-position (claude-input--cell-beg cell)) last-pos)))
(setq last-pos pos)
(push (list pos (setq i (1+ i)) cell) keyed)))
(setq claude-input--cells
(vconcat
(mapcar #'caddr
(sort (nreverse keyed)
(lambda (a b)
(or (< (car a) (car b))
(and (= (car a) (car b))
(< (cadr a) (cadr b)))))))))
(setq claude-input--index
(or (seq-position claude-input--cells current #'eq) 0))))
(defun claude-input--release-cell (cell)
"Release the markers of CELL."
(set-marker (claude-input--cell-beg cell) nil)
(set-marker (claude-input--cell-end cell) nil))
(defun claude-input--empty-cell-p (cell)
"Return non-nil if CELL has no symbols at all."
(not (seq-some #'identity (claude-input--cell-slots cell))))
(defun claude-input--enter (index)
"Make the cell at INDEX the one being edited.
The previous current cell must already have been saved. The text of
cell INDEX is handed over to Quail, and the candidates are rebuilt."
(setq claude-input--index index)
(let* ((cell (aref claude-input--cells index))
(beg (marker-position (claude-input--cell-beg cell)))
(end (marker-position (claude-input--cell-end cell))))
(setq claude-input--slots (copy-sequence (claude-input--cell-slots cell))
claude-input--last-tone-p nil
quail-current-key (claude-input--key claude-input--slots))
(cond
((and beg end)
(move-overlay quail-overlay beg end)
(goto-char end))
(t (move-overlay quail-overlay (point) (point))))
(setq quail-current-str (claude-input--cell-str cell))
(if (string-empty-p quail-current-key)
(quail-delete-region)
(let ((choice (claude-input--cell-choice cell)))
(quail-update-translation (quail-translate-key))
;; Restore the candidate that was selected earlier.
(when (and quail-current-translations (> choice 0))
(setcar (car quail-current-translations)
(min choice (1- (length (cdr quail-current-translations)))))
(quail-update-current-translations)
(quail-update-translation nil))))
(claude-input--refresh-overlay)))
(defun claude-input--move (delta)
"Save the current cell and edit the cell DELTA cells away from it.
Empty cells are dropped. Return non-nil if we moved."
(let ((target (aref claude-input--cells
(max 0 (min (+ claude-input--index delta)
(1- (length claude-input--cells)))))))
(unless (eq target (claude-input--current))
(let ((cur (claude-input--current)))
(claude-input--save-current)
(when (claude-input--empty-cell-p cur)
(claude-input--release-cell cur)
(setq claude-input--cells (vconcat (delq cur (append claude-input--cells nil)))))
(claude-input--enter (seq-position claude-input--cells target #'eq))
t))))
(defun claude-input--insert-cell-after-current ()
"Save the current cell and open a new empty cell right after it."
(claude-input--save-current)
(let ((pos (or (overlay-end quail-overlay) (point)))
(idx (1+ claude-input--index)))
(setq claude-input--cells
(vconcat (seq-subseq claude-input--cells 0 idx)
(vector (claude-input--make-cell nil "" pos pos))
(seq-subseq claude-input--cells idx)))
(setq claude-input--index idx
claude-input--slots (make-vector 4 nil)
claude-input--last-tone-p nil
quail-current-key ""
quail-current-str "")
(goto-char pos)
(move-overlay quail-overlay pos pos)))
;;;; Overlay
(defun claude-input--refresh-overlay ()
"Underline all text of the composition."
(when (claude-input--active-p)
(let ((lo nil) (hi nil))
(dotimes (i (length claude-input--cells))
(let* ((cell (aref claude-input--cells i))
(cur (= i claude-input--index))
(b (if cur (overlay-start quail-overlay)
(marker-position (claude-input--cell-beg cell))))
(e (if cur (overlay-end quail-overlay)
(marker-position (claude-input--cell-end cell)))))
(when (and b e)
(setq lo (if lo (min lo b) b)
hi (if hi (max hi e) e)))))
(when lo
(unless (overlayp claude-input--overlay)
(setq claude-input--overlay (make-overlay lo hi nil nil t))
(overlay-put claude-input--overlay 'face 'underline)
(overlay-put claude-input--overlay 'priority -1))
(move-overlay claude-input--overlay lo hi)))))
(defun claude-input--delete-overlay ()
"Delete the composition overlay."
(when (overlayp claude-input--overlay)
(delete-overlay claude-input--overlay))
(setq claude-input--overlay nil))
;;;; Translation advice
(defun claude-input--applies-p ()
"Return non-nil if our advice should act in the current Quail package."
(and (claude-input--active-p)
(equal (quail-name) claude-input-package)))
(defun claude-input--translate-advice (orig &rest args)
"Around advice for `quail-translate-key' (ORIG, ARGS).
Implements the slots; see the Commentary."
(let ((char last-command-event))
(cond
((not (claude-input--applies-p))
(apply orig args))
;; A symbol key typed by the user.
((and (eq this-command 'quail-self-insert-command)
(claude-input--slot-of char))
(let* ((slot (claude-input--slot-of char))
(tone-p (= slot 3))
(finished (aref claude-input--slots 3)))
;; A finished syllable followed by a non-tone symbol (or by a
;; tone after a non-tone edit) starts a new character. Several
;; tones in a row only replace the tone.
(when (and finished (not (and tone-p claude-input--last-tone-p)))
(claude-input--insert-cell-after-current))
(setq claude-input--last-tone-p tone-p)
(aset claude-input--slots slot char)
(setq quail-current-key (claude-input--key claude-input--slots))
(let ((res (apply orig args)))
(when (numberp res)
;; No translation for this combination: keep what we had,
;; show the symbols typed so far.
(setq quail-current-str
(or quail-current-str (quail-keyseq-translate quail-current-key))))
(claude-input--refresh-overlay)
;; Only the commit keys finish the composition.
nil)))
;; Anything else: a digit selecting a candidate, a backspace, or
;; a stray key that ends the composition.
(t
(let ((res (apply orig args)))
(setq claude-input--slots
(claude-input--slots-from-key quail-current-key))
(claude-input--refresh-overlay)
(cond ((eq res t) nil) ; a selection is not the end
(t res)))))))
;;;; Commands bound in the Quail translation keymap
(defun claude-input-backward ()
"Edit the previous character of the composition."
(interactive)
(if (claude-input--active-p)
(or (claude-input--move -1)
(message "Beginning of composition"))
(quail-error "Not composing")))
(defun claude-input-forward ()
"Edit the next character of the composition."
(interactive)
(if (claude-input--active-p)
(or (claude-input--move 1)
(message "End of composition"))
(quail-error "Not composing")))
(defun claude-input-delete-backward ()
"Delete the last symbol of the current syllable.
When the syllable is already empty, delete the character itself and
move to a neighbour."
(interactive)
(cond
((not (claude-input--active-p)) (quail-delete-last-char))
((> (length quail-current-key) 1)
(quail-delete-last-char))
(t
;; One symbol left: remove the cell.
(let ((cur claude-input--index)
(n (length claude-input--cells)))
(cond
((= n 1) (quail-abort-translation))
(t
(quail-delete-region)
(claude-input--release-cell (claude-input--current))
(setq claude-input--cells
(vconcat (seq-subseq claude-input--cells 0 cur)
(seq-subseq claude-input--cells (1+ cur))))
(setq claude-input--index (min cur (1- (length claude-input--cells))))
;; Previous cell if there is one, else the next.
(when (> cur 0) (setq claude-input--index (1- cur)))
(claude-input--enter claude-input--index)))))))
(defun claude-input-commit ()
"Finish the whole composition and keep the text."
(interactive)
(when (claude-input--active-p)
;; Make the last cell the current one so that point ends up after
;; all of the text.
(claude-input--move (length claude-input--cells)))
(quail-terminate-translation))
(defun claude-input--bind-keys ()
"Install our commands in the translation keymap of the package."
(when-let* ((map (quail-translation-keymap)))
;; Quail binds ESC itself, which blocks meta keys such as M-b.
;; Shadow it with a prefix map.
(unless (keymapp (lookup-key map [27]))
(define-key map [27] (make-sparse-keymap)))
(dolist (k claude-input-backward-keys)
(define-key map k #'claude-input-backward))
(dolist (k claude-input-forward-keys)
(define-key map k #'claude-input-forward))
(dolist (k claude-input-commit-keys)
(define-key map k #'claude-input-commit))
(define-key map "\177" #'claude-input-delete-backward)
(define-key map [backspace] #'claude-input-delete-backward)))
;;;; Session management
(defun claude-input--start ()
"Begin a composition."
(setq claude-input--cells (vector (claude-input--make-cell))
claude-input--index 0
claude-input--slots (make-vector 4 nil)
claude-input--last-tone-p nil))
(defun claude-input--end ()
"Release everything."
(when claude-input--cells
(seq-doseq (cell claude-input--cells)
(claude-input--release-cell cell)))
(claude-input--delete-overlay)
(setq claude-input--cells nil
claude-input--index 0
claude-input--slots (make-vector 4 nil)
claude-input--last-tone-p nil))
(defun claude-input--start-translation-advice (orig key)
"Around advice for `quail-start-translation' (ORIG, KEY).
Wrap the whole composition so that state is set up and cleaned."
(if (not (equal (quail-name) claude-input-package))
(funcall orig key)
(claude-input--bind-keys)
(claude-input--start)
(unwind-protect
(let* ((res (funcall orig key))
(last (and claude-input--cells
(1- (length claude-input--cells)))))
;; If the loop ended while editing a cell in the middle,
;; put its text back ourselves and move after the last cell.
(if (and last (/= claude-input--index last)
(stringp res) (> (length res) 0))
(progn
(insert res)
(let ((e (marker-position
(claude-input--cell-end
(aref claude-input--cells last)))))
(when e (goto-char e)))
"")
res))
(claude-input--end))))
;;;###autoload
(define-minor-mode claude-input-mode
"Globally enable slot-based, buffered input for `chinese-zozy'."
:global t
(cond
(claude-input-mode
(advice-add 'quail-translate-key :around #'claude-input--translate-advice)
(advice-add 'quail-start-translation :around
#'claude-input--start-translation-advice))
(t
(advice-remove 'quail-translate-key #'claude-input--translate-advice)
(advice-remove 'quail-start-translation
#'claude-input--start-translation-advice))))
;; (claude-input-mode 1)
(provide 'claude-input)
;;; claude-input.el ends here
|