From 72ac0c2a541dd0d54ac4234d88481176f77e39cd Mon Sep 17 00:00:00 2001 From: Yuanle Song <sylecn@gmail.com> Date: Sat, 25 May 2024 02:00:13 +0800 Subject: [PATCH] v2.10.0 add zero-input-panel-minibuffer.el - It is a panel based on Emacs minibuffer, so it works everywhere Emacs runs. No xorg/wayland required. - added zero-input-panel-is-ephemeral variable in zero-input-framework.el see its docstring to learn what it is used for. - this panel is tested works in tty emacs. - minor, do not update zero-input.el when content would not change. updated Makefile and build.py, now build.py will not overwrite zero-input.el if content would not change. --- ChangeLog | 7 + Makefile | 14 +- build.py | 23 ++- operational | 309 +++++++++++++++++++++++++++++++-- test-popup.el | 6 - zero-input-framework.el | 87 +++++++--- zero-input-panel-minibuffer.el | 181 +++++++++++++++++++ zero-input-panel-posframe.el | 4 +- zero-input-reload-all.el | 6 +- zero-input.el | 89 +++++++--- 10 files changed, 647 insertions(+), 79 deletions(-) delete mode 100644 test-popup.el create mode 100644 zero-input-panel-minibuffer.el diff --git a/ChangeLog b/ChangeLog index 815ff40..cac7759 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +2024-05-26 Yuanle Song <sylecn@gmail.com> + + zero-input v2.10.0 + - add an optional panel based on Emacs minibuffer. + it provides a panel that works everywhere Emacs runs. see + `zero-input-panel-minibuffer.el' for more information. + 2024-05-22 Yuanle Song <sylecn@gmail.com> zero-input v2.9.0 diff --git a/Makefile b/Makefile index 904f019..4a6822b 100644 --- a/Makefile +++ b/Makefile @@ -19,13 +19,23 @@ build: if ! python3 -m pytest --version; then python3 -m pip install --user pytest; fi python3 -m pytest build.py ./build.py - sed -i "s/PKG_VERSION/$(VERSION)/g" zero-input.el dist-check: build @echo "testing byte-compile is clean..." $(EMACS) -Q --batch -l $(S_EL) -l ./byte-compile-flags.el --eval='(byte-compile-file "zero-input.el")' $(EMACS) -Q --batch -l $(S_EL) -l $(POSFRAME_EL) -l ./byte-compile-flags.el -l ./zero-input.el --eval='(byte-compile-file "zero-input-panel-posframe.el")' + $(EMACS) -Q --batch -l $(S_EL) -l $(POSFRAME_EL) -l ./byte-compile-flags.el -l ./zero-input.el --eval='(byte-compile-file "zero-input-panel-minibuffer.el")' @echo "running unit tests..." - $(EMACS) -Q --batch -l $(S_EL) -l $(POSFRAME_EL) -l zero-input.el -l zero-input-panel-test.el -l zero-input-pinyin-service-test.el -l zero-input-framework-test.el -l zero-input-pinyin-test.el -l zero-input-table.el -l zero-input-table-test.el -f ert-run-tests-batch-and-exit + $(EMACS) -Q --batch -l $(S_EL) -l $(POSFRAME_EL) \ + -l zero-input.el \ + -l zero-input-panel-test.el \ + -l zero-input-pinyin-service-test.el \ + -l zero-input-framework-test.el \ + -l zero-input-pinyin-test.el \ + -l zero-input-table.el \ + -l zero-input-table-test.el \ + -l zero-input-panel-posframe.el \ + -l zero-input-panel-minibuffer.el \ + -f ert-run-tests-batch-and-exit #==================== # other make targets #==================== diff --git a/build.py b/build.py index 79cd58e..e971570 100755 --- a/build.py +++ b/build.py @@ -5,6 +5,7 @@ build zero-input.el from zero-input.el.in and other el files """ +import re import logging logger = logging.getLogger(__name__) @@ -54,12 +55,25 @@ def expand_placeholder_for_files(old_content, filenames): return result +def get_zero_input_version(): + """get zero-input-version from zero-input-framework.el file. + + """ + with open("zero-input-framework.el", encoding='utf-8') as f: + for line in f: + mo = re.match('^\\(setq zero-input-version "(.*)"', line) + if mo: + return mo.group(1) + raise RuntimeError("get zero-input-version failed.") + + def main(): logging.basicConfig( format='%(asctime)s [%(module)s] %(levelname)-8s %(message)s', level=logging.INFO) + zero_input_version = get_zero_input_version() with open("zero-input.el.in") as tpl: - content = tpl.read() + content = tpl.read().replace('PKG_VERSION', zero_input_version) expanded_content = expand_placeholder_for_files(content, [ "zero-input-panel.el", "zero-input-framework.el", @@ -67,8 +81,11 @@ def main(): "zero-input-pinyin-service.el", "zero-input-pinyin.el", ]) - with open('zero-input.el', 'w') as out: - out.write(expanded_content) + with open('zero-input.el', 'r', encoding="utf-8") as fin: + old_content = fin.read() + if expanded_content != old_content: + with open('zero-input.el', 'w', encoding="utf-8") as out: + out.write(expanded_content) if __name__ == '__main__': diff --git a/operational b/operational index 2b1c12a..a88c808 100644 --- a/operational +++ b/operational @@ -1,6 +1,6 @@ * COMMENT -*- mode: org -*- #+Date: 2019-10-08 -Time-stamp: <2024-05-23> +Time-stamp: <2024-05-26> #+STARTUP: content * notes :entry: ** 2019-04-01 zero-el a Chinese IM framework in emacs; FSM :doc: @@ -107,6 +107,10 @@ cd ~/lisp/elisp/zero/ (byte-compile-file "~/lisp/elisp/zero/zero-input-panel-posframe.el" t) (zero-input-panel-posframe-init) + ;; to test minibuffer panel + (byte-compile-file "~/lisp/elisp/zero/zero-input-panel-minibuffer.el" t) + (zero-input-panel-minibuffer-init) + now in some buffer, press F1 and start typing. @@ -158,19 +162,17 @@ see https://blog.emacsos.com/zero-el.html#org8e45833 - in the Makefile, ./build.py runs to create zero-input.el from zero-input.el.in -* later :entry: -* current :entry: -** -** 2024-05-22 issues when testing in emacs -Q. -- DONE frame bg color and fg color doesn't stand out. - easy to confuse with user's buffer content. - - to make it easier to customize the theme, create a function that user can - advice or redefine. +** 2024-05-25 issues with popup panel +- zero-input-panel-popup.el +- it didn't work at all in tty emacs. -- DONE should handle service already :exists case. - just run quit and retry. - if retry still fail, then fail. + // since I want a panel that works in tty emacs, I discontinued work on + popup panel. +- popup-tip width is not consistent across string lines. +- has glitches in xorg session. too many redraws. +* later :entry: +** 2024-05-22 posframe based panel bug: when two standalone emacs are running, panel +may show in the wrong instance. - when two standalone emacs are running. only one posframe panel service can run. @@ -183,6 +185,17 @@ see https://blog.emacsos.com/zero-el.html#org8e45833 well, get rid of dbus and this can work. +** 2024-05-26 minibuffer based panel, how to properly re-display when minibuffer is used by other package? +- when some other package/function shows information in minibuffer, zero-input + can't re-display candidates without user interaction (e.g. keep typing + preedit string). + + should there be a timed checker to implement sticky minibuffer? when user is + preediting, check minibuffer content and refresh it if necessary. + // that is close to sticky minibuffer, which I hate in the first place though. + +* current :entry: +** ** 2021-08-08 create a panel for wayland display server. currently zero-panel doesn't position itself correctly in wayland on debian 11 bullseye. @@ -227,6 +240,260 @@ I don't know it should recommend or suggest. ** 2019-10-23 checkdoc and package-lint can't ignore some non-issues. I can't run them in git pre-commit hook. * done :entry: +** 2024-05-26 fix package lint issues +6 issues found: + +- WONTFIX 61:12: warning: This file is not in the `cl-lib' ELPA compatibility package: require `cl-lib' instead. +- INVALID 216:38: error: You should depend on (emacs "25.1") if you need `window-absolute-pixel-position'. +- INVALID 221:4: error: You should depend on (emacs "25.1") if you need `frame-edges'. +- DONE 500:5: error: You should depend on (emacs "29.1") or the compat package if you need `take'. + + I included my own definition in cl-flet. + rename it to avoid shadow function definition. + +- INVALID 836:18: error: You should depend on (emacs "27.1") if you need `frame-focus-state'. +- INVALID 921:6: error: You should depend on (emacs "24.4") if you need `add-function'. + +** 2024-05-22 issues when testing in emacs -Q. +- DONE frame bg color and fg color doesn't stand out. + easy to confuse with user's buffer content. + + to make it easier to customize the theme, create a function that user can + advice or redefine. + +- DONE should handle service already :exists case. + just run quit and retry. + if retry still fail, then fail. + +- MOVED when two standalone emacs are running. + only one posframe panel service can run. + which means I can not type in two standalone emacs. + + currently when I start posframe panel service in one emacs, and type in the + other emacs, the panel will show at point in the first emacs instance. + + can I fix this? can each emacs has its own posframe panel service? + + well, get rid of dbus and this can work. + +** 2024-05-25 make minibuffer based panel work. +- DONE press - in page one should not clear the candidates. + press = in last page should not clear the candidates. + + just do early exit? + check how zero-panel and posframe did it. + + ~/c/zero-panel/server.c + if (candidate_count > 0) { + } + + when I add this in zero-input-panel-minibuffer.el, minibuffer is still empty + when I press - on first page. + it's because minibuffer is cleared when I type any character. + I need to redraw last candidates. + +- DONE redraw last candidates didn't work. + try add some debug log. + + when - key triggers no pagination, no panel async call is done. + minibuffer is erased because I typed any key. emacs auto erase minibuffer. + + either do a sticky minibuffer, or update zero-input to trigger another + refresh event, with hint {"no-change": t}. + + but I never liked sticky minibuffer. + so just trigger another refresh event. + + in zero-panel or posframe panel, when no-change hint is true, ignore the + call/event. in minibuffer panel, redraw the minibuffer. + + zero-input-page-up + zero-input-page-down + + just always redraw is easy in zero-input-framework.el, will it cause flick + in zero-panel or posframe? + (zero-input-panel-quit) + (zero-input-panel-posframe-init) + no flick in zero-panel or posframe. + just always redraw then. + + could also add a variable so redraw can be omit. + zero-input-panel-is-ephemeral + yeah, this is cheap, do it. + + document this variable. + +- DONE auto set minibuffer height to 2 when zero-input minor mode is enabled. + this will reduce flick when typing. + + auto revert it when zero-input mode is disabled, or panel exit. + auto enable when zero-input mode is enabled and the panel is 'minibuffer + based. I need a variable to store the current panel implementation name. + + zero-input-panel-name + + (let ((w (minibuffer-window))) + (if (window-size w) + + (setq resize-mini-windows nil) + (window-resize w 1)) + (setq resize-mini-windows 'grow-only) + + (zero-input-panel-minibuffer-init) + + // the panel can use a hook on mode activate/deactivate. + +- DONE bug: ESC can't clear preedit string when using tty emacs. // use C-g + ESC is meta escape by default. + + C-g can't clear preedit string either. + I think C-g should clear preedit string. + + reproduce the bug: + f1 + ruguo + C-g + woshuo + + // now ruguo is still in preedit string. C-g doesn't cancel anything in + zero-input preedit. + + how ESC key work with zero-panel and posframe? + (define-key zero-input-mode-map (kbd "<escape>") 'zero-input-reset) + + backspace also didn't work in tty emacs. + it is mapped to DEL key in tty emacs. + + DONE so the real problem is <backspace> and <escape> key event is NA in tty emacs. + + DONE fixed <backspace> issue. I also bind it to DEL key. + + how about <escape>? + search: capture escape key in tty emacs + + this is a problem. in tty, escape key is send as C-[, emacs doesn't know + about escape. + + just handle C-g maybe. + keyboard-quit + + there is no hook when this happen? + + search: emacs elisp run something when user C-g keyboard-quit + + I can use function advice. enable advice when enter preedit, disable advice + when exit preedit. or just always enable advice and use if checks. + + Info: (elisp) Advising Named Functions + + it's not recommend to use advice in library code. + try use hook or other ways. + + In my case, I can remap keyboard-quit to another function to accomplish the + same. just like org-mode remap newline-and-indent to org-newline-and-indent. + + so avoid advice. + + remap works. + +- MOVED when some other package/function shows information in minibuffer, zero-input + can't re-display candidates without user interaction. + + should there be a timed checker to implement sticky minibuffer? when user is + preediting, check minibuffer content and refresh it if necessary. + // that is close to sticky minibuffer, which I hate in the first place though. + +- problems + - zero-input-panel-minibuffer-show-candidates + (if (eql candidate-count 0) + (message "%s" zero-input-panel-minibuffer-last-candidates) + + I think this logic is wrong. + if I type a English word where there is no matching pinyin, I could get an + empty candidate list. + + I can't think of such a pinyin. + +** 2024-05-26 "make build" should not regenerate zero-input.el when content doesn't change. +- use build.py to replace PKG_VERSION + was using Makefile to do it. + sed -i "s/PKG_VERSION/$(VERSION)/g" zero-input.el +- if regenerate content would be the same, don't touch/write the file. + +** 2024-05-24 create a panel that works in tty. +- posframe require X/wayland to work. +- popup works in tty. + auto-complete/popup-el: Visual Popup Interface Library for Emacs + https://github.com/auto-complete/popup-el?tab=readme-ov-file + + (require 'popup) + (setq popup (popup-create (point) 10 10)) + (popup-set-list popup '("abc" "def" "ghi" "dfjsf")) + (popup-draw popup) + (popup-hide popup) + + (popup-hidden-p popup) + (popup-delete popup) + +- can I move a popup after creation? + + Info: (cl) Structures + + (setf (popup-point popup) (point)) + this didn't work. + + check source code of popup-draw. + it's not a good fit for my usage. + + try emacs overlay instead. + Info: (elisp) Overlays + + too low level. + +- (popup-tip "abc\ndef\nghi\n>") + (setq popup (popup-tip "abc\ndef\nghi\n>" :nowait t)) + (popup-hide popup) + +- problems + - newline in popup-tip is not working. + (popup-tip "abc\ndef\nghi\n>") + this works. why not in my code? + + #+begin_src elisp + (popup-tip "1.人 + 2.让 + 3.日 + 4.热 + 5.如 + 6.壬 + 7.认 + 8.软 + 9.肉 + 0.瑞 + < 1 >" :margin 1) + #+end_src + it's :width issue. when width is big, \n is not honored. + + popup-tip width issue + + - popup didn't work in tty emacs. + I tried in rh902 VM. + + the popup texts keeps recurring and is not usable. + not sure why. + performance is not good either. too many unnecessary refresh. + + use a regular buffer would be much better. + or just try use the minibuffer. + + minibuffer works on first try. + but it also has refresh glitch in xorg session. + + try tty. + it works even better in tty. + + continue make it work. I think it is useful to have a panel for tty emacs. + ** 2023-08-16 create a panel using emacs facility. make it work in terminal emacs. @@ -1316,6 +1583,22 @@ https://github.com/melpa/melpa/blob/master/CONTRIBUTING.org#preparing-a-pull-req fixed. * wontfix :entry: +** 2024-05-25 zero-input bug: +haikeyi += += += += +when there is no next page, press = should stay at candidate page 3. + +haikeyi +- +when there is no prev page, press - should stay at page 1. + +- not reproducible in posframe panel. +- not reproducible in zero-panel. +- it's a specific bug in popup panel. + ** 2020-02-20 gitlab doesn't render README as org-mode file. github does render it. should I name it README.org? diff --git a/test-popup.el b/test-popup.el deleted file mode 100644 index 6b53806..0000000 --- a/test-popup.el +++ /dev/null @@ -1,6 +0,0 @@ -(require 'popup) -(setq popup (popup-create (point) 10 10)) -(popup-set-list popup '("Foo" "Bar" "Baz")) -(popup-draw popup) -;; do something here -(popup-delete popup) diff --git a/zero-input-framework.el b/zero-input-framework.el index 3d83d4e..537e811 100644 --- a/zero-input-framework.el +++ b/zero-input-framework.el @@ -133,7 +133,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.9.1") +(setq zero-input-version "2.10.0") + +(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) @@ -252,7 +263,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 @@ -351,7 +362,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 @@ -366,7 +377,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) @@ -500,17 +511,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) - (setq zero-input-current-page (1- zero-input-current-page)) - (zero-input-show-candidates))) + (if (> zero-input-current-page 0) + (progn + (setq zero-input-current-page (1- zero-input-current-page)) + (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))) - (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)))) + (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)) + (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." @@ -711,7 +728,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)) @@ -725,13 +749,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)) @@ -739,6 +769,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)) @@ -757,20 +788,24 @@ Otherwise, show Zero." :init-value nil :lighter (:eval (zero-input-modeline-string)) :keymap zero-input-mode-map - ;; 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) - (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)) - (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)) + ;; 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)) + (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. diff --git a/zero-input-panel-minibuffer.el b/zero-input-panel-minibuffer.el new file mode 100644 index 0000000..4668d81 --- /dev/null +++ b/zero-input-panel-minibuffer.el @@ -0,0 +1,181 @@ +;;; zero-input-panel-minibuffer.el --- minibuffer based zero-input panel implementation. -*- lexical-binding: t -*- + +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;; URL: https://gitlab.emacsos.com/sylecn/zero-el +;; Package-Requires: ((emacs "24.3")) + +;;; Commentary: + +;; Implements a zero-input panel service using Emacs minibuffer. This service +;; works in tty, xorg and wayland sessions. +;; +;; To use this panel, add in your ~/.emacs.d/init.el file, +;; +;; (require 'zero-input-panel-minibuffer) +;; (zero-input-panel-minibuffer-init) +;; +;; If the service failed to start, quit the running zero-input panel service +;; first: +;; +;; (zero-input-panel-quit) + +;;; Code: + +(require 'dbus) +(require 'zero-input) + +;; 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) + +;;; zero-input-panel-minibuffer.el ends here diff --git a/zero-input-panel-posframe.el b/zero-input-panel-posframe.el index b1a1c26..45eb6a8 100644 --- a/zero-input-panel-posframe.el +++ b/zero-input-panel-posframe.el @@ -125,6 +125,7 @@ Argument CANDIDATE-COUNT how many candidates to show." (interactive) (when (posframe-workable-p) (dbus-unregister-service :session zero-input-panel-dbus-service-known-name)) + (setq zero-input-panel-is-ephemeral nil) :ignore) (defun zero-input-panel-posframe-init () @@ -153,7 +154,8 @@ Argument CANDIDATE-COUNT how many candidates to show." "/com/emacsos/zero/Panel1" "com.emacsos.zero.Panel1.PanelInterface" (car method) - (cdr method)))))) + (cdr method))) + (setq zero-input-panel-is-ephemeral nil)))) (provide 'zero-input-panel-posframe) diff --git a/zero-input-reload-all.el b/zero-input-reload-all.el index 3c0b429..fd50838 100644 --- a/zero-input-reload-all.el +++ b/zero-input-reload-all.el @@ -35,12 +35,14 @@ SOURCE-DIR where to find the zero source dir." "zero-input-pinyin.el" "zero-input-pinyin-test.el" "zero-input-panel-posframe.el" + "zero-input-panel-minibuffer.el" )) (byte-compile-disable-warning 'docstrings) (byte-compile-file (concat source-dir f) t)))) (defun zero-input-reload-all (&optional source-dir) - "Recompile and load all zero files." + "Recompile and load all zero files. +Optional argument SOURCE-DIR path to zero-input source dir." (interactive) (let ((source-dir (or source-dir "~/lisp/elisp/zero/")) (byte-compile-warnings nil)) @@ -57,6 +59,8 @@ SOURCE-DIR where to find the zero source dir." "zero-input-pinyin-test.elc" "zero-input-table.el" "zero-input-table-test.el" + "zero-input-panel-posframe.elc" + "zero-input-panel-minibuffer.elc" )) (load-file (concat source-dir f))))) diff --git a/zero-input.el b/zero-input.el index bb2c380..d369322 100644 --- a/zero-input.el +++ b/zero-input.el @@ -12,7 +12,7 @@ ;; See the License for the specific language governing permissions and ;; limitations under the License. -;; Version: 2.9.1 +;; Version: 2.10.0 ;; URL: https://gitlab.emacsos.com/sylecn/zero-el ;; Package-Requires: ((emacs "24.3") (s "1.2.0")) @@ -253,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.9.1") +(setq zero-input-version "2.10.0") + +(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) @@ -372,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 @@ -471,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 @@ -486,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) @@ -620,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) - (setq zero-input-current-page (1- zero-input-current-page)) - (zero-input-show-candidates))) + (if (> zero-input-current-page 0) + (progn + (setq zero-input-current-page (1- zero-input-current-page)) + (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))) - (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)))) + (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)) + (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." @@ -831,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)) @@ -845,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)) @@ -859,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)) @@ -877,20 +908,24 @@ Otherwise, show Zero." :init-value nil :lighter (:eval (zero-input-modeline-string)) :keymap zero-input-mode-map - ;; 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) - (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)) - (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)) + ;; 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)) + (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. -- GitLab