;;; meow-keypad.el --- Meow keypad mode -*- lexical-binding: t -*-
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License
;; as published by the Free Software Foundation; either version 3
;; of the License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;; Keypad state is a special state to simulate C-x and C-c key sequences.
;; There are three commands:
;;
;; meow-keypad-start
;; Enter keypad state, and simulate this key with Control modifier.
;;
;; meow-keypad-self-insert
;; This command is bound to every single key in keypad state.
;; The rules,
;; - If current key is SPC, the next will be considered without modifier.
;; - If current key is m, the next will be considered with Meta modifier.
;; - Other keys, or SPC and m after a prefix, means append a key input, by default, with Control modifier.
;;
;; meow-keypad-undo
;; Remove the last input, if there's no input in the sequence, exit the keypad state.
;;; Code:
(require 'subr-x)
(require 'meow-var)
(require 'meow-util)
(require 'meow-helpers)
(defun meow--keypad-format-upcase (k)
"Return S-k for upcase k."
(let ((case-fold-search nil))
(if (and (stringp k)
(string-match-p "^[A-Z]$" k))
(format "S-%s" (downcase k))
k)))
(defun meow--keypad-format-key-1 (key)
"Return a display format for input KEY."
(cl-case (car key)
(meta (format "M-%s" (cdr key)))
(control (format "C-%s" (meow--keypad-format-upcase (cdr key))))
(both (format "C-M-%s" (meow--keypad-format-upcase (cdr key))))
(literal (cdr key))))
(defun meow--keypad-format-prefix ()
"Return a display format for current prefix."
(cond
((equal '(4) meow--prefix-arg)
"C-u ")
(meow--prefix-arg
(format "%s " meow--prefix-arg))
(t "")))
(defun meow--keypad-lookup-key (keys)
(let* ((overriding-local-map meow--keypad-base-keymap)
(keybind (key-binding keys)))
(unless (and (meow--is-self-insertp keybind)
(not meow-keypad-self-insert-undefined))
keybind)))
(defun meow--keypad-has-sub-meta-keymap-p ()
(and (not meow--use-literal)
(not meow--use-both)
(not meow--use-meta)
(or (not meow--keypad-keys)
(let* ((key-str (meow--keypad-format-keys nil))
(keymap (meow--keypad-lookup-key (kbd key-str))))
(and (keymapp keymap)
(lookup-key keymap ""))))))
(defun meow--keypad-format-keys (&optional prompt)
"Return a display format for current input keys."
(let ((result ""))
(setq result
(thread-first
(mapcar #'meow--keypad-format-key-1 meow--keypad-keys)
(reverse)
(string-join " ")))
(cond
(meow--use-both
(setq result
(if (string-empty-p result)
"C-M-"
(concat result " C-M-"))))
(meow--use-meta
(setq result
(if (string-empty-p result)
"M-"
(concat result " M-"))))
(meow--use-literal
(setq result (concat result " ○")))
(prompt
(setq result (concat result " C-"))))
result))
(defun meow--keypad-quit ()
"Quit keypad state."
(setq meow--keypad-keys nil
meow--use-literal nil
meow--use-meta nil
meow--use-both nil
meow--keypad-help nil)
(setq overriding-local-map nil)
(meow--exit-keypad-state))
(defun meow-keypad-quit ()
"Quit keypad state."
(interactive)
(setq this-command last-command)
(when meow-keypad-message
(message "KEYPAD exit"))
(meow--keypad-quit))
(defun meow--make-keymap-for-describe (keymap control)
(let ((km (make-keymap)))
(suppress-keymap km t)
(when (keymapp keymap)
(map-keymap
(lambda (key def)
(unless (member (event-basic-type key) '(127))
(when (if control (member 'control (event-modifiers key))
(not (member 'control (event-modifiers key))))
(define-key km (vector (meow--get-event-key key))
(funcall meow-keypad-get-title-function def)))))
keymap))
km))
(defun meow--keypad-get-keymap-for-describe ()
(let* ((input (thread-first
(mapcar #'meow--keypad-format-key-1 meow--keypad-keys)
(reverse)
(string-join " ")))
(meta-both-keymap (meow--keypad-lookup-key
(read-kbd-macro
(if (string-blank-p input)
"ESC"
(concat input " ESC"))))))
(cond
(meow--use-meta
(when meta-both-keymap
(meow--make-keymap-for-describe meta-both-keymap nil)))
(meow--use-both
(when meta-both-keymap
(meow--make-keymap-for-describe meta-both-keymap t)))
(meow--use-literal
(when-let ((keymap (meow--keypad-lookup-key (read-kbd-macro input))))
(when (keymapp keymap)
(meow--make-keymap-for-describe keymap nil))))
;; For leader popup
;; meow-keypad-leader-dispatch can be string, keymap or nil
;; - string, dynamically find the keymap
;; - keymap, just use it
;; - nil, take the one in meow-keymap-alist
;; Leader keymap may contain meow-dispatch commands
;; translated names based on the commands they refer to
((null meow--keypad-keys)
(when-let ((keymap (if (stringp meow-keypad-leader-dispatch)
(meow--keypad-lookup-key (read-kbd-macro meow-keypad-leader-dispatch))
(or meow-keypad-leader-dispatch
(alist-get 'leader meow-keymap-alist)))))
(let ((km (make-keymap)))
(suppress-keymap km t)
(map-keymap
(lambda (key def)
(when (and (not (member 'control (event-modifiers key)))
(not (member key (list meow-keypad-meta-prefix
meow-keypad-ctrl-meta-prefix
meow-keypad-literal-prefix)))
(not (alist-get key meow-keypad-start-keys)))
(let ((keys (vector (meow--get-event-key key))))
(unless (lookup-key km keys)
(define-key km keys (funcall meow-keypad-get-title-function def))))))
keymap)
km)))
(t
(when-let ((keymap (meow--keypad-lookup-key (read-kbd-macro input))))
(when (keymapp keymap)
(let* ((km (make-keymap))
(has-sub-meta (meow--keypad-has-sub-meta-keymap-p))
(ignores (if has-sub-meta
(list meow-keypad-meta-prefix
meow-keypad-ctrl-meta-prefix
meow-keypad-literal-prefix
127)
(list meow-keypad-literal-prefix 127))))
(suppress-keymap km t)
(map-keymap
(lambda (key def)
(unless (member 'control (event-modifiers key))
(unless (member key ignores)
(define-key km (vector (meow--get-event-key key)) (funcall meow-keypad-get-title-function def)))))
keymap)
(map-keymap
(lambda (key def)
(when (member 'control (event-modifiers key))
(unless (member (meow--event-key key) ignores)
(when def
(define-key km (vector (meow--get-event-key key)) (funcall meow-keypad-get-title-function def))))))
keymap)
km)))))))
(defun meow--keypad-display-message ()
(let (overriding-local-map)
(when meow-keypad-describe-keymap-function
(when (or
meow--keypad-keymap-description-activated
(setq meow--keypad-keymap-description-activated
(sit-for meow-keypad-describe-delay t)))
(let ((keymap (meow--keypad-get-keymap-for-describe)))
(funcall meow-keypad-describe-keymap-function keymap))))))
(defun meow--describe-keymap-format (pairs &optional width)
(let* ((fw (or width (frame-width)))
(cnt (length pairs))
(best-col-w nil)
(best-rows nil))
(cl-loop for col from 5 downto 2 do
(let* ((row (1+ (/ cnt col)))
(v-parts (seq-partition pairs row))
(rows (meow--transpose-lists v-parts))
(col-w (thread-last
v-parts
(mapcar
(lambda (col)
(cons (seq-max (or (mapcar (lambda (it) (length (car it))) col) '(0)))
(seq-max (or (mapcar (lambda (it) (length (cdr it))) col) '(0))))))))
;; col-w looks like:
;; ((3 . 2) (4 . 3))
(w (thread-last
col-w
;; 4 is for the width of arrow(3) between key and command
;; and the end tab or newline(1)
(mapcar (lambda (it) (+ (car it) (cdr it) 4)))
(meow--sum))))
(when (<= w fw)
(setq best-col-w col-w
best-rows rows)
(cl-return nil))))
(if best-rows
(thread-last
best-rows
(mapcar
(lambda (row)
(thread-last
row
(seq-map-indexed
(lambda (it idx)
(let* ((key-str (car it))
(def-str (cdr it))
(l-r (nth idx best-col-w))
(l (car l-r))
(r (cdr l-r))
(key (meow--string-pad key-str l 32 t))
(def (meow--string-pad def-str r 32)))
(format "%s%s%s"
key
(propertize " → " 'face 'font-lock-comment-face)
def))))
(meow--string-join " "))))
(meow--string-join "\n"))
(propertize "Frame is too narrow for KEYPAD popup" 'face 'meow-keypad-cannot-display))))
(defun meow-describe-keymap (keymap)
(when (and keymap (not defining-kbd-macro) (not meow--keypad-help))
(let* ((rst))
(map-keymap
(lambda (key def)
(let ((k (if (consp key)
(format "%s .. %s"
(key-description (list (car key)))
(key-description (list (cdr key))))
(key-description (list key)))))
(let (key-str def-str)
(cond
((and (commandp def) (symbolp def))
(setq key-str (propertize k 'face 'font-lock-constant-face)
def-str (propertize (symbol-name def) 'face 'font-lock-function-name-face)))
((symbolp def)
(setq key-str (propertize k 'face 'font-lock-constant-face)
def-str (propertize (concat "+" (symbol-name def)) 'face 'font-lock-keyword-face)))
((functionp def)
(setq key-str (propertize k 'face 'font-lock-constant-face)
def-str (propertize "?closure" 'face 'font-lock-function-name-face)))
(t
(setq key-str (propertize k 'face 'font-lock-constant-face)
def-str (propertize "+prefix" 'face 'font-lock-keyword-face))))
(push (cons key-str def-str) rst))))
keymap)
(setq rst (reverse rst))
(let ((msg (meow--describe-keymap-format rst)))
(let ((message-log-max)
(max-mini-window-height 1.0))
(save-window-excursion
(with-temp-message
(format "%s\nKEYPAD: %s%s"
msg
(let ((pre (meow--keypad-format-prefix)))
(if (string-blank-p pre)
""
(propertize pre 'face 'font-lock-comment-face)))
(propertize (meow--keypad-format-keys nil) 'face 'font-lock-string-face))
(sit-for 1000000 t))))))))
(defun meow-keypad-get-title (def)
"Return a symbol as title or DEF.
Returning DEF will result in a generated title."
(if-let ((cmd (and (symbolp def)
(commandp def)
(get def 'meow-dispatch))))
(meow--keypad-lookup-key (kbd cmd))
def))
(defun meow-keypad-undo ()
"Pop the last input."
(interactive)
(setq this-command last-command)
(cond
(meow--use-both
(setq meow--use-both nil))
(meow--use-literal
(setq meow--use-literal nil))
(meow--use-meta
(setq meow--use-meta nil))
(t
(pop meow--keypad-keys)))
(if meow--keypad-keys
(progn
(meow--update-indicator)
(meow--keypad-display-message))
(when meow-keypad-message
(message "KEYPAD exit"))
(meow--keypad-quit)))
(defun meow--keypad-show-message ()
(let ((message-log-max))
(message "KEYPAD%s: %s%s"
(if meow--keypad-help " describe key" "")
(let ((pre (meow--keypad-format-prefix)))
(if (string-blank-p pre)
""
(propertize pre 'face 'font-lock-comment-face)))
(propertize (meow--keypad-format-keys nil) 'face 'font-lock-string-face))))
(defun meow--keypad-try-execute ()
"Try execute command.
If there is a command available on the current key binding,
try replacing the last modifier and try again."
(unless (or meow--use-literal
meow--use-meta
meow--use-both)
(let* ((key-str (meow--keypad-format-keys nil))
(cmd (meow--keypad-lookup-key (read-kbd-macro key-str))))
(cond
((commandp cmd t)
(setq current-prefix-arg meow--prefix-arg
meow--prefix-arg nil)
(if meow--keypad-help
(progn
(meow--keypad-quit)
(describe-function cmd))
(let ((meow--keypad-this-command cmd))
(meow--keypad-quit)
(setq real-this-command cmd
this-command cmd)
(call-interactively cmd))))
((keymapp cmd)
(when meow-keypad-message (meow--keypad-show-message))
(meow--keypad-display-message))
((equal 'control (caar meow--keypad-keys))
(setcar meow--keypad-keys (cons 'literal (cdar meow--keypad-keys)))
(meow--keypad-try-execute))
(t
(setq meow--prefix-arg nil)
(message "%s is undefined" (meow--keypad-format-keys nil))
(meow--keypad-quit))))))
(defun meow-keypad-self-insert ()
"Default command when keypad state is enabled."
(interactive)
(setq this-command last-command)
(when-let ((e (meow--event-key last-input-event))
(key (meow--parse-input-event e)))
(let ((has-sub-meta (meow--keypad-has-sub-meta-keymap-p)))
(cond
(meow--use-literal
(push (cons 'literal key)
meow--keypad-keys)
(setq meow--use-literal nil))
(meow--use-both
(push (cons 'both key) meow--keypad-keys)
(setq meow--use-both nil))
(meow--use-meta
(push (cons 'meta key) meow--keypad-keys)
(setq meow--use-meta nil))
((and (equal e meow-keypad-meta-prefix)
(not meow--use-meta)
has-sub-meta)
(setq meow--use-meta t))
((and (equal e meow-keypad-ctrl-meta-prefix)
(not meow--use-both)
has-sub-meta)
(setq meow--use-both t))
((and (equal e meow-keypad-literal-prefix)
(not meow--use-literal)
meow--keypad-keys)
(setq meow--use-literal t))
(meow--keypad-keys
(push (cons 'control key) meow--keypad-keys))
((alist-get e meow-keypad-start-keys)
(push (cons 'control (meow--parse-input-event
(alist-get e meow-keypad-start-keys)))
meow--keypad-keys))
(meow--keypad-allow-quick-dispatch
(if-let ((keymap (meow--get-leader-keymap)))
(setq meow--keypad-base-keymap keymap)
(setq meow--keypad-keys (meow--parse-string-to-keypad-keys meow-keypad-leader-dispatch)))
(push (cons 'literal key) meow--keypad-keys))
(t
(push (cons 'control key) meow--keypad-keys))))
;; Try execute if the input is valid.
(if (or meow--use-literal
meow--use-meta
meow--use-both)
(progn
(when meow-keypad-message (meow--keypad-show-message))
(meow--keypad-display-message))
(meow--keypad-try-execute))))
(defun meow-keypad ()
"Enter keypad state."
(interactive)
(setq this-command last-command)
(setq meow--keypad-previous-state (meow--current-state))
(meow--switch-state 'keypad)
(setq overriding-local-map meow-keypad-state-keymap
overriding-terminal-local-map nil)
(meow--keypad-display-message))
(defun meow-keypad-start ()
"Enter keypad state with current input as initial key sequences."
(interactive)
(setq this-command last-command)
(setq meow--keypad-previous-state (meow--current-state))
(meow--switch-state 'keypad)
(setq overriding-local-map meow-keypad-state-keymap
overriding-terminal-local-map nil
meow--keypad-allow-quick-dispatch nil)
(call-interactively 'meow-keypad-self-insert))
(defun meow-keypad-start-with (input)
"Enter keypad state with INPUT.
INPUT is a string, stands for initial keys."
(setq meow--keypad-previous-state (meow--current-state))
(meow--switch-state 'keypad)
(setq meow--keypad-keys (meow--parse-string-to-keypad-keys input)
overriding-terminal-local-map nil
overriding-local-map meow-keypad-state-keymap)
(meow--keypad-try-execute))
(defun meow-keypad-describe-key ()
"Describe key via KEYPAD input."
(interactive)
(setq this-command last-command)
(setq overriding-local-map meow-keypad-state-keymap
meow--keypad-help t
meow--keypad-previous-state (meow--current-state))
(meow--switch-state 'keypad)
(meow--keypad-show-message)
(meow--keypad-display-message))
(provide 'meow-keypad)
;;; meow-keypad.el ends here