;;; meow-util.el --- Utilities for Meow -*- 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:
;; Utilities for Meow.
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'color)
(require 'meow-var)
(require 'meow-keymap)
(require 'meow-face)
;; Modes
(defvar meow-normal-mode)
(declare-function meow--remove-match-highlights "meow-visual")
(declare-function meow--remove-expand-highlights "meow-visual")
(declare-function meow--remove-search-highlight "meow-visual")
(declare-function meow-insert-mode "meow-core")
(declare-function meow-motion-mode "meow-core")
(declare-function meow-normal-mode "meow-core")
(declare-function meow-keypad-mode "meow-core")
(declare-function meow-beacon-mode "meow-core")
(declare-function meow-mode "meow-core")
(declare-function meow--keypad-format-keys "meow-keypad")
(declare-function meow--keypad-format-prefix "meow-keypad")
(declare-function meow-minibuffer-quit "meow-command")
(defun meow--execute-kbd-macro (kbd-macro)
"Execute KBD-MACRO."
(when-let ((ret (key-binding (read-kbd-macro kbd-macro))))
(cond
((commandp ret)
(call-interactively ret))
((and (not meow-use-keypad-when-execute-kbd) (keymapp ret))
(set-transient-map ret nil nil))
((and meow-use-keypad-when-execute-kbd (keymapp ret))
(meow-keypad-start-with kbd-macro)))))
(defun meow-insert-mode-p ()
"Whether insert mode is enabled."
(bound-and-true-p meow-insert-mode))
(defun meow-motion-mode-p ()
"Whether motion mode is enabled."
(bound-and-true-p meow-motion-mode))
(defun meow-normal-mode-p ()
"Whether normal mode is enabled."
(bound-and-true-p meow-normal-mode))
(defun meow-keypad-mode-p ()
"Whether keypad mode is enabled."
(bound-and-true-p meow-keypad-mode))
(defun meow-beacon-mode-p ()
"Whether keypad mode is enabled."
(bound-and-true-p meow-beacon-mode))
(defun meow--disable-current-state ()
(when meow--current-state
(funcall (alist-get meow--current-state meow-state-mode-alist) -1)
(setq meow--current-state nil)))
(defun meow--read-cursor-face-color (face)
"Read cursor color from face."
(let ((f (face-attribute face :inherit)))
(if (equal 'unspecified f)
(let ((color (face-attribute face :background)))
(if (equal 'unspecified color)
(face-attribute 'default :foreground)
color))
(meow--read-cursor-face-color f))))
(defun meow--set-cursor-type (type)
(if (display-graphic-p)
(setq cursor-type type)
(let* ((shape (or (car-safe type) type))
(param (cond ((eq shape 'bar) "6")
((eq shape 'hbar) "4")
(t "2"))))
(send-string-to-terminal (concat "\e[" param " q")))))
(defun meow--set-cursor-color (face)
"Set cursor color by face."
(let ((color (meow--read-cursor-face-color face)))
(unless (equal (frame-parameter nil 'cursor-color) color)
(set-cursor-color color))))
(defun meow--update-cursor-default ()
"Set default cursor type and color"
(meow--set-cursor-type meow-cursor-type-default)
(meow--set-cursor-color 'meow-unknown-cursor))
(defun meow--update-cursor-insert ()
"Set insert cursor type and color"
(meow--set-cursor-type meow-cursor-type-insert)
(meow--set-cursor-color 'meow-insert-cursor))
(defun meow--update-cursor-normal ()
"Set normal cursor type and color"
(if meow-use-cursor-position-hack
(unless (use-region-p)
(meow--set-cursor-type meow-cursor-type-normal))
(meow--set-cursor-type meow-cursor-type-normal))
(meow--set-cursor-color 'meow-normal-cursor))
(defun meow--update-cursor-motion ()
"Set motion cursor type and color"
(meow--set-cursor-type meow-cursor-type-motion)
(meow--set-cursor-color 'meow-motion-cursor))
(defun meow--update-cursor-beacon ()
"Set beacon cursor type and color"
(meow--set-cursor-type meow-cursor-type-beacon)
(meow--set-cursor-color 'meow-beacon-cursor))
(defun meow--cursor-null-p ()
"Check if cursor-type is null"
(null cursor-type))
(defun meow--update-cursor ()
"Update cursor type according to the current state.
This uses the variable meow-update-cursor-functions-alist, finds the first
item in which the car evaluates to true, and runs the cdr. The last item's car
in the list will always evaluate to true."
(with-current-buffer (window-buffer)
(thread-last meow-update-cursor-functions-alist
(cl-remove-if-not (lambda (el) (funcall (car el))))
(cdar)
(funcall))))
(defun meow--get-state-name (state)
"Get the name of the current state.
Looks up the state in meow-replace-state-name-list"
(alist-get state meow-replace-state-name-list))
(defun meow--render-indicator ()
"Renders a short indicator based on the current state."
(when (bound-and-true-p meow-global-mode)
(let* ((state (meow--current-state))
(state-name (meow--get-state-name state))
(indicator-face (alist-get state meow-indicator-face-alist)))
(if state-name
(propertize
(format " %s " state-name)
'face indicator-face)
""))))
(defun meow--update-indicator ()
(let ((indicator (meow--render-indicator)))
(setq-local meow--indicator indicator)))
(defun meow--state-p (state)
(funcall (intern (concat "meow-" (symbol-name state) "-mode-p"))))
(defun meow--current-state ()
meow--current-state)
(defun meow--should-update-display-p ()
(cl-case meow-update-display-in-macro
((t) t)
((except-last-macro)
(or (null executing-kbd-macro)
(not (equal executing-kbd-macro last-kbd-macro))))
((nil)
(null executing-kbd-macro))))
(defun meow-update-display ()
(when (meow--should-update-display-p)
(meow--update-indicator)
(meow--update-cursor)))
(defun meow--switch-state (state &optional no-hook)
"Switch to STATE execute 'meow-switch-state-hook unless NO-HOOK is non-nil."
(unless (eq state (meow--current-state))
(let ((mode (alist-get state meow-state-mode-alist)))
(funcall mode 1))
(unless (bound-and-true-p no-hook)
(run-hook-with-args 'meow-switch-state-hook state))))
(defun meow--exit-keypad-state ()
"Exit keypad state."
(meow-keypad-mode -1)
(when (and (eq 'beacon meow--keypad-previous-state)
meow--current-state)
(meow--beacon-apply-command meow--keypad-this-command))
(when meow--keypad-previous-state
(meow--switch-state meow--keypad-previous-state)))
(defun meow--direction-forward ()
"Make the selection towards forward."
(when (and (region-active-p) (< (point) (mark)))
(exchange-point-and-mark)))
(defun meow--direction-backward ()
"Make the selection towards backward."
(when (and (region-active-p) (> (point) (mark)))
(exchange-point-and-mark)))
(defun meow--direction-backward-p ()
"Return whether we have a backward selection."
(and (region-active-p)
(> (mark) (point))))
(defun meow--direction-forward-p ()
"Return whether we have a forward selection."
(and (region-active-p)
(<= (mark) (point))))
(defun meow--selection-type ()
"Return current selection type."
(when (region-active-p)
(car meow--selection)))
(defun meow--in-string-p (&optional pos)
"Return whether POS or current position is in string."
(save-mark-and-excursion
(when pos (goto-char pos))
(nth 3 (syntax-ppss))))
(defun meow--in-comment-p (&optional pos)
"Return whether POS or current position is in string."
(save-mark-and-excursion
(when pos (goto-char pos))
(nth 4 (syntax-ppss))))
(defun meow--sum (sequence)
(seq-reduce #'+ sequence 0))
(defun meow--reduce (fn init sequence)
(seq-reduce fn sequence init))
(defun meow--string-pad (s len pad &optional start)
(if (<= len (length s))
s
(if start
(concat (make-string (- len (length s)) pad) s)
(concat s (make-string (- len (length s)) pad)))))
(defun meow--truncate-string (len s ellipsis)
(if (> (length s) len)
(concat (substring s 0 (- len (length ellipsis))) ellipsis)
s))
(defun meow--string-join (sep s)
(string-join s sep))
(defun meow--prompt-symbol-and-words (prompt beg end)
"Completion with PROMPT for symbols and words from BEG to END."
(let ((completions))
(save-mark-and-excursion
(goto-char beg)
(while (re-search-forward "\\_<\\(\\sw\\|\\s_\\)+\\_>" end t)
(let ((result (match-string-no-properties 0)))
(when (>= (length result) meow-visit-collect-min-length)
(if meow-visit-sanitize-completion
(push (cons result (format "\\_<%s\\_>" (regexp-quote result))) completions)
(push (format "\\_<%s\\_>" (regexp-quote result)) completions))))))
(setq completions (delete-dups completions))
(let ((selected (completing-read prompt completions nil nil)))
(if meow-visit-sanitize-completion
(or (cdr (assoc selected completions))
(regexp-quote selected))
selected))))
(defun meow--on-window-state-change (&rest _args)
"Update cursor style after switching window."
(meow--update-cursor)
(meow--update-indicator))
(defun meow--on-exit ()
(unless (display-graphic-p)
(send-string-to-terminal "\e[2 q")))
(defun meow--get-indent ()
"Get indent of current line."
(save-mark-and-excursion
(back-to-indentation)
(- (point) (line-beginning-position))))
(defun meow--empty-line-p ()
"Whether current line is empty."
(string-match-p "^ *$" (buffer-substring-no-properties
(line-beginning-position)
(line-end-position))))
(defun meow--ordinal (n)
(cl-case n
((1) "1st")
((2) "2nd")
((3) "3rd")
(t (format "%dth" n))))
(defun meow--allow-modify-p ()
(and (not buffer-read-only)
(not meow--temp-normal)))
(defun meow--with-universal-argument-p (arg)
(equal '(4) arg))
(defun meow--with-negative-argument-p (arg)
(< (prefix-numeric-value arg) 0))
(defun meow--with-shift-p ()
(member 'shift last-input-event))
(defun meow--bounds-with-type (type thing)
(when-let ((bounds (bounds-of-thing-at-point thing)))
(cons type bounds)))
(defun meow--push-search (search)
(unless (string-equal search (car regexp-search-ring))
(add-to-history 'regexp-search-ring search regexp-search-ring-max)))
(defun meow--remove-text-properties (text)
(set-text-properties 0 (length text) nil text)
text)
(defun meow--toggle-relative-line-number ()
(when display-line-numbers
(if (bound-and-true-p meow-insert-mode)
(setq display-line-numbers t)
(setq display-line-numbers 'relative))))
(defun meow--render-char-thing-table ()
(let* ((ww (frame-width))
(w 25)
(col (min 5 (/ ww w))))
(thread-last
meow-char-thing-table
(seq-group-by #'cdr)
(seq-sort-by #'car #'string-lessp)
(seq-map-indexed
(lambda (th-pairs idx)
(let* ((th (car th-pairs))
(pairs (cdr th-pairs))
(pre (thread-last
pairs
(mapcar (lambda (it) (char-to-string (car it))))
(meow--string-join " "))))
(format "%s%s%s%s"
(propertize
(meow--string-pad pre 8 32 t)
'face 'font-lock-constant-face)
(propertize " → " 'face 'font-lock-comment-face)
(propertize
(meow--string-pad (symbol-name th) 13 32 t)
'face 'font-lock-function-name-face)
(if (= (1- col) (mod idx col))
"\n"
" ")))))
(string-join)
(string-trim-right))))
(defun meow--transpose-lists (lists)
(when lists
(let* ((n (seq-max (mapcar #'length lists)))
(rst (apply #'list (make-list n ()))))
(mapc (lambda (l)
(seq-map-indexed
(lambda (it idx)
(cl-replace rst
(list (cons it (nth idx rst)))
:start1 idx
:end1 (1+ idx)))
l))
lists)
(mapcar #'reverse rst))))
(defun meow--get-event-key (e)
(if (and (integerp (event-basic-type e))
(member 'shift (event-modifiers e)))
(upcase (event-basic-type e))
(event-basic-type e)))
(defun meow--ensure-visible ()
(let ((overlays (overlays-at (1- (point))))
ov expose)
(while (setq ov (pop overlays))
(if (and (invisible-p (overlay-get ov 'invisible))
(setq expose (overlay-get ov 'isearch-open-invisible)))
(funcall expose ov)))))
(defun meow--minibuffer-setup ()
(local-set-key (kbd "<escape>") #'meow-minibuffer-quit)
(setq-local meow-normal-mode nil)
(when (or (member this-command meow-grab-fill-commands)
(member meow--keypad-this-command meow-grab-fill-commands))
(when-let ((s (meow--second-sel-get-string)))
(insert s))))
(defun meow--parse-string-to-keypad-keys (str)
(let ((strs (split-string str " ")))
(thread-last
strs
(mapcar
(lambda (str)
(cond
((string-prefix-p "C-M-" str)
(cons 'both (substring str 4)))
((string-prefix-p "C-" str)
(cons 'control (substring str 2)))
((string-prefix-p "M-" str)
(cons 'meta (substring str 2)))
(t
(cons 'literal str)))))
(reverse))))
(defun meow--parse-input-event (e)
(cond
((equal e 32)
"SPC")
((characterp e)
(string e))
((equal 'tab e)
"TAB")
((equal 'return e)
"RET")
((equal 'backspace e)
"DEL")
((equal 'escape e)
"ESC")
((symbolp e)
(format "<%s>" e))
(t nil)))
(defun meow--save-origin-commands ()
(cl-loop for key-code being the key-codes of meow-motion-state-keymap do
(ignore-errors
(let* ((key (meow--parse-input-event key-code))
(cmd (key-binding (kbd key))))
(when (and (commandp cmd)
(not (equal cmd 'undefined)))
(let ((rebind-key (concat meow-motion-remap-prefix key)))
(local-set-key (kbd rebind-key) cmd)))))))
(defun meow--prepare-region-for-kill ()
(when (and (equal 'line (cdr (meow--selection-type)))
(meow--direction-forward-p)
(< (point) (point-max)))
(forward-char 1)))
(defun meow--prepare-string-for-kill-append (s)
(let ((curr (current-kill 0 nil)))
(cl-case (cdr (meow--selection-type))
((line) (concat (unless (string-suffix-p "\n" curr) "\n")
(string-trim-right s "\n")))
((word block) (concat (unless (string-suffix-p " " curr) " ")
(string-trim s " " "\n")))
(t s))))
(defun meow--event-key (e)
(let ((c (event-basic-type e)))
(if (and (char-or-string-p c)
(member 'shift (event-modifiers e)))
(upcase c)
c)))
(defun meow--parse-def (def)
"Return a command or keymap for DEF.
If DEF is a string, return a command that calls the command or keymap
that bound to DEF. Otherwise, return DEF."
(if (stringp def)
(let ((cmd-name (gensym 'meow-dispatch_)))
;; dispatch command
(defalias cmd-name
(lambda ()
(:documentation
(format "Execute the command which is bound to %s." def))
(interactive)
(meow--execute-kbd-macro def)))
(put cmd-name 'meow-dispatch def)
cmd-name)
def))
(defun meow--second-sel-set-string (string)
(cond
((meow--second-sel-buffer)
(with-current-buffer (overlay-buffer mouse-secondary-overlay)
(goto-char (overlay-start mouse-secondary-overlay))
(delete-region (overlay-start mouse-secondary-overlay) (overlay-end mouse-secondary-overlay))
(insert string)))
((markerp mouse-secondary-start)
(with-current-buffer (marker-buffer mouse-secondary-start)
(goto-char (marker-position mouse-secondary-start))
(insert string)))))
(defun meow--second-sel-get-string ()
(when (meow--second-sel-buffer)
(with-current-buffer (overlay-buffer mouse-secondary-overlay)
(buffer-substring-no-properties
(overlay-start mouse-secondary-overlay)
(overlay-end mouse-secondary-overlay)))))
(defun meow--second-sel-buffer ()
(and (overlayp mouse-secondary-overlay)
(overlay-buffer mouse-secondary-overlay)))
(defun meow--second-sel-bound ()
(and (secondary-selection-exist-p)
(cons (overlay-start mouse-secondary-overlay)
(overlay-end mouse-secondary-overlay))))
(defmacro meow--with-selection-fallback (&rest body)
`(if (region-active-p)
(progn ,@body)
(meow--selection-fallback)))
(defmacro meow--wrap-collapse-undo (&rest body)
"Like `progn' but perform BODY with undo collapsed."
(declare (indent 0) (debug t))
(let ((handle (make-symbol "--change-group-handle--"))
(success (make-symbol "--change-group-success--")))
`(let ((,handle (prepare-change-group))
;; Don't truncate any undo data in the middle of this.
(undo-outer-limit nil)
(undo-limit most-positive-fixnum)
(undo-strong-limit most-positive-fixnum)
(,success nil))
(unwind-protect
(progn
(activate-change-group ,handle)
(prog1 ,(macroexp-progn body)
(setq ,success t)))
(if ,success
(progn
(accept-change-group ,handle)
(undo-amalgamate-change-group ,handle))
(cancel-change-group ,handle))))))
(defun meow--highlight-pre-command ()
(unless (member this-command '(meow-search))
(meow--remove-match-highlights))
(meow--remove-expand-highlights)
(meow--remove-search-highlight))
(defun meow--remove-fake-cursor (rol)
(when (overlayp rol)
(when-let ((ovs (overlay-get rol 'meow-face-cursor)))
(mapc (lambda (o) (when (overlayp o) (delete-overlay o)))
ovs))))
(defvar meow--region-cursor-faces '(meow-region-cursor-1
meow-region-cursor-2
meow-region-cursor-3))
(defun meow--add-fake-cursor (rol)
(if (and meow-use-enhanced-selection-effect
(or (meow-normal-mode-p)
(meow-beacon-mode-p)))
(when (overlayp rol)
(let ((start (overlay-start rol))
(end (overlay-end rol)))
(unless (= start end)
(let (ovs)
(if (meow--direction-forward-p)
(progn
(let ((p end)
(i 0))
(while (and (> p start)
(< i 3))
(let ((ov (make-overlay (1- p) p)))
(overlay-put ov 'face (nth i meow--region-cursor-faces))
(overlay-put ov 'priority 10)
(overlay-put ov 'window (overlay-get rol 'window))
(cl-decf p)
(cl-incf i)
(push ov ovs)))))
(let ((p start)
(i 0))
(while (and (< p end)
(< i 3))
(let ((ov (make-overlay p (1+ p))))
(overlay-put ov 'face (nth i meow--region-cursor-faces))
(overlay-put ov 'priority 10)
(overlay-put ov 'window (overlay-get rol 'window))
(cl-incf p)
(cl-incf i)
(push ov ovs)))))
(overlay-put rol 'meow-face-cursor ovs)))
rol))
rol))
(defun meow--redisplay-highlight-region-function (start end window rol)
(when (and (or (meow-normal-mode-p)
(meow-beacon-mode-p))
(equal window (selected-window)))
(if (use-region-p)
(meow--set-cursor-type meow-cursor-type-region-cursor)
(meow--set-cursor-type meow-cursor-type-normal)))
(when meow-use-enhanced-selection-effect
(meow--remove-fake-cursor rol))
(thread-first
(funcall meow--backup-redisplay-highlight-region-function start end window rol)
(meow--add-fake-cursor)))
(defun meow--redisplay-unhighlight-region-function (rol)
(meow--remove-fake-cursor rol)
(when (and (overlayp rol)
(equal (overlay-get rol 'window) (selected-window))
(or (meow-normal-mode-p)
(meow-beacon-mode-p)))
(meow--set-cursor-type meow-cursor-type-normal))
(funcall meow--backup-redisplay-unhighlight-region-function rol))
(defun meow--mix-color (color1 color2 n)
(mapcar (lambda (c) (apply #'color-rgb-to-hex c))
(color-gradient (color-name-to-rgb color1)
(color-name-to-rgb color2)
n)))
(defun meow--beacon-inside-secondary-selection ()
(and
(secondary-selection-exist-p)
(< (overlay-start mouse-secondary-overlay)
(overlay-end mouse-secondary-overlay))
(<= (overlay-start mouse-secondary-overlay)
(point)
(overlay-end mouse-secondary-overlay))))
(defun meow--narrow-secondary-selection ()
(narrow-to-region (overlay-start mouse-secondary-overlay)
(overlay-end mouse-secondary-overlay)))
(defun meow--hack-cursor-pos (pos)
"Hack the point when `meow-use-cursor-position-hack' is enabled."
(if meow-use-cursor-position-hack
(1- pos)
pos))
(defun meow--remove-modeline-indicator ()
(setq-default mode-line-format
(cl-remove '(:eval (meow-indicator)) mode-line-format
:test 'equal)))
(defun meow--init-buffers ()
"Enable meow in existing buffers."
(dolist (buf (buffer-list))
(unless (minibufferp buf)
(with-current-buffer buf
(setq-local meow-normal-mode 1)))))
(defun meow--get-leader-keymap ()
(cond
((keymapp meow-keypad-leader-dispatch)
meow-keypad-leader-dispatch)
((null meow-keypad-leader-dispatch)
(alist-get 'leader meow-keymap-alist))))
(provide 'meow-util)
;;; meow-util.el ends here