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