Commit 4b40d25f authored by Yuanle Song's avatar Yuanle Song
Browse files

v2.10.1 add optional minibuffer panel support;

- add optional posframe panel support.
- make C-g reset zero-input states when preedit
- accept DEL as backspace key in tty
- use keyword arguments in define-minor-mode
- other minor docstring changes.
parent b51e1d77
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
* COMMENT -*- mode: org -*-
#+Date: 2019-09-01
Time-stamp: <2024-05-23>
Time-stamp: <2024-05-26>

* zero-el

@@ -13,6 +13,14 @@ check [[https://gitlab.emacsos.com/sylecn/zero-el/blob/master/README][README in
* introduce to zero-el
https://blog.emacsos.com/zero-el.html

* optional packages

- zero-input-panel-posframe an optional zero-input panel implemented using
  elisp and posframe package. It requires posframe to work.

  To check difference between zero-input panels, see README in master branch
  on "Which panel should I use".

* License
zero-el is under Apache License 2.0

+229 −34
Original line number Diff line number Diff line
@@ -12,9 +12,9 @@
;; See the License for the specific language governing permissions and
;; limitations under the License.

;; Version: 2.8.0
;; Version: 2.10.1
;; URL: https://gitlab.emacsos.com/sylecn/zero-el
;; Package-Requires: ((emacs "24.3") (s "1.2.0"))
;; Package-Requires: ((emacs "24.4") (s "1.2.0"))

;;; Commentary:

@@ -80,6 +80,8 @@ EVENT and ERROR are error-handler arguments."

(add-hook 'dbus-event-error-functions 'zero-input-panel-error-handler)

(defvar zero-input-panel-dbus-service-known-name "com.emacsos.zero.Panel1")

(defun zero-input-panel-async-call (method _handler &rest args)
  "Call METHOD on zero-input-panel service asynchronously.

@@ -87,7 +89,7 @@ This is a wrapper around `dbus-call-method-asynchronously'.
ARGS optional extra args to pass to the wrapped function."
  (apply 'dbus-call-method-asynchronously
	 :session
	 "com.emacsos.zero.Panel1"	; well known name
	 zero-input-panel-dbus-service-known-name	; well known name
	 "/com/emacsos/zero/Panel1"	; object path
	 "com.emacsos.zero.Panel1.PanelInterface" ; interface name
	 method nil :timeout 500 args))
@@ -99,14 +101,14 @@ ARGS optional extra args to pass to the wrapped function."
(defun zero-input-alist-to-asv (hints)
  "Convert Lisp alist to dbus a{sv} data structure.

HINTS should be an alist of form '((k1 [v1type] v1) (k2 [v2type] v2)).
HINTS should be an alist of form \\='((k1 [v1type] v1) (k2 [v2type] v2)).

For example,
\(zero-input-alist-to-asv
  '((\"name\" \"foo\")
  \\='((\"name\" \"foo\")
    (\"timeout\" :int32 10)))
=>
'(:array
\\='(:array
  (:dict-entry \"name\" (:variant \"foo\"))
  (:dict-entry \"timeout\" (:variant :int32 10)))"
  (if (null hints)
@@ -251,7 +253,18 @@ If item is not in lst, return nil."

;; zero-input-el version
(defvar zero-input-version nil "Zero package version.")
(setq zero-input-version "2.8.0")
(setq zero-input-version "2.10.1")

(defvar zero-input-panel-is-ephemeral nil
  "Stores whether the panel service is ephemeral or not.

When a zero-input panel service can not persist its content on
key strokes, it should set this to t, so zero-input-framework
will call `zero-input-panel-show-candidates' on every keystroke
to refresh the candidates list even when no change is needed.

A zero-input panel service should revert this variable to nil on
exit.")

;; FSM state
(defconst zero-input--state-im-off 'IM-OFF)
@@ -370,7 +383,7 @@ Change will be effective only in new `zero-input-mode' buffer."
(defvar-local zero-input-initial-fetch-size 21
  "How many candidates to fetch for the first call to GetCandidates.

It's best set to (1+ (* zero-input-candidates-per-page N)) where
It\\='s best set to \\=(1+ (* zero-input-candidates-per-page N)) where
N is number of pages you want to fetch in initial fetch.")
;; zero-input-fetch-size is reset to 0 when preedit-str changes.
;; zero-input-fetch-size is set to fetch-size in build-candidates-async
@@ -469,7 +482,7 @@ STRING and OBJECTS are passed to `format'"

(defun zero-input-candidates-on-page (candidates)
  "Return candidates on current page for given CANDIDATES list."
  (cl-flet ((take (n lst)
  (cl-flet ((my-take (n lst)
	       "take the first n element from lst. if there is not
enough elements, return lst as it is."
	       (cl-loop
@@ -484,7 +497,7 @@ enough elements, return lst as it is."
		for n* = n then (1- n*)
		until (or (zerop n*) (null lst*))
		finally (return lst*))))
    (take zero-input-candidates-per-page
    (my-take zero-input-candidates-per-page
	  (drop (* zero-input-candidates-per-page zero-input-current-page) candidates))))

(defun zero-input-show-candidates (&optional candidates)
@@ -618,17 +631,23 @@ Return CH's Chinese punctuation if CH is converted. Return nil otherwise."
(defun zero-input-page-up ()
  "If not at first page, show candidates on previous page."
  (interactive)
  (when (> zero-input-current-page 0)
  (if (> zero-input-current-page 0)
      (progn
	(setq zero-input-current-page (1- zero-input-current-page))
    (zero-input-show-candidates)))
	(zero-input-show-candidates))
    (when zero-input-panel-is-ephemeral
      (zero-input-show-candidates))))

(defun zero-input-just-page-down ()
  "Just page down using existing candidates."
  (let ((len (length zero-input-candidates)))
    (when (> len (* zero-input-candidates-per-page (1+ zero-input-current-page)))
    (if (> len (* zero-input-candidates-per-page (1+ zero-input-current-page)))
	(progn
	  (setq zero-input-current-page (1+ zero-input-current-page))
	  (zero-input-debug "showing candidates on page %s\n" zero-input-current-page)
      (zero-input-show-candidates))))
	  (zero-input-show-candidates))
      (when zero-input-panel-is-ephemeral
	(zero-input-show-candidates)))))

(defun zero-input-page-down ()
  "If there is still candidates to be displayed, show candidates on next page."
@@ -811,6 +830,15 @@ N is the argument passed to `self-insert-command'."
    (zero-input-hide-candidate-list)
    (zero-input-leave-preedit-state)))

(defun zero-input-focus-changed ()
  "A callback function used in `after-focus-change-function'."
  (when (eq zero-input-state zero-input--state-im-preediting)
    (let ((state (frame-focus-state)))
      (cond
       ((null state) (zero-input-focus-out))
       ((eq state t) (zero-input-focus-in))
       (t nil)))))

(defun zero-input-buffer-list-changed ()
  "A hook function, run when buffer list has changed.  This includes user has switched buffer."
  (if (eq (car (buffer-list)) zero-input-buffer)
@@ -820,7 +848,14 @@ N is the argument passed to `self-insert-command'."
;; minor mode
;;============

(defvar zero-input-mode-map
(defun zero-input-keyboard-quit ()
  "Handle `keyboard-quit' when `zero-input-mode' is on."
  (interactive)
  (when (and (boundp 'zero-input-mode) zero-input-mode)
    (zero-input-reset))
  (keyboard-quit))

(defvar zero-input-mode-map-init
  (let ((map (make-sparse-keymap)))
    ;; build zero-input-prefix-map
    (defvar zero-input-prefix-map (define-prefix-command 'zero-input-prefix-map))
@@ -834,13 +869,19 @@ N is the argument passed to `self-insert-command'."
    ;; other keybindings
    (define-key map [remap self-insert-command]
      'zero-input-self-insert-command)
    (define-key map [remap keyboard-quit]
      'zero-input-keyboard-quit)
    map)
  "Initial keymap for `zero-input-mode'.")

(defvar zero-input-mode-map zero-input-mode-map-init
  "Keymap for `zero-input-mode'.")

(defun zero-input-enable-preediting-map ()
  "Enable preediting keymap in `zero-input-mode-map'."
  (zero-input-debug "zero-input-enable-preediting-map\n")
  (define-key zero-input-mode-map (kbd "<backspace>") 'zero-input-backspace)
  (define-key zero-input-mode-map (kbd "DEL") 'zero-input-backspace)
  (define-key zero-input-mode-map (kbd "RET") 'zero-input-return)
  (define-key zero-input-mode-map (kbd "<escape>") 'zero-input-reset))

@@ -848,6 +889,7 @@ N is the argument passed to `self-insert-command'."
  "Disable preediting keymap in `zero-input-mode-map'."
  (zero-input-debug "zero-input-disable-preediting-map\n")
  (define-key zero-input-mode-map (kbd "<backspace>") nil)
  (define-key zero-input-mode-map (kbd "DEL") nil)
  (define-key zero-input-mode-map (kbd "RET") nil)
  (define-key zero-input-mode-map (kbd "<escape>") nil))

@@ -860,23 +902,30 @@ Otherwise, show Zero."

;;;###autoload
(define-minor-mode zero-input-mode
  "a Chinese input method framework written as an emacs minor mode.
  "A Chinese input method framework written as an Emacs minor mode.

\\{zero-input-mode-map}"
  nil
  (:eval (zero-input-modeline-string))
  zero-input-mode-map
  :init-value nil
  :lighter (:eval (zero-input-modeline-string))
  :keymap zero-input-mode-map
  ;; body, it's run when mode is activated or deactivated.
  (if zero-input-mode
      (progn
	;; local variables and variable init
	(make-local-variable 'zero-input-candidates-per-page)
	(make-local-variable 'zero-input-full-width-mode)
	(zero-input-reset)
	(zero-input-set-im zero-input-im)
	;; hooks
	(if (boundp 'after-focus-change-function) ; emacs 27.1
	    (add-function :after (local 'after-focus-change-function)
			  #'zero-input-focus-changed)
	  (add-hook 'focus-in-hook 'zero-input-focus-in)
  (add-hook 'focus-out-hook 'zero-input-focus-out)
	  (add-hook 'focus-out-hook 'zero-input-focus-out))
	(setq zero-input-buffer (current-buffer))
	(add-hook 'post-self-insert-hook #'zero-input-post-self-insert-command nil t)
	(add-hook 'buffer-list-update-hook 'zero-input-buffer-list-changed))
    (zero-input-reset)))

(defun zero-input-post-self-insert-command (&optional ch)
  "Run after a regular `self-insert-command' is run by zero-input.
@@ -914,7 +963,7 @@ After registration, you can use `zero-input-set-default-im' and

IM-NAME should be a symbol.
IM-FUNCTIONS-ALIST should be a list of form
  '((:virtual-function-name . implementation-function-name))
  \\='((:virtual-function-name . implementation-function-name))

virtual functions                   corresponding variable
===========================================================================
@@ -1614,6 +1663,152 @@ DIGIT 0 means delete 10th candidate."

(provide 'zero-input-pinyin)

;; body of zero-input-panel-minibuffer.el


;; utils

(defun zero-input-panel-minibuffer--zip-pair1 (lst1 lst2 result)
  "Zip two lists LST1 and LST2, return a list of pairs from both lists.
RESULT is the accumulating list."
  (if (or (null lst1) (null lst2))
      (nreverse result)
    (zero-input-panel-minibuffer--zip-pair1
     (cdr lst1) (cdr lst2)
     (cons (cons (car lst1) (car lst2)) result))))

(defun zero-input-panel-minibuffer--zip-pair (lst1 lst2)
  "Zip two lists LST1 and LST2, return a list of pairs from both lists."
  (zero-input-panel-minibuffer--zip-pair1 lst1 lst2 nil))

;; main logic

(defvar zero-input-panel-minibuffer-last-candidates nil
  "Store last candidates string shown in minibuffer.")
(defvar zero-input-panel-minibuffer-original-resize-mini-windows
  resize-mini-windows
  "Store the original value of `resize-mini-windows'.
minibuffer panel works best when minibuffer height is 2 or
greater.  minibuffer height will be auto adjusted when
`zero-input-mode' is on and auto restored when `zero-input-mode' is
off.")

(defun zero-input-panel-minibuffer-make-string (candidates)
  "Create CANDIDATES string for use with minibuffer panel."
  (mapconcat 'identity (mapcar (lambda (pair)
				 (concat (int-to-string (% (car pair) 10))
					 "." (cdr pair)))
			       (zero-input-panel-minibuffer--zip-pair
				(cl-loop for i from 1 to 10 collect i)
				candidates)) " "))

(ert-deftest zero-input-panel-minibuffer-make-string ()
  (should (equal
	   (zero-input-panel-minibuffer--zip-pair '(1 2 3 4 5 6) '("a" "b" "c"))
	   '((1 . "a") (2 . "b") (3 . "c"))))
  (should (equal
	   (zero-input-panel-minibuffer--zip-pair '(1 2 3 4 5 6) '())
	   nil))
  (should (equal
	   (zero-input-panel-minibuffer-make-string '("a" "b" "c"))
	   "1.a 2.b 3.c"))
  (should (equal
	   (zero-input-panel-minibuffer-make-string '("a" "b" "c" "d"))
	   "1.a 2.b 3.c 4.d")))

(defun zero-input-panel-minibuffer-show-candidates (preedit-str _candidate-count candidates hints)
  "Show CANDIDATES using minibuffer package.
Argument PREEDIT-STR user typed characters.
Argument CANDIDATE-COUNT how many candidates to show."
  (interactive)
  ;; (zero-input-debug "candidates: %s\n" candidates)
  (let ((has-next-page (caadr (assoc "has_next_page" hints)))
	(has-previous-page (caadr (assoc "has_previous_page" hints)))
	(page-number (caadr (assoc "page_number" hints))))
    (let ((candidate-str (zero-input-panel-minibuffer-make-string candidates))
	  (pagination-str (concat (if has-previous-page "<" " ")
				  " " (int-to-string page-number) " "
				  (if has-next-page ">" " "))))
      (let ((str (concat preedit-str "\n" candidate-str " " pagination-str)))
	(setq zero-input-panel-minibuffer-last-candidates str)
	(message "%s" str))))
  :ignore)

(defun zero-input-panel-minibuffer-move (_x _y)
  "Move panel to (X, Y), based on origin at top left corner."
  (interactive)
  ;; move is not needed for minibuffer panel.
  :ignore)

(defun zero-input-panel-minibuffer-show ()
  "Show minibuffer panel."
  (interactive)
  (when zero-input-panel-minibuffer-last-candidates
    (message "%s" zero-input-panel-minibuffer-last-candidates))
  :ignore)

(defun zero-input-panel-minibuffer-hide ()
  "Hide minibuffer panel."
  (interactive)
  (message "%s" "")
  :ignore)

(defun zero-input-panel-minibuffer-quit ()
  "Quit minibuffer panel dbus service."
  (interactive)
  (setq resize-mini-windows
	zero-input-panel-minibuffer-original-resize-mini-windows)
  (dbus-unregister-service :session zero-input-panel-dbus-service-known-name)
  (setq zero-input-panel-is-ephemeral nil)
  :ignore)

(defun zero-input-panel-minibuffer-hook ()
  "Hook function to run when activate or deactivate `zero-input-mode'."
  (if zero-input-mode
      ;; activate zero-input-mode
      (progn
	(let ((w (minibuffer-window)))
	   (when (< (window-size w) 2)
	     (setq zero-input-panel-minibuffer-original-resize-mini-windows
		   resize-mini-windows)
	     (setq resize-mini-windows nil)
	     (window-resize w 1))))
    ;; deactivate zero-input-mode
    (setq resize-mini-windows
	  zero-input-panel-minibuffer-original-resize-mini-windows)))

(defun zero-input-panel-minibuffer-init ()
  "Init minibuffer based dbus panel service."
  (interactive)
  (let ((service-name zero-input-panel-dbus-service-known-name))
    (let ((res (dbus-register-service :session service-name
				      :do-not-queue)))
      (when (eq res :exists)
	;; replace existing panel service with minibuffer based service.
	(zero-input-panel-quit)
	;; async dbus call will return before server handle it.
	(sleep-for 0.1)
	(setq res (dbus-register-service :session service-name :do-not-queue)))
      (if (not (member res '(:primary-owner :already-owner)))
	  (error "Register dbus service failed: %s" res))
      (dolist (method (list
		       (cons "ShowCandidates" #'zero-input-panel-minibuffer-show-candidates)
		       (cons "Move" #'zero-input-panel-minibuffer-move)
		       (cons "Show" #'zero-input-panel-minibuffer-show)
		       (cons "Hide" #'zero-input-panel-minibuffer-hide)
		       (cons "Quit"  #'zero-input-panel-minibuffer-quit)))
	(dbus-register-method
	 :session
	 service-name
	 "/com/emacsos/zero/Panel1"
	 "com.emacsos.zero.Panel1.PanelInterface"
	 (car method)
	 (cdr method)))
      (setq zero-input-panel-is-ephemeral t)
      (add-hook 'zero-input-mode-hook 'zero-input-panel-minibuffer-hook))))

(provide 'zero-input-panel-minibuffer)


(provide 'zero-input)