;;; xah-fly-keys.el --- ergonomic modal keybinding minor mode. -*- coding: utf-8; lexical-binding: t; -*-

;; Copyright © 2013, 2026 by Xah Lee

;; Author: Xah Lee ( http://xahlee.info/ )
;; Maintainer: Xah Lee <xah@xahlee.org>
;; Version: 28.11.20260414134011
;; Created: 2013-09-10
;; Package-Requires: ((emacs "28.3"))
;; Keywords: convenience, vi, vim, ergoemacs, keybinding
;; License: GPL v3.
;; Homepage: http://xahlee.info/emacs/misc/xah-fly-keys.html

;; This file is not part of GNU Emacs.

;;; Commentary:

;; xah-fly-keys is a efficient keybinding for emacs. It is modal like
;; vi, but key choices are based on statistics of command call
;; frequency.

;;; Usage:

;; M-x xah-fly-keys to toggle the mode on/off.

;; Important command/insert mode switch keys:

;; M-x `xah-fly-command-mode-activate'
;; or press <escape>
;; or Alt+Space
;; or Ctrl+Space.
;; Note: if using emacs 28 or before, escape key only works when in emacs is running in graphical user interface mode.

;; M-x `xah-fly-insert-mode-activate'
;; when in command mode, press qwerty letter key f.

;; When in command mode:

;; "f" calls `xah-fly-insert-mode-activate'.

;; Space is a leader key. For example, "SPC r" calls `query-replace'.
;; Press "SPC C-h" to see the full list.

;; "SPC SPC" also activates insertion mode.

;; "SPC RET" calls `execute-extended-command'.

;; "a" calls `execute-extended-command'.

;; The leader key sequence basically supplant ALL emacs commands that
;; starts with C-x key.

;; When using xah-fly-keys, you don't need to press Control or Meta,
;; with the following exceptions:

;; "C-c" for major mode commands.
;; "C-g" for cancel.
;; "C-q" for quoted-insert.
;; "C-h" for getting a list of keys following a prefix/leader key.

;; Leader key

;; You NEVER need to press "C-x"

;; Any emacs command that has a keybinding starting with C-x, has also
;; a key sequence binding in xah-fly-keys. For example,

;; "C-x b" for `switch-to-buffer' is "SPC f"
;; "C-x C-f" for `find-file' is "SPC i e"
;; "C-x n n" for `narrow-to-region' is "SPC l l"

;; The first key we call it leader key. In the above examples, the SPC
;; is the leader key.

;; When in command mode, the "SPC" is a leader key.

;; if you want the following standard keys with Control
;; "C-TAB" `xah-next-user-buffer'
;; "C-S-TAB" `xah-previous-user-buffer'
;; "C-v" paste
;; "C-w" close
;; "C-z" undo
;; "C-n" new
;; "C-o" open
;; "C-s" save
;; "C-S-s" save as
;; "C-S-t" open last closed
;; "C-+" `text-scale-increase'
;; "C--" `text-scale-decrease'

;; add
;; (setq xah-fly-use-control-key t)

;; To disable any change to Control keybinding
;; add this to emacs init
;; (setq xah-fly-use-control-key nil)
;; before loading xah-fly-keys

;; To disable any change to meta keybinding
;; add this to emacs init
;; (setq xah-fly-use-meta-key nil)
;; before loading xah-fly-keys

;; If you have a bug, post on github.

;; For detail about design and other info, see home page at
;; http://xahlee.info/emacs/misc/xah-fly-keys.html

;; If you like this project, paypal me $30 to Xah@XahLee.org

;;; Installation:
;; here's how to manual install
;;
;; put the file xah-fly-keys.el in ~/.emacs.d/lisp/
;; create the dir if doesn't exist.
;;
;; put the following in your emacs init file:
;; (add-to-list 'load-path "~/.emacs.d/lisp/")
;; (require 'xah-fly-keys)
;; (xah-fly-keys-set-layout "qwerty") ; optional
;; (xah-fly-keys 1)
;;
;; possible layout values:

;; adnw (German)
;; azerty
;; azerty-be
;; bepo (French)
;; colemak
;; colemak-dh
;; dvorak
;; engrammer
;; halmak
;; koy (German)
;; minimak
;; neo2 (German)
;; norman
;; programer-dvorak
;; pt-nativo (Brazil)
;; qfmlwy
;; qgmlwb
;; qwerty
;; qwerty-abnt (Brazil)
;; qwerty-no (Norwegian)
;; qwerty-se (Swedish)
;; qwertz
;; qwpr
;; russian
;; workman

;; supported layouts are stored in the variable xah-fly-layout-diagrams

;; s------------------------------
;;; Code:

(require 'dired)
(require 'dired-x)
(require 'seq)

;; s------------------------------

(defgroup xah-fly-keys nil
  "Ergonomic modal keybinding minor mode."
  :group 'keyboard)

(defvar xah-fly-command-mode-activate-hook nil "Hook for `xah-fly-command-mode-activate'")
(defvar xah-fly-insert-mode-activate-hook nil "Hook for `xah-fly-insert-mode-activate'")

(defcustom xah-fly-use-control-key t
  "If true, change many emacs keybinding involving control key.
Keys changed:
Standard shortcut for open, close, copy, paste etc.
Remove many redundant emacs default keys
Must be set before loading xah-fly-keys."
  :type 'boolean)

(defcustom xah-fly-remove-control-x-keys nil
  "If true, remove the whole C-x prefix keybinding.
Must be set before loading xah-fly-keys."
  :type 'boolean)

(defcustom xah-fly-use-meta-key nil
  "If true, change some emacs keybinding involving meta key.
Remove many redundant emacs default keys.
Must be set before loading xah-fly-keys."
  :type 'boolean)

(defcustom xah-fly-use-isearch-arrows t
 "If true, set keys in variable `isearch-mode-map' to use
 left/right arrow for backward/forward occurrences.
up/down for search history.
and C-v is paste.
Must be set before loading xah-fly-keys."
 :type 'boolean)

(defcustom xah-fly-command-mode-hl-line t
 "If true, highlight current line when in command mode.
Must be set before loading xah-fly-keys."
 :type 'boolean)

(defcustom xah-fly-command-mode-cursor-color "red"
  "Cursor color when in command mode.
Value should be a string of color name. See `list-colors-display'.
Default is red.
If set to nil, use the color of current theme.
Must be set before loading xah-fly-keys."
 :type '(string))

(defcustom xah-fly-insert-mode-cursor-color "gray"
  "Cursor color when in insert mode.
Value should be a string of color name. See `list-colors-display'.
Default is gray.
If set to nil, use the color of current theme.
Must be set before loading xah-fly-keys."
 :type '(string))

;; s------------------------------
;; cursor movement

(defun xah-pop-local-mark-ring ()
  "Move cursor to last mark position of current buffer.
Repeat call cycles all positions in `mark-ring'.

After this command is called, press variable `xah-repeat-key' to repeat it.

URL `http://xahlee.info/emacs/emacs/emacs_cycle_local_mark_ring.html'
Created: 2016-04-04
Version: 2025-07-28"
  (interactive)
  (set-mark-command t)
  (set-transient-map
   (let ((xkmap (make-sparse-keymap)))
     (define-key xkmap (kbd (if (boundp 'xah-repeat-key) xah-repeat-key ".")) real-this-command)
     xkmap)))

(defun xah-beginning-of-line-or-block ()
  "Move cursor to beginning of indent or line, end of previous block, in that order.

If `visual-line-mode' is on, beginning of line means visual line.

URL `http://xahlee.info/emacs/emacs/emacs_move_by_paragraph.html'
Created: 2018-06-04
Version: 2024-10-30"
  (interactive)
  (let ((xp (point)))
    (if (or (eq (point) (line-beginning-position))
            (eq last-command this-command))
        (when (re-search-backward "\n[\t\n ]*\n+" nil 1)
          (skip-chars-backward "\n\t ")
          ;; (forward-char)
          )
      (if visual-line-mode
          (beginning-of-visual-line)
        (if (eq major-mode 'eshell-mode)
            (beginning-of-line)
          (back-to-indentation)
          (when (eq xp (point))
            (beginning-of-line)))))))

(defun xah-end-of-line-or-block ()
  "Move cursor to end of line or next block.

• When called first time, move cursor to end of line.
• When called again, move cursor forward by jumping over any sequence of whitespaces containing 2 blank lines.
• if `visual-line-mode' is on, end of line means visual line.

URL `http://xahlee.info/emacs/emacs/emacs_move_by_paragraph.html'
Created: 2018-06-04
Version: 2024-10-30"
  (interactive)
  (if (or (eq (point) (line-end-position))
          (eq last-command this-command))
      (re-search-forward "\n[\t\n ]*\n+" nil 1)
    (if visual-line-mode
        (end-of-visual-line)
      (end-of-line))))

(defun xah-page-up ()
  "Call `scroll-down-command'. (page up key.)
Created: 2024-10-09
Version: 2024-10-09"
  (interactive)
  (progn
    (scroll-down-command)
    (set-transient-map
     (let ((xkmap (make-sparse-keymap)))
       (define-key xkmap (kbd "<up>") #'xah-page-up)
       (define-key xkmap (kbd "<down>") #'xah-page-down)
       xkmap))))

(defun xah-page-down ()
  "Call `scroll-up-command'. (page down key.)
Created: 2024-10-09
Version: 2024-10-09"
  (interactive)
  (progn
    (scroll-up-command)
    (set-transient-map
     (let ((xkmap (make-sparse-keymap)))
       (define-key xkmap (kbd "<up>") #'xah-page-up)
       (define-key xkmap (kbd "<down>") #'xah-page-down)
       xkmap))))

(defun xah-goto-char (&optional zposition)
  "Move cursor to a position in current buffer or next buffer, at a number value under cursor.

If `universal-argument' is called first, goto the specified position.
If `universal-argument' value is 1, ask for a position.
Else use the number under cursor.
If no number is under cursor, ask for a position.

If there is more than one pane (aka emacs window), goto position in that pane.

URL `http://xahlee.info/emacs/emacs/emacs_goto_line_other_buffer.html'
Created: 2025-04-10
Version: 2025-11-13"
  (interactive
   (if current-prefix-arg
       (if (eq 1 (prefix-numeric-value current-prefix-arg))
           (list (read-number "goto position:"))
         (list (prefix-numeric-value current-prefix-arg)))
     nil))
  (let ((xpos
         (if zposition
             zposition
           (let (xbeg xend)
             (skip-chars-backward "0-9")
             (setq xbeg (point))
             (skip-chars-forward "0-9")
             (setq xend (point))
             (if (eq xbeg xend)
                 (progn (read-number "No number under cursor. goto position:"))
               (string-to-number (buffer-substring-no-properties xbeg xend)))))))
    (if (one-window-p)
        nil
      (other-window 1))
    (goto-char xpos)))

(defun xah-goto-line (&optional zline-num)
  "Move cursor to a line in current buffer or next buffer, at a number value under cursor.

If `universal-argument' is called first, goto the specified line number.
If `universal-argument' value is 1, ask for a line number.
Else use the number under cursor.
If no number is under cursor, ask for a line number.

If there is more than one pane (aka emacs window), goto the line number in that pane.

URL `http://xahlee.info/emacs/emacs/emacs_goto_line_other_buffer.html'
Created: 2025-04-10
Version: 2025-11-13"
  (interactive
   (if current-prefix-arg
       (if (eq 1 (prefix-numeric-value current-prefix-arg))
           (list (read-number "goto line number:"))
         (list (prefix-numeric-value current-prefix-arg)))
     nil))
  (let ((xline-num
         (if zline-num
             zline-num
           (let (xbeg xend)
             (skip-chars-backward "0-9")
             (setq xbeg (point))
             (skip-chars-forward "0-9")
             (setq xend (point))
             (if (eq xbeg xend)
                 (progn (read-number "No number under cursor. goto line number:"))
               (string-to-number (buffer-substring-no-properties xbeg xend)))))))
    (if (one-window-p)
        nil
      (other-window 1))
    (goto-char (point-min))
    (forward-line xline-num)))

(defvar xah-brackets '( "“”" "()" "[]" "{}" "<>" "＜＞" "（）" "［］" "｛｝" "⦅⦆" "〚〛" "⦃⦄" "‹›" "«»" "「」" "〈〉" "《》" "【】" "〔〕" "⦗⦘" "『』" "〖〗" "〘〙" "｢｣" "⟦⟧" "⟨⟩" "⟪⟫" "⟮⟯" "⟬⟭" "⌈⌉" "⌊⌋" "⦇⦈" "⦉⦊" "❛❜" "❝❞" "❨❩" "❪❫" "❴❵" "❬❭" "❮❯" "❰❱" "❲❳" "〈〉" "⦑⦒" "⧼⧽" "﹙﹚" "﹛﹜" "﹝﹞" "⁽⁾" "₍₎" "⦋⦌" "⦍⦎" "⦏⦐" "⁅⁆" "⸢⸣" "⸤⸥" "⟅⟆" "⦓⦔" "⦕⦖" "⸦⸧" "⸨⸩" "｟｠")
 "A list of strings, each element is a string of 2 chars, the left bracket and a matching right bracket.
Used by `xah-select-text-in-quote' and others.")

(defconst xah-left-brackets
  (mapcar (lambda (x) (substring x 0 1)) xah-brackets)
  "List of left bracket chars. Each element is a string.")

(defconst xah-right-brackets
  (mapcar (lambda (x) (substring x 1 2)) xah-brackets)
  "List of right bracket chars. Each element is a string.")

(defun xah-backward-left-bracket ()
  "Move cursor to the previous occurrence of left bracket.
The list of brackets to jump to is defined by `xah-left-brackets'.

URL `http://xahlee.info/emacs/emacs/emacs_navigating_keys_for_brackets.html'
Version: 2015-10-01"
  (interactive)
  (re-search-backward (regexp-opt xah-left-brackets) nil t))

(defun xah-forward-right-bracket ()
  "Move cursor to the next occurrence of right bracket.
The list of brackets to jump to is defined by `xah-right-brackets'.

URL `http://xahlee.info/emacs/emacs/emacs_navigating_keys_for_brackets.html'
Version: 2015-10-01"
  (interactive)
  (re-search-forward (regexp-opt xah-right-brackets) nil t))

(defun xah-goto-matching-bracket ()
  "Move cursor to the matching bracket.
If cursor is not on a bracket, call `backward-up-list'.
The list of brackets to jump to is defined by `xah-left-brackets' and `xah-right-brackets'.

URL `http://xahlee.info/emacs/emacs/emacs_navigating_keys_for_brackets.html'
Created: 2016-11-22
Version: 2025-11-18"
  (interactive)
  (if (nth 3 (syntax-ppss))
      (backward-up-list 1 t t)
    (cond
     ((eq (char-after) ?\") (forward-sexp))
     ((eq (char-before) ?\") (backward-sexp))
     ((looking-at (regexp-opt xah-left-brackets))
      (forward-sexp))
     ((if (eq (point-min) (point))
          nil
        (prog2
            (backward-char)
            (looking-at (regexp-opt xah-right-brackets))
          (forward-char)))
      (backward-sexp)
      (while (looking-at "\\s'") (forward-char)))
     (t (backward-up-list 1 t t)))))

(defvar xah-punctuation-regex nil "A regex string for the purpose of moving cursor to a punctuation.")
(setq xah-punctuation-regex "\"")

(defun xah-forward-punct ()
  "Move cursor to the next occurrence of punctuation.
Punctuations is defined by `xah-punctuation-regex'

URL `http://xahlee.info/emacs/emacs/emacs_jump_to_punctuations.html'
Created: 2017-06-26
Version: 2025-07-18"
  (interactive)
  (set-transient-map
   (let ((xkmap (make-sparse-keymap)))
     (define-key xkmap (kbd "<left>") #'xah-backward-punct)
     (define-key xkmap (kbd "<right>") #'xah-forward-punct)
     xkmap))
  (re-search-forward xah-punctuation-regex nil t))

(defun xah-backward-punct ()
  "Move cursor to the previous occurrence of punctuation.
See `xah-forward-punct'

URL `http://xahlee.info/emacs/emacs/emacs_jump_to_punctuations.html'
Created: 2017-06-26
Version: 2025-07-18"
  (interactive)
  (set-transient-map
   (let ((xkmap (make-sparse-keymap)))
     (define-key xkmap (kbd "<left>") #'xah-backward-punct)
     (define-key xkmap (kbd "<right>") #'xah-forward-punct)
     xkmap))
  (re-search-backward xah-punctuation-regex nil t))

(defun xah-sort-lines ()
  "Like `sort-lines' but if no region, do the current block.
Created: 2022-01-22
Version: 2025-03-25"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (sort-lines current-prefix-arg xbeg xend)))

(defun xah-narrow-to-region ()
  "Same as `narrow-to-region', but if no selection, narrow to the current block.
Created: 2022-01-22
Version: 2025-03-25"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (narrow-to-region xbeg xend)))

;; s------------------------------
;; editing commands

(defun xah-copy-line-or-region ()
  "Copy current line or selection.

Copy current line. When called repeatedly, append copy subsequent lines.
Except:

If `universal-argument' is called first, copy whole buffer (respects `narrow-to-region').
If `rectangle-mark-mode' is on, copy the rectangle.
If `region-active-p', copy the region.

URL `http://xahlee.info/emacs/emacs/emacs_copy_cut_current_line.html'
Created: 2010-05-21
Version: 2024-06-19"
  (interactive)
  (cond
   (current-prefix-arg (copy-region-as-kill (point-min) (point-max)))
   ((and (boundp 'rectangle-mark-mode) rectangle-mark-mode)
    (copy-region-as-kill (region-beginning) (region-end) t))
   ((region-active-p) (copy-region-as-kill (region-beginning) (region-end)))
   ((eq last-command this-command)
    (if (eobp)
        nil
      (progn
        (kill-append "\n" nil)
        (kill-append (buffer-substring (line-beginning-position) (line-end-position)) nil)
        (end-of-line)
        (forward-char))))
   ((eobp)
    (if (eq (char-before) 10)
        (progn)
      (progn
        (copy-region-as-kill (line-beginning-position) (line-end-position))
        (end-of-line))))
   (t
    (copy-region-as-kill (line-beginning-position) (line-end-position))
    (end-of-line)
    (forward-char))))

(defun xah-cut-line-or-region ()
  "Cut current line or selection.
If `universal-argument' is called first, cut whole buffer (respects `narrow-to-region').

URL `http://xahlee.info/emacs/emacs/emacs_copy_cut_current_line.html'
Created: 2010-05-21
Version: 2015-06-10"
  (interactive)
  (if current-prefix-arg
      (progn ; not using kill-region because we don't want to include previous kill
        (kill-new (buffer-string))
        (delete-region (point-min) (point-max)))
    (progn (if (region-active-p)
               (kill-region (region-beginning) (region-end) t)
             (kill-region (line-beginning-position) (line-beginning-position 2))))))

(defun xah-copy-all-or-region ()
  "Copy buffer or selection content to `kill-ring'.
Respects `narrow-to-region'.

URL `http://xahlee.info/emacs/emacs/emacs_copy_cut_all_or_region.html'
Created: 2015-08-22
Version: 2015-08-22"
  (interactive)
  (if (region-active-p)
      (progn
        (kill-new (buffer-substring (region-beginning) (region-end)))
        (message "Text selection copied."))
    (progn
      (kill-new (buffer-string))
      (message "Buffer content copied."))))

(defun xah-cut-all-or-region ()
  "Cut buffer or selection content to `kill-ring'.
Respects `narrow-to-region'.

URL `http://xahlee.info/emacs/emacs/emacs_copy_cut_all_or_region.html'
Created: 2015-08-22
Version: 2025-08-06"
  (interactive)
  (if (region-active-p)
      (progn
        (kill-new (buffer-substring (region-beginning) (region-end)))
        (delete-region (region-beginning) (region-end)))
    (progn
      (kill-new (buffer-string))
      (delete-region (point-min) (point-max))
      (message "buffer text cut"))))

(defun xah-paste-or-paste-previous ()
  "Paste. When called repeatedly, paste previous.
This command calls `yank', and if repeated, call `yank-pop'.

If `universal-argument' is called first with a number arg, paste that many times.

URL `http://xahlee.info/emacs/emacs/emacs_paste_or_paste_previous.html'
Created: 2017-07-25
Version: 2020-09-08"
  (interactive)
  (progn
    (when (and delete-selection-mode (region-active-p))
      (delete-region (region-beginning) (region-end)))
    (if current-prefix-arg
        (progn
          (dotimes (_ (prefix-numeric-value current-prefix-arg))
            (yank)))
      (if (eq real-last-command this-command)
          (yank-pop 1)
        (yank)))))

(defalias 'xah-show-copy-history 'xah-show-kill-ring)

(defun xah-show-kill-ring ()
  "Insert all `kill-ring' content in a new buffer named *copy history*.

URL `http://xahlee.info/emacs/emacs/emacs_show_kill_ring.html'
Created: 2019-12-02
Version: 2024-05-07"
  (interactive)
  (let ((xbuf (generate-new-buffer "*copy history*"))
        (inhibit-read-only t))
    (progn
      (switch-to-buffer xbuf)
      (funcall 'fundamental-mode)
      (mapc
       (lambda (x)
         (insert x "\n\nsss97707------------------------------------------------\n\n" ))
       kill-ring))
    (goto-char (point-min))))

(defun xah-move-block-up ()
  "Swap the current text block with the previous.
After this command is called, press <up> or <down> to move. Any other key to exit.
Version: 2022-03-04"
  (interactive)
  (let ((xp0 (point))
        xc1 ; current block begin
        xc2 ; current Block End
        xbeg ; prev Block Begin
        xend ; prev Block end
        )
    (if (re-search-forward "\n[ \t]*\n+" nil 1)
        (setq xc2 (match-beginning 0))
      (setq xc2 (point)))
    (goto-char xp0)
    (if (re-search-backward "\n[ \t]*\n+" nil 1)
        (progn
          (skip-chars-backward "\n \t")
          (setq xend (point))
          (skip-chars-forward "\n \t")
          (setq xc1 (point)))
      (error "No previous block."))
    (goto-char xend)
    (if (re-search-backward "\n[ \t]*\n+" nil 1)
        (progn
          (setq xbeg (match-end 0)))
      (setq xbeg (point)))
    (transpose-regions xbeg xend xc1 xc2)
    (goto-char xbeg)
    (set-transient-map
     (let ((xkmap (make-sparse-keymap)))
       (define-key xkmap (kbd "<up>") #'xah-move-block-up)
       (define-key xkmap (kbd "<down>") #'xah-move-block-down)
       xkmap))))

(defun xah-move-block-down ()
  "Swap the current text block with the next.
After this command is called, press <up> or <down> to move. Any other key to exit.
Version: 2022-03-04"
  (interactive)
  (let ((xp0 (point))
        xc1 ; current block begin
        xc2 ; current Block End
        xn1 ; next Block Begin
        xn2 ; next Block end
        )
    (if (eq (point-min) (point))
        (setq xc1 (point))
      (if (re-search-backward "\n\n+" nil 1)
          (progn
            (setq xc1 (match-end 0)))
        (setq xc1 (point))))
    (goto-char xp0)
    (if (re-search-forward "\n[ \t]*\n+" nil 1)
        (progn
          (setq xc2 (match-beginning 0))
          (setq xn1 (match-end 0)))
      (error "No next block."))
    (if (re-search-forward "\n[ \t]*\n+" nil 1)
        (progn
          (setq xn2 (match-beginning 0)))
      (setq xn2 (point)))
    (transpose-regions xc1 xc2 xn1 xn2)
    (goto-char xn2))
  (set-transient-map
   (let ((xkmap (make-sparse-keymap)))
     (define-key xkmap (kbd "<up>") #'xah-move-block-up)
     (define-key xkmap (kbd "<down>") #'xah-move-block-down)
     xkmap)))

(defun xah-shrink-whitespaces ()
  "Remove whitespaces around cursor .

Shrink neighboring spaces, then newlines, then spaces again, leaving one space or newline at each step, till no more white space.

URL `http://xahlee.info/emacs/emacs/emacs_shrink_whitespace.html'
Created: 2014-10-21
Version: 2023-07-12"
  (interactive)
  (let ((xeol-count 0)
        (xp0 (point))
        xbeg  ; whitespace begin
        xend  ; whitespace end
        (xcharBefore (char-before))
        (xcharAfter (char-after))
        xspace-neighbor-p)
    (setq xspace-neighbor-p (or (eq xcharBefore 32) (eq xcharBefore 9) (eq xcharAfter 32) (eq xcharAfter 9)))
    (skip-chars-backward " \n\t　")
    (setq xbeg (point))
    (goto-char xp0)
    (skip-chars-forward " \n\t　")
    (setq xend (point))
    (goto-char xbeg)
    (while (search-forward "\n" xend t)
      (setq xeol-count (1+ xeol-count)))
    (goto-char xp0)
    (cond
     ((eq xeol-count 0)
      (if (> (- xend xbeg) 1)
          (progn
            (delete-horizontal-space) (insert " "))
        (progn (delete-horizontal-space))))
     ((eq xeol-count 1)
      (if xspace-neighbor-p
          (delete-horizontal-space)
        (progn (delete-space--internal "\n" nil) (insert " "))))
     ((eq xeol-count 2)
      (if xspace-neighbor-p
          (delete-horizontal-space)
        (progn
          (delete-space--internal "\n" nil)
          (insert "\n"))))
     ((> xeol-count 2)
      (if xspace-neighbor-p
          (delete-horizontal-space)
        (progn
          (goto-char xend)
          (search-backward "\n")
          (delete-region xbeg (point))
          (insert "\n"))))
     (t (progn
          (message "nothing done. logic error 40873. shouldn't reach here"))))))

(defun xah-delete-string-backward (&optional DeleteJustQuote)
  "Delete string to the left of cursor.

Cursor must be on the right of a string delimiter.
e.g. \"▮some\" or \"some\"▮
Else, do nothing.

String delimiter is determined by current syntax table. (see `describe-syntax')

If DeleteJustQuote is true, delete only the quotation marks.

Created: 2023-11-12
Version: 2024-06-06"
  (when (prog2 (backward-char) (looking-at "\\s\"") (forward-char))
    (let ((xp0 (point)) xbeg xend)
      ;; xbeg xend are the begin and end pos of the string
      (if (nth 3 (syntax-ppss))
          (setq xbeg (1- xp0)
                xend
                (progn
                  (backward-char)
                  (forward-sexp)
                  (point)))
        (setq xend (point)
              xbeg
              (progn (forward-sexp -1) (point))))
      (if DeleteJustQuote
          (progn (goto-char xend)
                 (delete-char -1)
                 (goto-char xbeg)
                 (delete-char 1))
        (if (eq real-this-command real-last-command)
            (kill-append (delete-and-extract-region xbeg xend) t)
          (kill-region xbeg xend))))))

(defvar xah-smart-delete-dispatch
  '((xah-wolfram-mode . xah-wolfram-smart-delete-backward)
    (xah-html-mode . xah-html-smart-delete-backward))
  "Used by `xah-smart-delete'.
This makes that function behavior dependent on current major-mode.
Value is Alist of pairs, each is of the form
(‹major-mode-name› . ‹function-name›)
If ‹major-mode-name› match current var `major-mode', the paired function is called.
If no major mode matches, `xah-smart-delete' default behavior is used.
Version: 2024-06-05")

(defun xah-smart-delete (&optional BracketOnly SkipDispatch)
  "Smart backward delete.
Typically, delete to the left 1 char or entire bracketed text.
Behavior depends on what's left char, and current `major-mode'.

If `xah-smart-delete-dispatch' match, call the matched function instead.
If region active, delete region.
If cursor left is space tab newline, delete them.
If cursor left is bracket, delete the whole bracket block.
If cursor left is string quote, delete the string.
Else just delete one char to the left.

If `universal-argument' is called first, do not delete bracket's innertext.

In elisp code, arg BracketOnly if true, do not delete innertext. SkipDispatch if true, skip checking `xah-smart-delete-dispatch'.

Created: 2023-07-22
Version: 2025-07-30"
  (interactive (list current-prefix-arg nil))
  (let (xfun)
    (cond
     ((and (not SkipDispatch) (setq xfun (assq major-mode xah-smart-delete-dispatch)))
      (message "calling cdr of %s" xfun)
      (funcall (cdr xfun)))
     ((region-active-p)
      (kill-region (region-beginning) (region-end)))
     ((or
       ;; 32 is space, 9 is tab, 10 is newline
       (eq (char-before) 32)
       (eq (char-before) 10)
       (eq (char-before) 9))
      (if (minibufferp (current-buffer))
          (while (or (eq (char-before) 32) (eq (char-before) 10) (eq (char-before) 9))
            (delete-char -1))
        (let ((xp0 (point)) xbeg xend)
          (skip-chars-backward " \t\n")
          (setq xbeg (point) xend xp0)
          (if (eq real-this-command real-last-command)
              (kill-append (delete-and-extract-region xbeg xend) t)
            (kill-region xbeg xend)))))
     ((prog2 (backward-char) (looking-at "\\s)") (forward-char))
      ;; (message "cursor left is closing bracket")
      (cond
       ;; unmatched bracket, just delete it
       ((not (condition-case nil (scan-sexps (point) -1) (scan-error nil)))
        (warn "There was unmatched bracket: no paired opening bracket on left of cursor")
        (delete-char -1))
       ;; delete just the brackets
       (BracketOnly
        (let ((xp0 (point)) xbeg)
          (forward-sexp -1)
          (while (looking-at "\\s'") (forward-char))
          (setq xbeg (point))
          (goto-char xp0)
          (delete-char -1)
          (goto-char xbeg)
          (delete-char 1)
          (goto-char (- xp0 2))))
       ;; delete the bracket block
       (t
        (let ((xp0 (point)) xbeg xend)
          (forward-sexp -1)
          (while (looking-at "\\s'") (forward-char))
          (setq xbeg (point) xend xp0)
          (if (eq real-this-command real-last-command)
              (kill-append (delete-and-extract-region xbeg xend) t)
            (kill-region xbeg xend))))))
     ((prog2 (backward-char) (looking-at "\\s(") (forward-char))
      ;; (message "cursor left is opening bracket")
      (cond
       ;; unmatched bracket, just delete it
       ((save-excursion
          (backward-char)
          (not (condition-case nil (scan-sexps (point) 1) (scan-error nil))))
        (warn "There was unmatched bracket: no paired closing bracket on right of cursor")
        (delete-char -1))
       ;; delete just the brackets
       (BracketOnly
        (let (xbeg)
          (backward-char)
          (setq xbeg (point))
          (forward-sexp 1)
          (delete-char -1)
          (goto-char xbeg)
          (delete-char 1)))
       ;; delete the bracket block
       (t
        (let (xbeg xend)
          (backward-char)
          (setq xbeg (point))
          (forward-sexp 1)
          (setq xend (point))
          (if (eq real-this-command real-last-command)
              (kill-append (delete-and-extract-region xbeg xend) t)
            (kill-region xbeg xend))))))
     ((prog2 (backward-char) (looking-at "\\s\"") (forward-char))
      (message "calling xah-delete-string-backward")
      (xah-delete-string-backward BracketOnly))
     (t (delete-char -1)))))

(defun xah-change-bracket-pairs (FromChars ToChars)
  "Change bracket pairs to another type or none.
For example, change all parenthesis () to square brackets [].
Works on current block or selection.

In lisp code, FromChars is a string with at least 2 spaces.
e.g.
paren ( )
french angle ‹ ›
double bracket [[ ]]
etc.
It is split by space, and last 2 items are taken as left and right brackets.

ToChars is similar, with a special value of
none
followed by 2 spaces.
,it means replace by empty string.

URL `http://xahlee.info/emacs/emacs/elisp_change_brackets.html'
Created: 2020-11-01
Version: 2025-10-13"
  (interactive
   (let ((xbrackets
          '(
            "square [ ]"
            "brace { }"
            "paren ( )"
            "less greater than < >"
            "QUOTATION MARK \" \""
            "APOSTROPHE ' '"
            "emacs ` '"
            "GRAVE ACCENT ` `"
            "double square [[ ]]"
            "tilde ~ ~"
            "equal = ="
            "double curly quote “ ”"
            "single curly quote ‘ ’"
            "french angle quote ‹ ›"
            "french double angle « »"
            "corner 「 」"
            "white corner 『 』"
            "lenticular 【 】"
            "white lenticular 〖 〗"
            "title angle 〈 〉"
            "double angle 《 》"
            "tortoise 〔 〕"
            "white tortoise 〘 〙"
            "white square 〚 〛"
            "white paren ⦅ ⦆"
            "white curly bracket ⦃ ⦄"
            "pointing angle 〈 〉"
            "angle with dot ⦑ ⦒"
            "curved angle ⧼ ⧽"
            "math square ⟦ ⟧"
            "math angle ⟨ ⟩"
            "math double angle ⟪ ⟫"
            "math flattened parenthesis ⟮ ⟯"
            "math white tortoise shell ⟬ ⟭"
            "heavy single quotation mark ornament ❛ ❜"
            "heavy double turned comma quotation mark ornament ❝ ❞"
            "medium parenthesis ornament ❨ ❩"
            "medium flattened parenthesis ornament ❪ ❫"
            "medium curly ornament ❴ ❵"
            "medium pointing angle ornament ❬ ❭"
            "heavy pointing angle quotation mark ornament ❮ ❯"
            "heavy pointing angle ornament ❰ ❱"
            "square bracket with underbar ⦋ ⦌"
            "none  ")))
     (let ((completion-ignore-case t))
       (list
        (completing-read "Replace this:" xbrackets nil t nil nil (car xbrackets))
        (completing-read "To:" xbrackets nil t nil nil (car (last xbrackets)))))))
  (let (xbeg xend xleft xright xtoL xtoR)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (let ((xsFrom (last (split-string FromChars " ") 2))
          (xsTo (last (split-string ToChars " ") 2)))

      ;; (when (length< xsFrom 3)
      ;; (error "cannot find input brackets %s" xsFrom))

      ;; (when (length< xsTo 3)
      ;;   (message "replace blacket is empty string")
      ;;   (setq xsTo (list "" "" "")))

      (setq xleft (car xsFrom) xright (car (cdr xsFrom))
            xtoL (car xsTo) xtoR (car (cdr xsTo)))

      (save-excursion
        (save-restriction
          (narrow-to-region xbeg xend)
          (let ((case-fold-search nil))
            (if (string-equal xleft xright)
                (let ((xx (regexp-quote xleft)))
                  (goto-char (point-min))
                  (while
                      (re-search-forward
                       (format "%s\\([^%s]+?\\)%s" xx xx xx)
                       nil t)
                    (overlay-put (make-overlay (match-beginning 0) (match-end 0)) 'face 'highlight)
                    (replace-match (concat xtoL "\\1" xtoR) t)))
              (progn
                (progn
                  (goto-char (point-min))
                  (while (search-forward xleft nil t)
                    (overlay-put (make-overlay (match-beginning 0) (match-end 0)) 'face 'highlight)
                    (replace-match xtoL t t)))
                (progn
                  (goto-char (point-min))
                  (while (search-forward xright nil t)
                    (overlay-put (make-overlay (match-beginning 0) (match-end 0)) 'face 'highlight)
                    (replace-match xtoR t t)))))))))))

(defun xah-toggle-letter-case ()
  "Toggle the letter case of current word or selection.
Always cycle in this order: Init Caps, ALL CAPS, all lower.

URL `http://xahlee.info/emacs/emacs/emacs_toggle_letter_case.html'
Created: 2020-06-26
Version: 2024-06-17"
  (interactive)
  (let ((deactivate-mark nil) xbeg xend)
    (if (region-active-p)
        (setq xbeg (region-beginning) xend (region-end))
      (save-excursion
        (skip-chars-backward "[:alnum:]")
        (setq xbeg (point))
        (skip-chars-forward "[:alnum:]")
        (setq xend (point))))
    (when (not (eq last-command this-command))
      (put this-command 'state 0))
    (cond
     ((equal 0 (get this-command 'state))
      (upcase-initials-region xbeg xend)
      (put this-command 'state 1))
     ((equal 1 (get this-command 'state))
      (upcase-region xbeg xend)
      (put this-command 'state 2))
     ((equal 2 (get this-command 'state))
      (downcase-region xbeg xend)
      (put this-command 'state 0)))))

;; test case
;; test_case some
;; test-case
;; tes▮t-case

(defun xah-toggle-previous-letter-case ()
  "Toggle the letter case of the letter to the left of cursor.

URL `http://xahlee.info/emacs/emacs/emacs_toggle_letter_case.html'
Created: 2015-12-22
Version: 2026-01-06"
  (interactive)
  (let ((case-fold-search nil))
    (backward-char)
    (cond
     ((looking-at "[[:lower:]]") (upcase-region (point) (1+ (point))))
     ((looking-at "[[:upper:]]") (downcase-region (point) (1+ (point)))))
    (forward-char)))

(defun xah-upcase-sentence ()
  "Upcase first letters of sentences of current block or selection.

URL `http://xahlee.info/emacs/emacs/emacs_upcase_sentence.html'
Created: 2020-12-08
Version: 2025-03-25"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (let ((case-fold-search nil))
        ;; after period or question mark or exclamation
        (goto-char (point-min))
        (while (re-search-forward "\\(\\.\\|\\?\\|!\\)[ \n]+ *\\([a-z]\\)" nil 1)
          (upcase-region (match-beginning 2) (match-end 2))
          (overlay-put (make-overlay (match-beginning 2) (match-end 2)) 'face 'highlight))
        ;; after a blank line, after a bullet, or beginning of buffer
        (goto-char (point-min))
        (while (re-search-forward "\\(\\`\\|• \\|\n\n\\)\\([a-z]\\)" nil 1)
          (upcase-region (match-beginning 2) (match-end 2))
          (overlay-put (make-overlay (match-beginning 2) (match-end 2)) 'face 'highlight))
        ;; for HTML. first letter after tag
        (when
            (or
             (eq major-mode 'xah-html-mode)
             (eq major-mode 'html-mode)
             (eq major-mode 'sgml-mode)
             (eq major-mode 'nxml-mode)
             (eq major-mode 'xml-mode)
             (eq major-mode 'mhtml-mode))
          (goto-char (point-min))
          (while
              (re-search-forward "\\(<title>[ \n]?\\|<h[1-6]>[ \n]?\\|<p>[ \n]?\\|<li>[ \n]?\\|<dd>[ \n]?\\|<td>[ \n]?\\|<br ?/?>[ \n]?\\|<figcaption>[ \n]?\\)\\([a-z]\\)" nil 1)
            (upcase-region (match-beginning 2) (match-end 2))
            (overlay-put (make-overlay (match-beginning 2) (match-end 2)) 'face 'highlight))))
      (goto-char (point-max)))
    (skip-chars-forward " \n\t")))

(defun xah-title-case-region-or-line (&optional Begin End)
  "Title case text between nearest brackets, or current line or selection.
Capitalize first letter of each word, except words like {to, of, the, a, in, or, and}. If a word already contains cap letters such as HTTP, URL, they are left as is.

When called in a elisp program, Begin End are region boundaries.

URL `http://xahlee.info/emacs/emacs/elisp_title_case_text.html'
Created: 2017-01-11
Version: 2021-09-19"
  (interactive)
  (let* ((xskipChars "^\"<>(){}[]“”‘’‹›«»「」『』【】〖〗《》〈〉〔〕")
         (xp0 (point))
         (xbeg (if Begin
                  Begin
                (if (region-active-p)
                    (region-beginning)
                  (progn
                    (skip-chars-backward xskipChars (line-beginning-position)) (point)))))
         (xend (if End
                  End
                (if (region-active-p)
                    (region-end)
                  (progn (goto-char xp0)
                         (skip-chars-forward xskipChars (line-end-position)) (point)))))
         (xstrPairs [
                     [" A " " a "]
                     [" An " " an "]
                     [" And " " and "]
                     [" At " " at "]
                     [" As " " as "]
                     [" By " " by "]
                     [" Be " " be "]
                     [" Into " " into "]
                     [" In " " in "]
                     [" Is " " is "]
                     [" It " " it "]
                     [" For " " for "]
                     [" Of " " of "]
                     [" Or " " or "]
                     [" On " " on "]
                     [" Via " " via "]
                     [" The " " the "]
                     [" That " " that "]
                     [" To " " to "]
                     [" Vs " " vs "]
                     [" With " " with "]
                     [" From " " from "]
                     ["'S " "'s "]
                     ["'T " "'t "]
                     ]))
    (save-excursion
      (save-restriction
        (narrow-to-region xbeg xend)
        (upcase-initials-region (point-min) (point-max))
        (let ((case-fold-search nil))
          (mapc
           (lambda (xx)
             (goto-char (point-min))
             (while
                 (search-forward (aref xx 0) nil t)
               (replace-match (aref xx 1) t t)))
           xstrPairs))))))

(defun xah-add-space-after-comma ()
  "Add a space after comma of current block or selection.
and highlight changes made.
Created: 2022-01-20
Version: 2025-03-25"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (goto-char (point-min))
      (while
          (re-search-forward ",\\b" nil t)
        (replace-match ", ")
        (overlay-put
         (make-overlay
          (match-beginning 0)
          (match-end 0)) 'face 'highlight)))))

(defun xah-fill-or-unfill ()
  "Reformat current block or selection to short/long line.
First call breaks text into multiple lines. Repeated call toggles between multiple and singe line.
This commands calls `fill-region' to do its work. Set `fill-column' for short line length.

URL `http://xahlee.info/emacs/emacs/emacs_unfill-paragraph.html'
Created: 2020-11-22
Version: 2025-08-29"
  (interactive)
  ;; This command symbol has a property “'longline-p”, the possible values are t and nil. This property is used to easily determine whether to compact or uncompact, when this command is called again
  (let ((xisLongline (if (eq last-command this-command) (get this-command 'longline-p) t))
        (deactivate-mark nil)
        xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (if xisLongline
        (fill-region xbeg xend)
      (let ((fill-column 99999))
        (fill-region xbeg xend)))
    (put this-command 'longline-p (not xisLongline))))

(defun xah-reformat-lines (&optional Width)
  "Reformat current block or selection into short lines or 1 long line.
When called for the first time, change to one line. Second call change it to multi-lines. Repeated call toggles.
If `universal-argument' is called first, ask user to type max length of line. By default, it is 70.

Note: this command is different from emacs `fill-region' or `fill-paragraph'.
This command never adds or delete non-whitespace chars. It only exchange whitespace sequence.

URL `http://xahlee.info/emacs/emacs/emacs_reformat_lines.html'
Created: 2016
Version: 2025-09-07"
  (interactive
   (list
    (if current-prefix-arg
        (cond
         ((not (numberp current-prefix-arg)) 70)
         (t (prefix-numeric-value current-prefix-arg)))
      80)))
  ;; This symbol has a property 'is-long-p, the possible values are t and nil. This property is used to easily determine whether to compact or uncompact, when this command is called again
  (let ((xisLong (if (eq last-command this-command) (get this-command 'is-long-p) nil))
        (xwidth (if Width Width 70))
        xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (if xisLong
        (save-excursion
          (save-restriction
            (narrow-to-region xbeg xend)
            (goto-char (point-min))
            (while (re-search-forward " +" nil 1)
              (when (> (- (point) (line-beginning-position)) xwidth)
                (replace-match "\n")))))
      (save-excursion
        (save-restriction
          (narrow-to-region xbeg xend)
          (goto-char (point-min))
          (while (re-search-forward "[ \n\t]+" xend 1) (replace-match " ")))))
    (put this-command 'is-long-p (not xisLong))))

(defun xah-reformat-to-sentence-lines ()
  "Reformat current block or selection into multiple lines by ending period.
Move cursor to the beginning of next text block.
After this command is called, press variable `xah-repeat-key' to repeat it.

URL `http://xahlee.info/emacs/emacs/elisp_reformat_to_sentence_lines.html'
Created: 2020-12-02
Version: 2025-05-21"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (goto-char (point-min)) (while (search-forward "。" nil t) (replace-match "。\n"))
      ;; (goto-char (point-min)) (while (search-forward " <a " nil t) (replace-match "\n<a "))
      ;; (goto-char (point-min)) (while (search-forward "</a> " nil t) (replace-match "</a>\n"))
      (goto-char (point-min))
      (while (re-search-forward "\\([A-Za-z0-9]+\\)[ \t]*\n[ \t]*\\([A-Za-z0-9]+\\)" nil t)
        (replace-match "\\1 \\2"))
      (goto-char (point-min))
      (while (re-search-forward "\\([,]\\)[ \t]*\n[ \t]*\\([A-Za-z0-9]+\\)" nil t)
        (replace-match "\\1 \\2"))
      (goto-char (point-min))
      (while (re-search-forward "  +" nil t) (replace-match " "))
      (goto-char (point-min))
      (while (re-search-forward "\\([.?!]\\) +\\([(0-9A-Za-z<]+\\)" nil t) (replace-match "\\1\n\\2"))
      (goto-char (point-min))
      (while (re-search-forward "\\(<br ?/?>\\)" nil t) (replace-match "\\1\n"))

      (goto-char (point-min))
      (while (search-forward "e.g.\n" nil t) (replace-match "e.g. "))

      (goto-char (point-max))
      (while (eq (char-before) 32) (delete-char -1))))
  (re-search-forward "\n+" nil 1)
  (set-transient-map (let ((xkmap (make-sparse-keymap))) (define-key xkmap (kbd (if (boundp 'xah-repeat-key) xah-repeat-key ".")) this-command) xkmap)))

(defun xah-space-to-newline ()
  "Replace space sequence to a newline char in current block or selection.

URL `http://xahlee.info/emacs/emacs/emacs_space_to_newline.html'
Created: 2017-08-19
Version: 2025-03-25"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (goto-char (point-min))
      (while (re-search-forward " +" nil t)
        (replace-match "\n")))))

(defun xah-slash-to-backslash (&optional Begin End)
  "Replace slash by backslash on current line or region.
Created: 2021-07-14
Version: 2021-09-12"
  (interactive)
  (let (xbeg xend)
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (setq xbeg (line-beginning-position) xend (line-end-position))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (let ((case-fold-search nil))
        (goto-char (point-min))
        (while (search-forward "/" nil t)
          (replace-match "\\\\"))))))

(defun xah-backslash-to-slash (&optional Begin End)
  "Replace backslash by slash on current line or region.
Version: 2021-09-11"
  (interactive)
  (let (xbeg xend)
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (setq xbeg (line-beginning-position) xend (line-end-position))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (let ((case-fold-search nil))
        (goto-char (point-min))
        (while (search-forward "\\" nil t)
          (replace-match "/"))))))

(defun xah-double-backslash (&optional Begin End)
  "Replace backslash by two backslash on current line or region.
Version: 2021-11-09"
  (interactive)
  (let (xbeg xend)
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (setq xbeg (line-beginning-position) xend (line-end-position))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (let ((case-fold-search nil))
        (goto-char (point-min))
        (while (search-forward "\\" nil t)
          (replace-match "\\\\\\\\"))))))

(defun xah-double-backslash-to-single (&optional Begin End)
  "Replace double backslash by single backslash on current line or region.
Version: 2021-11-09"
  (interactive)
  (let (xbeg xend)
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (setq xbeg (line-beginning-position) xend (line-end-position))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (let ((case-fold-search nil))
        (goto-char (point-min))
        (while (search-forward "\\\\"  nil t)
          (replace-match "\\\\"))))))

(defun xah-slash-to-double-backslash (&optional Begin End)
  "Replace slash by double backslash on current line or region.
Version: 2021-07-14"
  (interactive)
  (let (xbeg xend)
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (setq xbeg (line-beginning-position) xend (line-end-position))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (let ((case-fold-search nil))
        (goto-char (point-min))
        (while (search-forward "/" nil t)
          (replace-match "\\\\\\\\"))))))

(defun xah-double-backslash-to-slash (&optional Begin End)
  "Replace double backslash by slash on current line or region.
Version: 2021-07-14"
  (interactive)
  (let (xbeg xend)
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (setq xbeg (line-beginning-position) xend (line-end-position))))
    (save-restriction
      (narrow-to-region xbeg xend)
      (let ((case-fold-search nil))
        (goto-char (point-min))
        (while (search-forward "\\\\" nil t)
          (replace-match "/"))))))

(defun xah-comment-dwim ()
  "Toggle comment in programing language code.

Like `comment-dwim', but toggle comment if cursor is not at end of line.
If cursor is at end of line, either add comment at the line end or move cursor to start of line end comment. call again to comment out whole line.

URL `http://xahlee.info/emacs/emacs/emacs_toggle_comment_by_line.html'
Created: 2016-10-25
Version: 2023-07-10"
  (interactive)
  (if (region-active-p)
      (comment-dwim nil)
    (let ((xbegin (line-beginning-position))
          (xend (line-end-position)))
      (if (eq xbegin xend)
          (progn
            (comment-dwim nil))
        (if (eq (point) xend)
            (progn
              (comment-dwim nil))
          (progn
            (comment-or-uncomment-region xbegin xend)
            (forward-line )))))))

(defun xah-quote-lines (QuoteL QuoteR Sep)
  "Add quotes/brackets and separator (comma) to lines.
Act on current block or selection.

For example,

 cat
 dog
 cow

becomes

 \"cat\",
 \"dog\",
 \"cow\",

or

 (cat)
 (dog)
 (cow)

In lisp code, QuoteL QuoteR Sep are strings.

URL `http://xahlee.info/emacs/emacs/emacs_quote_lines.html'
Created: 2020-06-26
Version: 2025-03-25"
  (interactive
   (let ((xbrackets
          '(
            "\"QUOTATION MARK\""
            "'APOSTROPHE'"
            "(paren)"
            "{brace}"
            "[square]"
            "<greater>"
            "`emacs'"
            "`markdown`"
            "~tilde~"
            "=equal="
            "“curly double”"
            "‘curly single’"
            "‹french angle›"
            "«french double angle»"
            "「corner」"
            "none"
            "other"
            ))
         (xcomma '("comma ," "semicolon ;" "none" "other"))
         xbktChoice xsep xsepChoice xquoteL xquoteR)
     (let ((completion-ignore-case t))
       (setq xbktChoice (completing-read "Quote to use:" xbrackets nil t nil nil (car xbrackets)))
       (setq xsepChoice (completing-read "line separator:" xcomma nil t nil nil (car xcomma))))
     (cond
      ((string-equal xbktChoice "none")
       (setq xquoteL "" xquoteR ""))
      ((string-equal xbktChoice "other")
       (let ((xx (read-string "Enter 2 chars, for begin/end quote:")))
         (setq xquoteL (substring xx 0 1)
               xquoteR (substring xx 1 2))))
      (t (setq xquoteL (substring xbktChoice 0 1)
               xquoteR (substring xbktChoice -1))))
     (setq xsep
           (cond
            ((string-equal xsepChoice "comma ,") ",")
            ((string-equal xsepChoice "semicolon ;") ";")
            ((string-equal xsepChoice "none") "")
            ((string-equal xsepChoice "other") (read-string "Enter separator:"))
            (t xsepChoice)))
     (list xquoteL xquoteR xsep)))
  (let (xbeg xend (xquoteL QuoteL) (xquoteR QuoteR) (xsep Sep))
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (save-excursion
      (save-restriction
        (narrow-to-region xbeg xend)
        (goto-char (point-min))
        (catch 1111
          (while t
            (skip-chars-forward "\t ")
            (insert xquoteL)
            (end-of-line)
            (insert xquoteR xsep)
            (if (eq (point) (point-max))
                (throw 1111 t)
              (forward-char))))))))

(defun xah-escape-quotes (Begin End)
  "Add slash before double quote in current line or selection.
Double quote is codepoint 34.
See also: `xah-unescape-quotes'
URL `http://xahlee.info/emacs/emacs/elisp_escape_quotes.html'
Created: 2010-08-16
Version: 2025-11-23"
  (interactive
   (if (region-active-p)
       (list (region-beginning) (region-end))
     (list (line-beginning-position) (line-end-position))))
  (save-excursion
    (save-restriction
      (narrow-to-region Begin End)
      (goto-char (point-min))
      (while (search-forward "\"" nil t)
        (replace-match "\\\"" t t))))
  (message "done. %s" real-this-command))

(defun xah-unescape-quotes (&optional Begin End)
  "Replace 「\\\"」 by 「\"」 in current line or selection.
See also: `xah-escape-quotes'
URL `http://xahlee.info/emacs/emacs/elisp_escape_quotes.html'
Created: 2017-01-11
Version: 2023-11-02"
  (interactive)
  (let (xbeg xend)
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (setq xbeg (line-beginning-position) xend (line-end-position))))
    (save-excursion
      (save-restriction
        (narrow-to-region xbeg xend)
        (goto-char (point-min))
        (while (search-forward "\\\"" nil t)
          (replace-match "\"" t t)))))
  (message "done. %s" real-this-command))

(defun xah-cycle-hyphen-lowline-space (&optional Begin End)
  "Cycle {hyphen lowline space} chars.

The region to work on is by this order:
 1. if there is a selection, use that.
 2. If cursor is in a string quote or any type of bracket, and is within current line, work on that region.
 3. else, work on current line.

After this command is called, press variable `xah-repeat-key' to repeat it.

URL `http://xahlee.info/emacs/emacs/elisp_change_space-hyphen_underscore.html'
Created: 2019-02-12
Version: 2025-01-24"
  (interactive)
  ;; this function sets a property 'state. Possible values are 0 to length of xcharArray.
  (let (xbeg xend xlen
            (xcharArray ["-" "_" " "])
            (xregionWasActive-p (region-active-p))
            (xnowState (if (eq last-command this-command) (get 'xah-cycle-hyphen-lowline-space 'state) 0))
            xchangeTo)
    (setq
     xlen (length xcharArray)
     xchangeTo (elt xcharArray xnowState))
    (if (and Begin End)
        (setq xbeg Begin xend End)
      (if (region-active-p)
          (setq xbeg (region-beginning) xend (region-end))
        (let ((xskipChars "^\"<>(){}[]“”‘’‹›«»「」『』【】〖〗《》〈〉〔〕（）"))
          (skip-chars-backward xskipChars (line-beginning-position))
          (setq xbeg (point))
          (skip-chars-forward xskipChars (line-end-position))
          (setq xend (point))
          (push-mark xbeg))))
    (save-excursion
      (save-restriction
        (narrow-to-region xbeg xend)
        (goto-char (point-min))
        (while (re-search-forward (elt xcharArray (% (+ xnowState 2) xlen)) (point-max) 1)
          (replace-match xchangeTo t t))))
    (when (or (string-equal xchangeTo " ") xregionWasActive-p)
      (goto-char xend)
      (push-mark xbeg)
      (setq deactivate-mark nil))
    (put 'xah-cycle-hyphen-lowline-space 'state (% (+ xnowState 1) xlen)))
  (set-transient-map (let ((xkmap (make-sparse-keymap))) (define-key xkmap (kbd (if (boundp 'xah-repeat-key) xah-repeat-key ".")) this-command) xkmap)))

(defun xah-copy-file-path (&optional DirPathOnlyQ)
  "Copy current buffer file path or dired path.
Result is full path.
If `universal-argument' is called first, copy only the dir path.

If in dired, copy the current or marked files.

If a buffer is not file and not dired, copy value of `default-directory'.

URL `http://xahlee.info/emacs/emacs/emacs_copy_file_path.html'
Created: 2018-06-18
Version: 2025-05-23"
  (interactive "P")
  (let (xfpath)
    (setq xfpath
          (if (eq major-mode 'dired-mode)
              (progn
                (let ((xresult (mapconcat #'identity
                                          (dired-get-marked-files) "\n")))
                  (if (length= xresult 0)
                      default-directory
                    xresult)))
            (or buffer-file-name default-directory)))
    (kill-new
     (if DirPathOnlyQ
         (progn
           (message "Directory copied: %s" (file-name-directory xfpath))
           (file-name-directory xfpath))
       (progn
         (message "File path copied: %s" xfpath)
         xfpath)))))

(defun xah-delete-current-text-block ()
  "Delete the current text block plus blank lines, or selection, and copy to `kill-ring'.

If cursor is between blank lines, delete following blank lines.

URL `http://xahlee.info/emacs/emacs/emacs_delete_block.html'
Created: 2017-07-09
Version: 2024-10-07"
  (interactive)
  (let (xbeg xend (xp (point)))
    (if (region-active-p)
        (setq xbeg (region-beginning) xend (region-end))
      (progn
        (setq xbeg
              (if (re-search-backward "\n[ \t]*\n+" nil 1)
                  (match-end 0)
                (point)))
        (goto-char xp)
        (setq xend (if (re-search-forward "\n[ \t]*\n+" nil 1)
                       (match-end 0)
                     (point-max)))))
    (kill-region xbeg xend)))

(defun xah-copy-to-register-1 ()
  "Copy selection or text block to register 1.
URL `http://xahlee.info/emacs/emacs/elisp_copy-paste_register_1.html'
Created: 2012-07-17
Version: 2025-11-05"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (copy-to-register ?1 xbeg xend)
    (message "Copied to register 1: %s." (buffer-substring xbeg xend))))

(defun xah-append-to-register-1 ()
  "Append selection or text block to register 1,
with one added newline char at end.
URL `http://xahlee.info/emacs/emacs/elisp_copy-paste_register_1.html'
Created: 2015-12-08
Version: 2025-11-07"
  (interactive)
  (let (xbeg xend)
    (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
    (let ((register-separator "\n"))
      (append-to-register ?1 xbeg xend)
      (message "Done. Append to register 1."))))

(defun xah-paste-from-register-1 ()
  "Paste text from register 1.
URL `http://xahlee.info/emacs/emacs/elisp_copy-paste_register_1.html'
Created: 2015-12-08
Version: 2025-11-07"
  (interactive)
  (when (region-active-p)
    (delete-region (region-beginning) (region-end)))
  (insert-register ?1 t))

(defun xah-clear-register-1 ()
  "Clear register 1.
URL `http://xahlee.info/emacs/emacs/elisp_copy-paste_register_1.html'
Created: 2015-12-08
Version: 2025-11-07"
  (interactive)
  (progn
    (copy-to-register ?1 (point-min) (point-min))
    (message "Cleared register 1.")))

;; s------------------------------
;; insertion commands

(defun xah-insert-date ()
  "Insert current date time.
Insert date in this format: yyyy-mm-dd.
If `universal-argument' is called first, prompt for a format to use.
If there is selection, delete it first.

URL `http://xahlee.info/emacs/emacs/elisp_insert-date-time.html'
Created: 2013-05-10
Version: 2026-02-04"
  (interactive)
  (let (xmenu xstyle)
    (setq
     xmenu
     (list
      (concat "ISO date⚫" (format-time-string "%Y-%m-%d"))
      (concat "dotted version⚫" (format-time-string "%Y.%m.%d"))
      (concat "coder⚫" (format-time-string "%Y-%m-%d_%H%M%S"))
      (concat "all digits⚫" (format-time-string "%Y%m%d%H%M%S"))

      (concat "unix seconds⚫"
              ;; (number-to-string (car (let ((current-time-list nil)) (current-time))))
              (number-to-string (time-convert (current-time) 'integer)))

      ;; (concat "ISO full⚫" (format-time-string "%Y-%m-%dT%T") (funcall (lambda (xx) (format "%s:%s" (substring xx 0 3) (substring xx 3 5))) (format-time-string "%z")))

      (concat "ISO full⚫" (format-time-string "%FT%T%z"))

      (concat "ISO + weekday⚫" (format-time-string "%Y-%m-%d %A"))
      (concat "USA + weekday⚫" (format-time-string "%A, %B %d, %Y"))
      (concat "USA + weekday abbrev⚫" (format-time-string "%a, %b %d, %Y"))
      (concat "USA⚫" (format-time-string "%B %d, %Y"))
      (concat "USA abbrev⚫" (format-time-string "%b %d, %Y"))))
    (setq xstyle
          (if current-prefix-arg
              (let ((completion-ignore-case t))
                (completing-read "Style:" xmenu nil t))
            (car xmenu)))
    (when (region-active-p) (delete-region (region-beginning) (region-end)))
    (insert (nth 1 (split-string xstyle "⚫")))))

(defun xah-insert-bracket-pair (LBracket RBracket &optional WrapMethod)
  "Insert brackets around selection, word, at point, and maybe move cursor in between.

 LBracket and RBracket are strings. WrapMethod must be either `line' or `block'. `block' means between empty lines.

• If there is a active region, wrap around region.
Else
• If WrapMethod is `line', wrap around line.
• If WrapMethod is `block', wrap around block.
Else
• If cursor is at beginning of line and its not empty line and contain at least 1 space, wrap around the line.
• If cursor is at end of a word or buffer, one of the following happens:
 xyz▮ → xyz(▮)
 xyz▮ → (xyz▮)       if in one of the lisp modes.
• wrap brackets around word if any. e.g. xy▮z → (xyz▮). Or just (▮)

URL `http://xahlee.info/emacs/emacs/elisp_insert_brackets_by_pair.html'
Created: 2017-01-17
Version: 2025-03-25"
  (if (region-active-p)
      (progn
        (let ((xbeg (region-beginning)) (xend (region-end)))
          (goto-char xend) (insert RBracket)
          (goto-char xbeg) (insert LBracket)
          (goto-char (+ xend 2))))
    (let (xbeg xend)
      (cond
       ((eq WrapMethod 'line)
        (setq xbeg (line-beginning-position) xend (line-end-position))
        (goto-char xend)
        (insert RBracket)
        (goto-char xbeg)
        (insert LBracket)
        (goto-char (+ xend (length LBracket))))
       ((eq WrapMethod 'block)
        (save-excursion
          (setq xbeg (if (region-active-p) (region-beginning) (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point)))) xend (if (region-active-p) (region-end) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point)))))
          (goto-char xend)
          (insert RBracket)
          (goto-char xbeg)
          (insert LBracket)
          (goto-char (+ xend (length LBracket)))))
       ( ; do line. line must contain space
        (and
         (eq (point) (line-beginning-position))
         (not (eq (line-beginning-position) (line-end-position))))
        (insert LBracket)
        (end-of-line)
        (insert  RBracket))
       ((and
         (or ; cursor is at end of word or buffer. i.e. xyz▮
          (looking-at "[^-_[:alnum:]]")
          (eq (point) (point-max)))
         (not (or
               (eq major-mode 'xah-elisp-mode)
               (eq major-mode 'emacs-lisp-mode)
               (eq major-mode 'lisp-mode)
               (eq major-mode 'lisp-interaction-mode)
               (eq major-mode 'common-lisp-mode)
               (eq major-mode 'clojure-mode)
               (eq major-mode 'xah-clojure-mode)
               (eq major-mode 'scheme-mode))))
        (progn
          (setq xbeg (point) xend (point))
          (insert LBracket RBracket)
          (search-backward RBracket)))
       (t (progn
            ;; wrap around “word”. basically, want all alphanumeric, plus hyphen and underscore, but don't want space or punctuations. Also want chinese chars
            ;; 我有一帘幽梦，不知与谁能共。多少秘密在其中，欲诉无人能懂。
            (skip-chars-backward "-_[:alnum:]")
            (setq xbeg (point))
            (skip-chars-forward "-_[:alnum:]")
            (setq xend (point))
            (goto-char xend)
            (insert RBracket)
            (goto-char xbeg)
            (insert LBracket)
            (goto-char (+ xend (length LBracket)))))))))

(defun xah-insert-paren () (interactive) (xah-insert-bracket-pair "(" ")"))
(defun xah-insert-square-bracket () (interactive) (xah-insert-bracket-pair "[" "]"))
(defun xah-insert-brace () (interactive) (xah-insert-bracket-pair "{" "}"))

(defun xah-insert-ascii-double-quote () (interactive) (xah-insert-bracket-pair "\"" "\""))
(defun xah-insert-ascii-single-quote () (interactive) (xah-insert-bracket-pair "'" "'"))
(defun xah-insert-ascii-angle-bracket () (interactive) (xah-insert-bracket-pair "<" ">"))

(defun xah-insert-emacs-quote () (interactive) (xah-insert-bracket-pair "`" "'"))
(defun xah-insert-markdown-quote () (interactive) (xah-insert-bracket-pair "`" "`"))
(defun xah-insert-markdown-triple-quote () (interactive) (xah-insert-bracket-pair "```\n" "\n```"))

(defun xah-insert-double-curly-quote“” () (interactive) (xah-insert-bracket-pair "“" "”"))
(defun xah-insert-curly-single-quote‘’ () (interactive) (xah-insert-bracket-pair "‘" "’"))
(defun xah-insert-single-angle-quote‹› () (interactive) (xah-insert-bracket-pair "‹" "›"))
(defun xah-insert-double-angle-quote«» () (interactive) (xah-insert-bracket-pair "«" "»"))

(defun xah-insert-corner-bracket「」 () (interactive) (xah-insert-bracket-pair "「" "」"))
(defun xah-insert-white-corner-bracket『』 () (interactive) (xah-insert-bracket-pair "『" "』"))
(defun xah-insert-angle-bracket〈〉 () (interactive) (xah-insert-bracket-pair "〈" "〉"))
(defun xah-insert-double-angle-bracket《》 () (interactive) (xah-insert-bracket-pair "《" "》"))
(defun xah-insert-white-lenticular-bracket〖〗 () (interactive) (xah-insert-bracket-pair "〖" "〗"))
(defun xah-insert-black-lenticular-bracket【】 () (interactive) (xah-insert-bracket-pair "【" "】"))
(defun xah-insert-tortoise-shell-bracket〔〕 () (interactive) (xah-insert-bracket-pair "〔" "〕"))
(defun xah-insert-deco-angle-bracket❮❯ () (interactive) (xah-insert-bracket-pair "❮" "❯"))
(defun xah-insert-deco-angle-fat-bracket❰❱ () (interactive) (xah-insert-bracket-pair "❰" "❱"))

(defun xah-insert-space-before ()
  "Insert space before cursor."
  (interactive)
  (insert " "))

(defun xah-insert-seperator ()
  "Insert a visual seperator line.
Version: 2026-03-20"
  (interactive)
  (cond
   ((not comment-start)
    (insert "\ns------------------------------\n"))
   (t (insert "\ns------------------------------\n")
      (backward-char)
      (comment-line 1))))

(defvar xah-unicode-list nil
 "A alist.
Each item is (prompStr . xString). Used by `xah-insert-unicode'.
prompStr is used for prompt.
xString is is the char to insert.
xString can be multiple chars or any string.
")

(setq
 xah-unicode-list
 '(
   ;;
   ("smile beaming 😊" . "😊")
   ("omg 😂" . "😂")
   ("hug 🤗" . "🤗")
   ("angry 😠" . "😠")
   ("skull 💀" . "💀")
   ("heart 🩷" . "🩷")

   ("thumb up 👍" . "👍")
   ("thumb down 👎" . "👎")

   ("glowing star 🌟" . "🌟")
   ("star ⭐" . "⭐")
   ("sparkles ✨" . "✨")
   ("black star ★" . "★")

   ("tv 📺" . "📺")

   ("lotus 🪷" . "🪷")
   ("butterfly 🦋" . "🦋")
   ("LADY bug BEETLE 🐞" . "🐞")

   ("checkmark ✅" . "✅")
   ("new 🆕" . "🆕")
   ("rocket 🚀" . "🚀")
   ("large circle" . "⭕")
   ("cross ❌" . "❌")
   ("red triangle 🔺" . "🔺")
   ("diamond 💠" . "💠")
   ("square ⬛" . "⬛")
   ("SMALL ORANGE DIAMOND 🔸" . "🔸")
   ("BLACK RIGHT-POINTING TRIANGLE ▶" . "▶")
   ("BLACK DIAMOND ◆" . "◆")

   ("script 📜" . "📜")
   ("package 📦" . "📦")
   ("cursor ▮" . "▮")
   ("music 🎵" . "🎵")
   ("ok 🆗" . "🆗")

   ("dagger †" . "†")
   ("double dagger ‡" . "‡")

   ("double angle bracket" . "《》")
   ("black lenticular bracket" . "【】")
   ("corner-bracket" . "「」")
   ("tortoise shell bracket" . "〔〕")
   ("angle bracket" . "〈〉")
   ("double angle quote" . "«»")

   ("bullet •" . "•")
   ("fullwidth ampersand ＆" . "＆")

   ("f hook ƒ" . "ƒ")

   ("emdash —" . "—")
   ("middle dot ·" . "·")
   ("...ellipsis …" . "…")

   ("nbsp non breaking space" . " ")

   ("left arrow ←" . "←")
   ("right arrow →" . "→")
   ("up arrow ↑" . "↑")
   ("down arrow ↓" . "↓")

   ("LONG RIGHTWARDS ARROW ⟶" . "⟶")
   ("mapto ↦" . "↦")

   ;;
   ))

(defun xah-insert-unicode ()
  "Insert a unicode from a custom list `xah-unicode-list'.
URL `http://xahlee.info/emacs/emacs/emacs_insert_unicode.html'
Created: 2021-01-05
Version: 2023-09-19"
  (interactive)
  (let ((xkey
         (let ((completion-ignore-case t))
           (completing-read "Insert:" xah-unicode-list nil t))))
    (insert (cdr (assoc xkey xah-unicode-list)))))

;; s------------------------------
;; text selection

(defun xah-select-block ()
  "Select the current/next block.

URL `http://xahlee.info/emacs/emacs/emacs_select_text_block.html'
Created: 2019-12-26
Version: 2025-07-07"
  (interactive)
  (if (region-active-p)
      (re-search-forward "\n[ \t]*\n[ \t]*\n*" nil 1)
    (progn
      (when (re-search-backward "\n[ \t]*\n" nil 1)
        (goto-char (match-end 0)))
      (push-mark (point) t t)
      (re-search-forward "\n[ \t]*\n" nil 1)
      (skip-chars-backward " \t\n"))))

(defun xah-select-line ()
  "Select current line. If region is active, extend selection downward by line.
If `visual-line-mode' is on, consider line as visual line.

URL `http://xahlee.info/emacs/emacs/emacs_select_line.html'
Created: 2017-11-01
Version: 2023-11-14"
  (interactive)
  (if (region-active-p)
      (if visual-line-mode
          (let ((xbeg (point)))
            (end-of-visual-line 1)
            (when (eq xbeg (point))
              (end-of-visual-line 2)))
        (progn
          (forward-line 1)
          (end-of-line)))
    (if visual-line-mode
        (progn (beginning-of-visual-line)
               (push-mark (point) t t)
               (end-of-visual-line))
      (progn
        (push-mark (line-beginning-position) t t)
        (end-of-line)))))

(defun xah-extend-selection ()
  "Select the current word, bracket/quote expression, or expand selection.
Subsequent calls expands the selection.

when there is no selection,
• If cursor is on any type of bracket (including parenthesis, quotation mark), select whole bracketed thing including bracket
• else, select current word.

when there is a selection, the selection extension behavior is still experimental. But when cursor is on a any type of bracket (parenthesis, quote), it extends selection to outer bracket.

After this command is called, press variable `xah-repeat-key' to repeat it.

URL `http://xahlee.info/emacs/emacs/emacs_extend_selection.html'
Created: 2020-02-04
Version: 2025-11-18"
  (interactive)
  (if (region-active-p)
      (let ((xbeg (region-beginning)) (xend (region-end)))
        (goto-char xbeg)
        (cond
         ((looking-at "\\s(")
          (if (eq (nth 0 (syntax-ppss)) 0)
              (progn
                (message "%s debug: left bracket, depth 0." real-this-command)
                (end-of-line) ; select current line
                (push-mark (line-beginning-position) t t))
            (progn
              (message "%s debug: left bracket, depth not 0" real-this-command)
              (up-list -1 t t)
              (mark-sexp) (exchange-point-and-mark))))
         ((eq xbeg (line-beginning-position))
          (progn
            (goto-char xbeg)
            (let ((xfirstLineEndPos (line-end-position)))
              (cond
               ((eq xend xfirstLineEndPos)
                (progn
                  (message "%s debug: exactly 1 line. extend to next whole line." real-this-command)
                  (forward-line 1)
                  (end-of-line)))
               ((< xend xfirstLineEndPos)
                (progn
                  (message "%s debug: less than 1 line. complete the line." real-this-command)
                  (end-of-line)))
               ((> xend xfirstLineEndPos)
                (progn
                  (message "%s debug: beginning of line, but end is greater than 1st end of line" real-this-command)
                  (goto-char xend)
                  (if (eq (point) (line-end-position))
                      (progn
                        (message "%s debug: exactly multiple lines" real-this-command)
                        (forward-line 1)
                        (end-of-line))
                    (progn
                      (message "%s debug: multiple lines but end is not eol. make it so" real-this-command)
                      (goto-char xend)
                      (end-of-line)))))
               (t (error "%s: logic error 42946" real-this-command))))))
         ((and (> (point) (line-beginning-position)) (<= (point) (line-end-position)))
          (progn
            (message "%s debug: less than 1 line" real-this-command)
            (end-of-line) ; select current line
            (push-mark (line-beginning-position) t t)))
         (t
          (message "%s debug: last resort" real-this-command)
          nil)))
    (cond
     ((looking-at "\\s(")
      (message "%s debug: left bracket" real-this-command)
      (mark-sexp) (exchange-point-and-mark))

     ;; ((looking-at "\\s)")
     ;; (message "%s debug: right bracket" real-this-command)
     ;; (backward-up-list) (mark-sexp) (exchange-point-and-mark))

     ((looking-at "\\s\"")
      (message "%s debug: string quote" real-this-command)

      (if (nth 3 (syntax-ppss))
          (progn
            (forward-char)
            (backward-sexp)
            (mark-sexp)
            (exchange-point-and-mark))
        (progn
          (mark-sexp)
          (exchange-point-and-mark))))

     ((looking-at "[ \t\n]")
      (message "%s debug: is white space" real-this-command)
      ;; (skip-chars-backward " \t\n")
      (push-mark)
      (skip-chars-forward " \t\n")
      (setq mark-active t))

     ((looking-at "[-_[:word:]]")
      (message "%s debug: right is word or symbol" real-this-command)
      (skip-chars-backward "-_[:word:]")
      (push-mark)
      (skip-chars-forward "-_[:word:]")
      (setq mark-active t))

     ;; ((and (looking-at "[:blank:]")
     ;;       (prog2 (backward-char) (looking-at "[[:blank:]]") (forward-char)))
     ;;  ;; (message "%s debug: left and right both space" real-this-command)
     ;;  (skip-chars-backward "[:blank:]") (push-mark (point) t t)
     ;;  (skip-chars-forward "[:blank:]"))

     ((and (looking-at "\n")
           (eq (char-before) 10))
      (message "%s debug: left and right both newline" real-this-command)
      (skip-chars-forward "\n")
      (push-mark (point) t t)
      (re-search-forward "\n[ \t]*\n"))

     (t
      (message "%s debug: no condition met. just select 1 char" real-this-command)
      (push-mark (point) t t)
      (forward-char))))

  (set-transient-map
   (let ((xkmap (make-sparse-keymap)))
     (define-key xkmap (kbd (if (boundp 'xah-repeat-key) xah-repeat-key ".")) real-this-command)
     xkmap)))

(defun xah-select-text-in-quote ()
  "Select text between the nearest left and right delimiters.
Delimiters here includes QUOTATION MARK, GRAVE ACCENT, and anything in variable `xah-brackets'.
This command ignores nesting. For example, if text is
「(a(b)c▮)」
the selected char is 「c」, not 「a(b)c」.

URL `http://xahlee.info/emacs/emacs/emacs_select_quote_text.html'
Created: 2020-11-24
Version: 2023-11-14"
  (interactive)
  (let ((xskipChars (concat "^\"`" (mapconcat #'identity xah-brackets ""))))
    (skip-chars-backward xskipChars)
    (push-mark (point) t t)
    (skip-chars-forward xskipChars)))

(defun xah-cut-text-in-quote ()
  "Cut text between the nearest left and right delimiters.
See `xah-select-text-in-quote'

Created: 2023-07-23
Version: 2024-10-02"
  (interactive)
  (let (xbeg xend
        (xskipChars (concat "^\"`" (mapconcat #'identity xah-brackets ""))))
    (skip-chars-backward xskipChars)
    (setq xbeg (point))
    (skip-chars-forward xskipChars)
    (setq xend (point))
    (kill-region xbeg xend)))

;; s------------------------------
;; misc

(defun xah-user-buffer-p ()
  "Return t if current buffer is a user buffer, else nil.
A user buffer has buffer name NOT starts with * or space, and is not dired mode, help mode, etc.
This function is used by buffer switching command and close buffer command, so that next buffer shown is a user buffer.
You can override this function to get your idea of “user buffer”.
URL `http://xahlee.info/emacs/emacs/elisp_next_prev_user_buffer.html'
Created: 2016-06-18
Version: 2024-09-23"
  (interactive)
  (cond
   ((string-match "^\*" (buffer-name)) nil)
   ;; ((eq major-mode 'dired-mode) nil)
   ;; ((eq major-mode 'eww-mode) nil)
   ;; ((eq major-mode 'help-mode) nil)
   (t t)))

(defun xah-next-user-buffer ()
  "Switch to the next user buffer.
User Buffer here is determined by `xah-user-buffer-p'.
URL `http://xahlee.info/emacs/emacs/elisp_next_prev_user_buffer.html'
Created: 2016-06-19
Version: 2025-09-08"
  (interactive)
  (seq-some
   (lambda (_)
     (next-buffer)
     (xah-user-buffer-p))
   (make-list 50 0)))

(defun xah-previous-user-buffer ()
  "Switch to the previous user buffer.
User Buffer here is determined by `xah-user-buffer-p'.
URL `http://xahlee.info/emacs/emacs/elisp_next_prev_user_buffer.html'
Created: 2016-06-19
Version: 2025-09-08"
  (interactive)
  (seq-some
   (lambda (_)
     (previous-buffer)
     (xah-user-buffer-p))
   (make-list 50 0)))

(defun xah-new-empty-buffer ()
  "Create a new empty buffer.
Returns the buffer object.
New buffer is named untitled, untitled<2>, etc.

Warning: new buffer is not prompted for save when killed, see `kill-buffer'.
Or manually `save-buffer'

URL `http://xahlee.info/emacs/emacs/emacs_new_empty_buffer.html'
Created: 2017-11-01
Version: 2022-04-05"
  (interactive)
  (let ((xbuf (generate-new-buffer "untitled")))
    (switch-to-buffer xbuf)
    (funcall initial-major-mode)
    xbuf
    ))

(declare-function minibuffer-keyboard-quit "delsel" ())
(declare-function org-edit-src-save "org-src" ())

(defcustom xah-recently-closed-buffers-max 100 "The maximum length for `xah-recently-closed-buffers'."
  :type 'integer)

(defvar xah-recently-closed-buffers nil "A Alist of recently closed buffers.
Each element is (bufferName . filePath).
The max number to track is controlled by the variable `xah-recently-closed-buffers-max'.")

(defvar xah-create-buffer-backup t "If true, `xah-close-current-buffer' creates a backup file when closing non-file buffer.
Version: 2024-11-09")

(defvar xah-temp-dir-path
  (concat user-emacs-directory "temp/")
  "Path to temp dir used by xah commands.
by default, the value is dir named temp at `user-emacs-directory'.")

(defun xah-add-to-recently-closed (&optional BufferName BufferFileName)
  "Add to `xah-recently-closed-buffers'.
URL `http://xahlee.info/emacs/emacs/elisp_close_buffer_open_last_closed.html'
Created: 2023-03-02
Version: 2026-04-14"
  (let ((bufname (if BufferName BufferName (buffer-name)))
        (fpath (if BufferFileName BufferFileName buffer-file-name)))
    (setq xah-recently-closed-buffers (cons (cons bufname fpath) xah-recently-closed-buffers)))
  (when (length> xah-recently-closed-buffers xah-recently-closed-buffers-max)
    (setq xah-recently-closed-buffers (butlast xah-recently-closed-buffers 1))))

(defun xah-close-current-buffer ()
  "Close the current buffer with possible backup.

• If the buffer is a file and modified, save it first.
• If the buffer is not a file, and variable `xah-create-buffer-backup' is true, then save a backup to `xah-temp-dir-path' named untitled_‹datetime›_‹randomhex›.txt.

If `universal-argument' is called first, call `kill-buffer'. (this is useful to force kill.)

If the buffer is a file, add the path to the list `xah-recently-closed-buffers'. so you can reopen it by `xah-open-last-closed'.

URL `http://xahlee.info/emacs/emacs/elisp_close_buffer_open_last_closed.html'
Created: 2016-06-19
Version: 2026-04-14"
  (interactive)
  (widen)
  (cond
   (current-prefix-arg (kill-buffer))
   ;; ((eq major-mode 'minibuffer-inactive-mode) (minibuffer-keyboard-quit))
   ;; ((active-minibuffer-window) (minibuffer-keyboard-quit))
   ((minibufferp (current-buffer)) (minibuffer-keyboard-quit))

   ((eq major-mode 'dired-mode)
    (xah-add-to-recently-closed (buffer-name) default-directory)
    (kill-buffer))

   (buffer-file-name
    (when (buffer-modified-p)
      (progn
        (save-buffer)
        (message "Buffer file modified. Now it saved.\n%s" buffer-file-name)))
    (xah-add-to-recently-closed (buffer-name) buffer-file-name)
    (kill-buffer))

   ((and xah-create-buffer-backup (not buffer-file-name) (xah-user-buffer-p) (not (eq (point-max) 1)))
    (let ((xnewName (format "%suntitled_%s_%x.txt"
                            xah-temp-dir-path
                            (format-time-string "%Y-%m-%d_%H%M%S")
                            (random #xfffff))))
      (when (not (file-exists-p xah-temp-dir-path)) (make-directory xah-temp-dir-path))
      (write-region (point-min) (point-max) xnewName)
      (xah-add-to-recently-closed (buffer-name) xnewName)
      (kill-buffer)))
   (t (kill-buffer))))

(defun xah-open-last-closed ()
  "Open the last closed file.
URL `http://xahlee.info/emacs/emacs/elisp_close_buffer_open_last_closed.html'
Created: 2016-06-19
Version: 2026-04-14"
  (interactive)
  (if (length> xah-recently-closed-buffers 0)
      (find-file (cdr (pop xah-recently-closed-buffers)))
    (progn (message "No recently close buffer in this session."))))

(defun xah-open-recently-closed ()
  "Open recently closed file. Prompt for a choice.

URL `http://xahlee.info/emacs/emacs/elisp_close_buffer_open_last_closed.html'
Created: 2016-06-19
Version: 2026-04-14"
  (interactive)
  (find-file
   (let ((completion-ignore-case t))
     (completing-read
      "Open:"
      (mapcar (lambda (f) (cdr f)) xah-recently-closed-buffers)
      nil t))))

(defun xah-list-recently-closed ()
  "List recently closed file.
URL `http://xahlee.info/emacs/emacs/elisp_close_buffer_open_last_closed.html'
Version: 2016-06-19"
  (interactive)
  (let ((xbuf (generate-new-buffer "*recently closed*")))
    (switch-to-buffer xbuf)
    (mapc (lambda (xf) (insert (cdr xf) "\n"))
          xah-recently-closed-buffers)))

(defvar xah-open-file-at-cursor-pre-hook nil "Hook for `xah-open-file-at-cursor'.
Functions in the hook is called in order, each given the raw input text (path) as arg.
The first return non-nil, its value is given to `xah-open-file-at-cursor' as input. rest functions in hook is ignored.
This is useful for transforming certain url into file path. e.g. change
http://xahlee.info/emacs/index.html
to
C:/Users/xah/web/xahlee_info/emacs/index.html
, so instead of opening in browser, it opens in emacs as file.")

(defun xah-open-file-at-cursor ()
  "Open the file path under cursor.

• If there is selection, use it for path.
• Path can be {relative, full path, URL}.
• If the path starts with 「https*://」, open the URL in browser.
• Path may have a trailing 「:‹n›」 that indicates line number, or 「:‹n›:‹m›」 with line and column number. If so, jump to that line number.

If path does not have a file extension, automatically try with .el for elisp files.

See also `xah-open-file-at-cursor-pre-hook'.

This command is similar to `find-file-at-point' but without prompting for confirmation.

URL `http://xahlee.info/emacs/emacs/emacs_open_file_path_fast.html'
Created: 2020-10-17
Version: 2024-09-25"
  (interactive)
  (let (xinput xinput2 xpath)
    (setq xinput (if (region-active-p)
                     (buffer-substring-no-properties (region-beginning) (region-end))
                   (let ((xp0 (point)) xbeg xend
                         (xpathStops "^  \t\n\"`'‘’“”|()[]{}「」<>〔〕〈〉《》【】〖〗«»‹›❮❯❬❭〘〙·。\\"))
                     (skip-chars-backward xpathStops)
                     (setq xbeg (point))
                     (goto-char xp0)
                     (skip-chars-forward xpathStops)
                     (setq xend (point))
                     (goto-char xp0)
                     (buffer-substring-no-properties xbeg xend))))
    (setq xinput2 (if (length> xah-open-file-at-cursor-pre-hook 0)
                      (let ((xprehook (run-hook-with-args-until-success 'xah-open-file-at-cursor-pre-hook xinput)))
                        (if xprehook xprehook xinput))
                    xinput))

    (setq xpath
          (cond
           ((string-match "\\`file:///[A-Za-z]:/" xinput2) (substring xinput2 8))
           ((string-match "\\`file://[A-Za-z]:/" xinput2) (substring xinput2 7))
           (t xinput2)))

    (if (string-match-p "\\`https?://" xpath)
        (browse-url xpath)
      (let ((xpathNoQ
             (let ((xHasQuery (string-match "\?[a-z]+=" xpath)))
               (if xHasQuery
                   (substring xpath 0 xHasQuery)
                 xpath))))
        (cond
         ((string-match "#" xpathNoQ)
          (let ((xfpath (substring xpathNoQ 0 (match-beginning 0)))
                (xfractPart (substring xpathNoQ (1+ (match-beginning 0)))))
            (if (file-exists-p xfpath)
                (progn
                  (find-file xfpath)
                  (goto-char (point-min))
                  (search-forward xfractPart))
              (progn
                (message "File does not exist. Created at\n%s" xfpath)
                (find-file xfpath)))))
         ((string-match "\\`\\(.+?\\):\\([0-9]+\\)\\(:[0-9]+\\)?\\'" xpathNoQ)
          (let ((xfpath (match-string-no-properties 1 xpathNoQ))
                (xlineNum (string-to-number (match-string-no-properties 2 xpathNoQ))))
            (if (file-exists-p xfpath)
                (progn
                  (find-file xfpath)
                  (goto-char (point-min))
                  (forward-line (1- xlineNum)))
              (progn
                (message "File does not exist. Created at\n%s" xfpath)
                (find-file xfpath)))))
         ((file-exists-p xpathNoQ)
          (progn ; open f.ts instead of f.js
            (let ((xext (file-name-extension xpathNoQ))
                  (xfnamecore (file-name-sans-extension xpathNoQ)))
              (if (and (string-equal xext "js")
                       (file-exists-p (concat xfnamecore ".ts")))
                  (progn
                    (find-file (concat xfnamecore ".ts"))
                    (warn "Typescript file .ts exist, opening it"))

                (find-file xpathNoQ)))))
         ((file-exists-p (concat xpathNoQ ".el"))
          (find-file (concat xpathNoQ ".el")))
         (t (progn
              (message "File does not exist. Created at\n%s" xpathNoQ)
              (find-file xpathNoQ))))))))

;; s------------------------------

(defun xah-java-compile-and-run (Filename)
  "Compile and run java of current buffer.
Buffer is saved first if modified.

This command is designed for a simple single java class source file.
requires the commands 「javac」 and 「java」.

If compile fails, the error is displayed in a buffer.

URL `http://xahlee.info/emacs/emacs/elisp_run_current_file.html'
Created: 2024-12-20
Version: 2024-12-20"
  (interactive (if buffer-file-name (progn (when (buffer-modified-p) (save-buffer)) (list buffer-file-name)) (user-error "Buffer is not file. Save it first.")))
  (let ((xoutbuf (get-buffer-create "*xah java output*" t))
        (xjavac-buf (get-buffer-create "*xah java compile output*" t)))
    (with-current-buffer xjavac-buf (erase-buffer))
    (call-process "javac" nil xjavac-buf nil Filename)
    (if (eq 1 (with-current-buffer xjavac-buf (point-max)))
        (progn
          (with-current-buffer xoutbuf (erase-buffer))
          (call-process "java" nil xoutbuf nil (file-name-nondirectory (file-name-sans-extension Filename)))
          (display-buffer xoutbuf))
      (display-buffer xjavac-buf))))

(defvar xah-run-current-file-dispatch
  (list (cons "el" 'load)
        (cons "elc" 'load)
        (cons "java" 'xah-java-compile-and-run))
  "A dispatch table used by `xah-run-current-file' to call dedicated function to run code.
Value is a association list.
Each item is (EXT . FUNCTION).
EXT is filename extension (sans the dot), of type string.
FUNCTION is a elisp function name to call, of type symbol.
If file extension match, and FUNCTION is defined, call it, pass current buffer's filepath as arg.
Else, `xah-run-current-file-map' is looked up.
URL `http://xahlee.info/emacs/emacs/elisp_run_current_file.html'
")

(defvar xah-run-current-file-map
  '(
    ;; following are tested as of 2024-12-20
    ("fs" . "dotnet fsi")
    ("fsx" . "dotnet fsi")
    ("go" . "go run")
    ("js" . "deno run")
    ("php" . "php")
    ("pl" . "perl")
    ("ps1" . "pwsh")
    ("py" . "python")
    ("py2" . "python2")
    ("py3" . "python3")
    ("rb" . "ruby")
    ("ts" . "deno run")
    ("m" . "wolframscript -print all -file")
    ("wl" . "wolframscript -print all -file")
    ("wls" . "wolframscript -print all -file")

    ;; following may be outdated

    ("clj" . "clj")
    ("hs" . "runhaskell")
    ("latex" . "pdflatex")
    ("ml" . "ocaml")
    ("rkt" . "racket")
    ("sh" . "bash")
    ("tex" . "pdflatex")
    ("tsx" . "tsc")
    ("vbs" . "cscript")
    ("pov" . "povray +R2 +A0.1 +J1.2 +Am2 +Q9 +H480 +W640"))
  "A association list that maps file extension to a command for running the file, used by `xah-run-current-file'.
Each item is (EXT . PROGRAM).
EXT is filename extension (sans the dot), type string.
PROGRAM is program name or path, with command options to run a file, type string.
A filename is appended after the PROGRAM string as external command to call.
URL `http://xahlee.info/emacs/emacs/elisp_run_current_file.html'
")

(defun xah-run-current-file (Filename)
  "Execute the current file.
Output is printed to buffer *xah-run output*.

File suffix is used to determine what external command to run, in the variable `xah-run-current-file-map'.

If file is modified, it is auto saved before run.

The variable `xah-run-current-file-dispatch' allows you to customize this command to call other function to run the current file.

URL `http://xahlee.info/emacs/emacs/elisp_run_current_file.html'
Created: 2020-09-24
Version: 2025-08-07"
  (interactive (if buffer-file-name (progn (when (buffer-modified-p) (save-buffer)) (list buffer-file-name)) (user-error "Buffer is not file. Save it first.")))
  (let ((xoutbuf (get-buffer-create "*xah-run output*" t))
        (xext (file-name-extension Filename))
        xdispatch)
    (setq xdispatch (assoc xext xah-run-current-file-dispatch))
    (if xdispatch
        (if (fboundp (cdr xdispatch))
            (progn
              (message "%s dispatch call %s" real-this-command xdispatch)
              (funcall (cdr xdispatch) Filename))
          (warn "`xah-run-current-file' found function %s in xah-run-current-file-dispatch but it is unbound. Normal run continues using `xah-run-current-file-map'." xdispatch))
      (let ((xappCmdStr (cdr (assoc xext xah-run-current-file-map))))
        (when (not xappCmdStr) (error "%s: Unknown file extension: %s. check `xah-run-current-file-map'" real-this-command xext))
        (progn
          (with-current-buffer xoutbuf (erase-buffer))
          (apply 'start-process (append (list "xah-run" xoutbuf) (split-string xappCmdStr " +" t) (list Filename) nil))
          (display-buffer xoutbuf)
          ;; (pop-to-buffer xoutbuf)
          ;; (with-current-buffer xoutbuf (goto-char (point-min)))
          ))))
  (message "done. %s" real-this-command))

(defun xah-clean-empty-lines ()
  "Replace repeated blank lines to just 1, in whole buffer or selection.
Respects `narrow-to-region'.

URL `http://xahlee.info/emacs/emacs/elisp_compact_empty_lines.html'
Created: 2017-09-22
Version: 2020-09-08"
  (interactive)
  (let (xbegin xend)
    (if (region-active-p)
        (setq xbegin (region-beginning) xend (region-end))
      (setq xbegin (point-min) xend (point-max)))
    (save-excursion
      (save-restriction
        (narrow-to-region xbegin xend)
        (progn
          (goto-char (point-min))
          (while (re-search-forward "\n\n\n+" nil 1)
            (replace-match "\n\n")))))))

(defun xah-clean-whitespace (&optional Begin End)
  "Delete trailing whitespace, and replace repeated blank lines to just 1.
Only space and tab is considered whitespace here.
Works on whole buffer or selection, respects `narrow-to-region'.

URL `http://xahlee.info/emacs/emacs/elisp_compact_empty_lines.html'
Created: 2017-09-22
Version: 2025-05-04"
  (interactive)
  (let (xbeg xend)
    (seq-setq (xbeg xend)
              (if (and Begin End)
                  (list Begin End)
                (if (region-active-p)
                    (list (region-beginning) (region-end))
                  (list (point-min) (point-max)))))
    (save-excursion
      (save-restriction
        (narrow-to-region xbeg xend)
        (progn
          (goto-char (point-min))
          (while (re-search-forward "[ \t]+\n" nil t) (replace-match "\n")))
        (progn
          (goto-char (point-min))
          (while (re-search-forward "\n\n\n+" nil t) (replace-match "\n\n")))
        (progn
          (goto-char (point-max))
          (while (eq (char-before) 32) (delete-char -1))))))
  (message "done xah-clean-whitespace"))

(defun xah-make-backup ()
  "Make a backup copy of current file or dired marked files.
If in dired, backup current file or marked files.

The backup file name is the original name with the datetime .yyyymmddhhmmss~ appended.

Overwrite existing file.

If the current buffer is not associated with a file, do nothing.

URL `http://xahlee.info/emacs/emacs/elisp_make-backup.html'
Created: 2018-06-06
Version: 2025-07-22"
  (interactive)
  (let ((xtimestamp (format-time-string "%Y%m%d%H%M%S")))
    (if buffer-file-name
        (let ((xbackupName (concat buffer-file-name "." xtimestamp "~")))
          (copy-file buffer-file-name xbackupName t)
          (message "\nBackup saved at: %s" xbackupName))
      (if (eq major-mode 'dired-mode)
          (progn
            (mapc (lambda (xx)
                    (let ((xbackupName
                           (concat xx "." xtimestamp "~")))
                      (copy-file xx xbackupName t)))
                  (dired-get-marked-files))
            (revert-buffer))
        (user-error "xah-make-backup: buffer not file nor dired")))))

(defun xah-make-backup-and-save ()
  "Backup of current file and save, or backup dired marked files.
For detail, see `xah-make-backup'.
If the current buffer is not associated with a file nor dired, nothing's done.

URL `http://xahlee.info/emacs/emacs/elisp_make-backup.html'
Created: 2015-10-14
Version: 2025-09-26"
  (interactive)
  (if buffer-file-name
      (progn
        (let ((xtimestamp (format-time-string "%Y%m%d%H%M%S")))
          (let ((xbackupName (concat buffer-file-name "." xtimestamp "~")))
            (copy-file buffer-file-name xbackupName t)
            (message "\nBackup saved at: %s" xbackupName)))
        (when (buffer-modified-p)
          (save-buffer)))
    (progn
      (xah-make-backup))))

(defun xah-delete-current-file-make-backup ()
  "Makes a backup~, delete current file, close the buffer.

Backup filename is ‹name›.‹dateTimeStamp›~
Overwrite existing file.

If buffer is not a file, copy content to `kill-ring', delete buffer.

If buffer is not a file, the backup file name starts with “xx_”.

Call `xah-open-last-closed' to open the backup file.

URL `http://xahlee.info/emacs/emacs/elisp_delete-current-file.html'
Created: 2018-05-15
Version: 2026-02-22"
  (interactive)
  (when (eq major-mode 'dired-mode)
    (user-error "%s: In dired. Nothing is done." real-this-command))
  (let ((xfname buffer-file-name)
        (xbuffname (buffer-name)))
    (if xfname
        (let ((xbackupPath
               (concat
                xfname "."
                (format-time-string "%Y%m%d%H%M%S") "~")))
          (save-buffer xfname)
          (kill-buffer xbuffname)
          (rename-file xfname xbackupPath t)
          (message "done.
xah-delete-current-file-make-backup.
Backup at
%s
To open, do M-x xah-open-last-closed" xbackupPath)
          (when (boundp 'xah-recently-closed-buffers)
            (push (cons nil xbackupPath) xah-recently-closed-buffers)))
      (progn
        (widen)
        (kill-new (buffer-string))
        (kill-buffer xbuffname)
        (message "non-file buffer killed. buffer text copied to `kill-ring'."))))
  (when (eq major-mode 'dired-mode) (revert-buffer)))

;; s------------------------------

(defun xah-search-current-word ()
  "Call `isearch' on current word or selection.
“word” here is A to Z, a to z, and hyphen [-] and lowline [_], independent of syntax table.

URL `http://xahlee.info/emacs/emacs/emacs_search_current_word.html'
Created: 2010-05-29
Version: 2026-01-06"
  (interactive)
  (let (xbeg xend)
    (if (region-active-p)
        (setq xbeg (region-beginning) xend (region-end))
      (save-excursion
        (skip-chars-backward "-_A-Za-z0-9")
        (setq xbeg (point))
        (forward-char)
        (skip-chars-forward "-_A-Za-z0-9")
        (setq xend (point))))
    (deactivate-mark)
    (when (< xbeg (point)) (goto-char xbeg))
    (isearch-mode t)
    (isearch-yank-string (buffer-substring-no-properties xbeg xend))))

(defun xah-fly-cancel ()
  "Cancle selection or call `minibuffer-keyboard-quit' and `keyboard-quit'.
This command is intended to replace key C-g , but not always work. Sometimes you still need to press C-g to cancel or abort or exit some commands.

Created: 2025-08-01
Version: 2025-08-03"
  (interactive)
  (if (minibufferp (current-buffer))
      (progn (minibuffer-keyboard-quit))
    (if (region-active-p)
        (progn (deactivate-mark))
      (progn (keyboard-quit)))))

(declare-function w32-shell-execute "w32fns.c" (operation document &optional parameters show-flag)) ; (w32-shell-execute "open" default-directory)

(defun xah-show-in-desktop ()
  "Show current file in desktop.
 (Mac Finder, Microsoft Windows File Explorer, Linux file manager)
This command can be called when in a file buffer or in `dired'.

URL `http://xahlee.info/emacs/emacs/emacs_show_in_desktop.html'
Created: 2020-11-20
Version: 2023-09-09"
  (interactive)
  (let ((xpath (if (eq major-mode 'dired-mode)
                   (if (eq nil (dired-get-marked-files))
                       default-directory
                     (car (dired-get-marked-files)))
                 (if buffer-file-name buffer-file-name default-directory))))
    (cond
     ((eq system-type 'windows-nt)
      (shell-command (format "PowerShell -Command invoke-item '%s'" default-directory ))
      ;; (let ((xcmd (format "Explorer /select,%s"
      ;;                     (replace-regexp-in-string "/" "\\" xpath t t)
      ;;                     ;; (shell-quote-argument (replace-regexp-in-string "/" "\\" xpath t t ))
      ;;                     )))
      ;;   (shell-command xcmd))
      )
     ((eq system-type 'darwin)
      (shell-command
       (concat "open -R " (shell-quote-argument xpath))))
     ((eq system-type 'gnu/linux)
      (call-process shell-file-name nil 0 nil
                    shell-command-switch
                    (format "%s %s"
                            "xdg-open"
                            (file-name-directory xpath)))
      ;; (shell-command "xdg-open .") ;; 2013-02-10 this sometimes froze emacs till the folder is closed. eg with nautilus
      ))))

(defun xah-open-in-vscode ()
  "Open current file or dir in vscode.
URL `http://xahlee.info/emacs/emacs/emacs_open_in_vscode.html'

Created: 2020-02-13
Version: 2024-12-15"
  (interactive)
  (let ((xpath (or buffer-file-name default-directory)))
    (message "path is %s" xpath)
    (cond
     ((eq system-type 'darwin)
      (shell-command (format "open -a Visual\\ Studio\\ Code.app %s" (shell-quote-argument xpath))))
     ((eq system-type 'windows-nt)
      (shell-command (format "code.cmd %s" (shell-quote-argument xpath))))
     ((eq system-type 'gnu/linux)
      (shell-command (format "code %s" (shell-quote-argument xpath)))))))

(defun xah-open-in-external-app (&optional Fname)
  "Open the current file or dired marked files in external app.
When called in emacs lisp, if Fname is given, open that.

URL `http://xahlee.info/emacs/emacs/emacs_dired_open_file_in_ext_apps.html'
Created: 2019-11-04
Version: 2025-04-18"
  (interactive)
  (let (xfileList xdoIt)
    (setq xfileList
          (if Fname
              (list Fname)
            (if (eq major-mode 'dired-mode)
                (dired-get-marked-files)
              (list buffer-file-name))))
    (setq xdoIt (if (length< xfileList 10) t (y-or-n-p "Open more than 10 files? ")))
    (when xdoIt
      (cond
       ((eq system-type 'windows-nt)
        (let ((xoutbuf (get-buffer-create "*xah open in external app*"))
              (xcmdlist (list "PowerShell" "-Command" "Invoke-Item" "-LiteralPath")))
          (mapc
           (lambda (x)
             (apply 'start-process (append (list "xah open in external app" xoutbuf) xcmdlist (list (format "'%s'" (string-replace "'" "`'" x))) nil)))
           xfileList)))
       ((eq system-type 'darwin)
        (mapc (lambda (xfpath) (shell-command (concat "open " (shell-quote-argument xfpath)))) xfileList))
       ((eq system-type 'gnu/linux)
        (mapc (lambda (xfpath)
                (call-process shell-file-name nil 0 nil
                              shell-command-switch
                              (format "%s %s"
                                      "xdg-open"
                                      (shell-quote-argument xfpath))))
              xfileList))
       ((eq system-type 'berkeley-unix)
        (mapc (lambda (xfpath) (let ((process-connection-type nil)) (start-process "" nil "xdg-open" xfpath))) xfileList))))))

(defvar xah-fly-mswin-terminal
  "wt"
  "A string. Value should be one of: wt (for Windows Terminal) or pwsh (for PowerShell Core (cross-platform)) or powershell (for Microsoft PowerShell).")

(defun xah-open-in-terminal ()
  "Open the current dir in a new terminal window.
On Microsoft Windows, which terminal it starts depends on `xah-fly-mswin-terminal'.

URL `http://xahlee.info/emacs/emacs/emacs_open_in_terminal.html'
Created: 2020-11-21
Version: 2023-06-26"
  (interactive)
  (cond
   ((eq system-type 'windows-nt)
    (cond
     ((string-equal xah-fly-mswin-terminal "wt")
      (shell-command (format "wt -d \"%s\"" default-directory)))
     ((string-equal xah-fly-mswin-terminal "pwsh")
      (shell-command
       (format "pwsh -Command Start-Process pwsh -WorkingDirectory '%s'" (shell-quote-argument default-directory))))
     ((string-equal xah-fly-mswin-terminal "powershell")
      (shell-command
       (format "powershell -Command Start-Process powershell -WorkingDirectory '%s'" (shell-quote-argument default-directory))))
     (t (error "Error 702919: value of `xah-fly-mswin-terminal' is not expected. Its value is %s" xah-fly-mswin-terminal))))
   ((eq system-type 'darwin)
    (shell-command (concat "open -a terminal " (shell-quote-argument default-directory))))
   ((eq system-type 'gnu/linux)
    (let ((process-connection-type nil)) (start-process "" nil "x-terminal-emulator" (concat "--working-directory=" default-directory))))
   ((eq system-type 'berkeley-unix)
    (let ((process-connection-type nil)) (start-process "" nil "x-terminal-emulator" (concat "--working-directory=" default-directory))))))

(defun xah-next-window-or-frame ()
  "Switch to next window or frame.
If current frame has only one window, switch to next frame.
If `universal-argument' is called first, do switch frame.
Version: 2017-01-27"
  (interactive)
  (if current-prefix-arg
      (other-frame 1)
    (if (one-window-p)
        (other-frame 1)
      (other-window 1))))

(defun xah-unsplit-window-or-next-frame ()
  "Unsplit window. If current frame has only one window, switch to next frame.
Version: 2017-01-29"
  (interactive)
  (if (one-window-p)
      (other-frame 1)
    (delete-other-windows)))

;; s------------------------------
;; layout lookup tables for key conversion

(defvar xah-fly-layout-diagrams (make-hash-table :test 'equal)
  "A hashtable.
Key is string, of keyboard layout name.
Value is a string, of text-art (aka ASCII-art) form of the layout.
The text-art string, each key (sequence of chars) is separated by whitespace.
The string is split by white space, and each item is considered a key.
The key is usually a single char, can be unicode, but may also be alt ctrl tab shift return and other.
It is used to generate key conversion table of a key from layout to layout.
")

(progn

  (puthash "adnw" "
~ ! @ # $ % ^ & * ( ) { }
` 1 2 3 4 5 6 7 8 9 0 [ ]

k u ü . ä v g c l j f = \\
h i e a o d t r n s ß
x y ö , q b p w m z

K U Ü > Ä V G C L J F + |
H I E A O D T R N S ẞ
X Y Ö < Q B P W M Z
" xah-fly-layout-diagrams)

  (puthash "azerty"
           "
~ ! @ # $ % ^ & * ( ) { }
² & é \" ' ( - è _ ç à ) =

a z e r t y u i o p ^ $ *
q s d f g h j k l m ù
w x c v b n , ; : !

A Z E R T Y U I O P ? + |
Q S D F G H J K L M Ù
W X C V B N ? . / §
" xah-fly-layout-diagrams)

  (puthash "azerty-be"
           "
~ ! @ # $ % ^ & * ( ) { }
² & é \" ' ( § è ! ç à ) -

a z e r t y u i o p ^ $ µ
q s d f g h j k l m ù
w x c v b n , ; : =

A Z E R T Y U I O P ^ $ Μ
Q S D F G H J K L M Ù
W X C V B N ? . / +
" xah-fly-layout-diagrams)

  (puthash "bepo" "
#  1 2 3 4 5 6 7 8 9 0 ° `
$ \" « » ( ) @ + - / * = %

b é p o è ^ v d l j z w \\
a u i e , c t s r n m
à y x . k ' q g h f

B É P O È ! V D L J Z W |
A U I E ; C T S R N M
À Y X : K ? Q G H F
" xah-fly-layout-diagrams)

  (puthash "colemak" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q w f p g j l u y ; [ ] \\
a r s t d h n e i o '
z x c v b k m , . /

Q W F P G J L U Y : { } |
A R S T D H N E I O \"
Z X C V B K M < > ?
" xah-fly-layout-diagrams)

  (puthash "colemak-dh" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q w f p b j l u y ; [ ] \\
a r s t g m n e i o '
z x c d v k h , . /

Q W F P B J L U Y : { } |
A R S T G M N E I O \"
Z X C D V K H < > ?
" xah-fly-layout-diagrams)

  (puthash "dvorak" "
~ ! @ # $ % ^ & * ( ) { }
` 1 2 3 4 5 6 7 8 9 0 [ ]

' , . p y f g c r l / = \\
a o e u i d h t n s -
; q j k x b m w v z

\" < > P Y F G C R L ? + |
A O E U I D H T N S _
: Q J K X B M W V Z
" xah-fly-layout-diagrams)

  (puthash "engrammer" "
~ ! @ # $ % ^ & * ( ) { }
` 1 2 3 4 5 6 7 8 9 0 [ ]

b y o u ' ; l d w v z = \\
c i e a , . h t s n q
g x j k - / r m f p

B Y O U \" : L D W V Z + |
C I E A < > H T S N Q
G X J K _ ? R M F P
" xah-fly-layout-diagrams)

  (puthash "koy" "
^ ! @ # $ % ^ & * ( ) _ ~
˘ 1 2 3 4 5 6 7 8 9 0 - `

k . o , y v g c l ß / = \\
h a e i u d t r n s f
x q ä ü ö b p w m j

K > O < Y V G C L ẞ ? + |
H A E I U D T R N S F
X Q Ä Ü Ö B P W M J
" xah-fly-layout-diagrams)

  (puthash "halmak" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

w l r b z ; q u d j [ ] \\
s h n t , . a e o i '
f m v c / g p x k y

W L R B Z : Q U D J { } |
S H N T < > A E O I \"
F M V C ? G P X K Y
" xah-fly-layout-diagrams)

  (puthash "minimak" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q w d r k y u i o p [ ] \\
a s t f g h j e l ; '
z x c v b n m , . /

Q W D R K Y U I O P { } |
A S T F G H J E L : \"
Z X C V B N M < > ?
" xah-fly-layout-diagrams)

(puthash "neo2" "
ˇ ° § ℓ » « $ € „ “ ” — ¸
^ 1 2 3 4 5 6 7 8 9 0 - `

x v l c w k h g f q ß ´ \\
u i a e o s n r t d y
ü ö ä p z b m , . j

X V L C W K H G F Q ẞ ~ |
U I A E O S N R T D Y
Ü Ö Ä P Z B M – • J
" xah-fly-layout-diagrams)

  (puthash "norman" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q w d f k j u r l ; [ ] \\
a s e t g y n i o h '
z x c v b p m , . /

Q W D F K J U R L : { } |
A S E T G Y N I O H \"
Z X C V B P M < > ?
" xah-fly-layout-diagrams)

  (puthash "programer-dvorak" "
~ % 7 5 3 1 9 0 2 4 6 8 `
$ & [ { } ( = * ) + ] ! #

; , . p y f g c r l / @ \\
a o e u i d h t n s -
' q j k x b m w v z

: < > P Y F G C R L ? ^ |
A O E U I D H T N S _
\" Q J K X B M W V Z
" xah-fly-layout-diagrams)

  (puthash "pt-nativo" "
* ! \" # $ % & / ( ) = ª >
+ 1  2 3 4 5 6 7 8 9 0 º <

' , . h x w l t c p ~ - \\
i e a o u m d s r n ´
« ç j b k q v g f z

? ; : H X W L T C P ^ _ |
I E A O U M D S R N `
Y Ç J B K Q V G F Z
" xah-fly-layout-diagrams)

  (puthash "qfmlwy" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q f m l w y u o b j [ ] \\
d s t n r i a e h ; '
z v g c x p k , . /

Q F M L W Y U O B J { } |
D S T N R I A E H : \"
Z V G C X P K < > ?
" xah-fly-layout-diagrams)

(puthash "qgmlwb" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q g m l w b y u v ; [ ] \\
d s t n r i a e o h '
z x c f j k p , . /

Q G M L W B Y U V : { } |
D S T N R I A E O H \"
Z X C F J K P < > ?
" xah-fly-layout-diagrams)

  (puthash "qwerty" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q w e r t y u i o p [ ] \\
a s d f g h j k l ; '
z x c v b n m , . /

Q W E R T Y U I O P { } |
A S D F G H J K L : \"
Z X C V B N M < > ?
" xah-fly-layout-diagrams)

  (puthash "qwerty-abnt" "
\" ! @ # $ % ^ & * ( ) _ +
 ' 1 2 3 4 5 6 7 8 9 0 - =

q w e r t y u i o p ´ [ ]
a s d f g h j k l ç ~
z x c v b n m , . ;

Q W E R T Y U I O P ` + |
A S D F G H J K L Ç ^
Z X C V B N M < > :
" xah-fly-layout-diagrams)

  (puthash "qwerty-no" "
§ ! \" # ¤ % & / ( ) = ? `
| 1  2 3 4 5 6 7 8 9 0 + \\

q w e r t y u i o p å ¨ '
a s d f g h j k l ø æ
z x c v b n m , . -

Q W E R T Y U I O P Å ^ *
A S D F G H J K L Ø Æ
Z X C V B N M < > _
" xah-fly-layout-diagrams)

  (puthash "qwerty-se" "
§ ! \" # ¤ % & / ( ) = ? `
| 1  2 3 4 5 6 7 8 9 0 + \\

q w e r t y u i o p å ¨ '
a s d f g h j k l ö ä
z x c v b n m , . -

Q W E R T Y U I O P Å ^ *
A S D F G H J K L Ö Ä
Z X C V B N M < > _
" xah-fly-layout-diagrams)

  (puthash "qwertz" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q w e r t z u i o p [ ] \\
a s d f g h j k l ; '
y x c v b n m , . /

Q W E R T Z U I O P { } |
A S D F G H J K L : \"
Y X C V B N M < > ?
" xah-fly-layout-diagrams)

  (puthash "qwpr" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q w p r f y u k l ; [ ] \\
a s d t g h n i o e '
z x c v b j m , . /

Q W P R F Y U K L : { } |
A S D T G H N I O E \"
Z X C V B J M < > ?
" xah-fly-layout-diagrams)

  (puthash "russian" "
Ё ! \" № ; % : ? * ( ) _ +
ё 1  2 3 4 5 6 7 8 9 0 - =

й ц у к е н г ш щ з х ъ \\
ф ы в а п р о л д ж э
я ч с м и т ь б ю .

Й Ц У К Е Н Г Ш Щ З Х Ъ |
Ф Ы В А П Р О Л Д Ж Э
Я Ч С М И Т Ь Б Ю ,
" xah-fly-layout-diagrams)

  (puthash "workman" "
~ ! @ # $ % ^ & * ( ) _ +
` 1 2 3 4 5 6 7 8 9 0 - =

q d r w b j f u p ; [ ] \\
a s h t g y n e o i '
z x m c v k l , . /

Q D R W B J F U P : { } |
A S H T G Y N E O I \"
Z X M C V K L < > ?
" xah-fly-layout-diagrams))

(defun xah-fly-create-key-conv-table (Layout1 Layout2)
  "Takes two text diagrams Layout1 Layout2, return a hashtable.
For remapping key from layout1 to layout2.

example:

Layout1 is a string. e.g.

a b c d
e f shift

Layout2 is a string. e.g.

a o e i
m n ctrl

return a hashtable, e.g.

b → o
c → e
d → i
e → m
f → n
shift → ctrl

If the keys in layouts are the same, it's not in the table.

Created: 2024-04-19
Version: 2025-06-05"
  (let (xkeys1 xkeys2 (xtable (make-hash-table :test 'equal)))
    (setq xkeys1 (split-string Layout1 "[ \n]+" t))
    (setq xkeys2 (split-string Layout2 "[ \n]+" t))
    (when (not (eq (length xkeys1) (length xkeys2)))
      (error "layout %s and %s lengths not same." (length Layout1) (length Layout2)))
    (seq-mapn
     (lambda (x y)
       (if (string-equal x y)
           nil
         (puthash x y xtable)))
     xkeys1 xkeys2)
    xtable))

;; (xah-fly-create-key-conv-table
;;   (gethash "qwerty" xah-fly-layout-diagrams)
;;   (gethash "dvorak" xah-fly-layout-diagrams))

(defvar xah-fly-key-current-layout nil
 "The current keyboard layout.
Value is a key in `xah-fly-layout-diagrams'.
Do not set this variable manually.
Use `xah-fly-keys-set-layout' to set it.
Default to qwerty.
Version: 2022-10-22")

(if xah-fly-key-current-layout nil (setq xah-fly-key-current-layout "qwerty"))

(defvar xah-fly--key-convert-table nil
  "A hashtable that's the conversion table from dvorak to current layout.
Value is a hashtable.
Created: 2019-02-12
Version: 2025-07-11" )

(setq
 xah-fly--key-convert-table
 (xah-fly-create-key-conv-table
  (gethash "dvorak" xah-fly-layout-diagrams)
  (gethash xah-fly-key-current-layout xah-fly-layout-diagrams)))

(defun xah-fly--convert-key (Keystr)
 "Return the corresponding Keystr according to variable `xah-fly--key-convert-table'.
Keystr must be a string that is the valid argument to `kbd'.
Keystr may be a single key, or key sequence separated by whitespaces.
Created: 2022-10-25
Version: 2024-04-22"
  (interactive)
  (mapconcat
   'identity
   (mapcar
    (lambda (x)
      (let ((xnew (gethash x xah-fly--key-convert-table)))
        (if xnew xnew x)))
    (split-string Keystr " +"))
   " "))

(defun xah-fly--define-keys (KeymapName KeyCmdAlist &optional Direct-p)
  "Map `define-key' over a alist KeyCmdAlist, with key layout remap.
The key is remapped from Dvorak to the current keyboard layout by `xah-fly--convert-key'.
If Direct-p is t, do not remap key to current keyboard layout.
Example usage:
 (xah-fly--define-keys
  (define-prefix-command 'xyz-map)
  '(
    (\"h\" . highlight-symbol-at-point)
    (\".\" . isearch-forward-symbol-at-point)
    (\"w\" . isearch-forward-word)))
Created: 2020-04-18
Version: 2025-10-07"
  (mapcar
   (lambda (x)
     (define-key
      KeymapName
      (kbd (if Direct-p (car x) (xah-fly--convert-key (car x))))
      (cdr x)))
   KeyCmdAlist))

;; s------------------------------
;; keymaps

(defvar xah-fly-key-map (make-sparse-keymap)
 "If `xah-fly-insert-state-p' is true, point to `xah-fly-insert-map', else, points to `xah-fly-command-map'.")

(defvar xah-fly-command-map (make-sparse-keymap)
  "Keymap when in command mode.")

(defvar xah-fly-insert-map (make-sparse-keymap)
  "Keymap when in insert mode.")

(defvar xah-fly--deactivate-command-mode-func nil)

;; s------------------------------
;; setting keys

(defun xah-fly-define-keys ()
  "Define the keys for xah-fly-keys.
Created: 2022-10-31
Version: 2024-04-22"
  (interactive)
  (let ()

    ;; Movement key integrations with built-in Emacs packages
    (xah-fly--define-keys
     indent-rigidly-map
     '(("h" . indent-rigidly-left)
       ("n" . indent-rigidly-right)))

    (xah-fly--define-keys
     xah-fly-command-map
     '(("<escape>" . xah-fly-command-mode-activate))
     :direct)

    (xah-fly--define-keys
     xah-fly-insert-map
     '(("<escape>" . xah-fly-command-mode-activate))
     :direct)

    (when xah-fly-use-isearch-arrows
      (xah-fly--define-keys
       isearch-mode-map
       '(("<up>" . isearch-ring-retreat)
         ("<down>" . isearch-ring-advance)
         ("<left>" . isearch-repeat-backward)
         ("<right>" . isearch-repeat-forward)
         ("C-v" . isearch-yank-kill))
       :direct)
      (xah-fly--define-keys
       minibuffer-local-isearch-map
       '(("<left>" . isearch-reverse-exit-minibuffer)
         ("<right>" . isearch-forward-exit-minibuffer))
       :direct))

    (xah-fly--define-keys
     (define-prefix-command 'xah-fly-leader-key-map)
     '(("SPC" . xah-fly-insert-mode-activate)
       ("RET" . execute-extended-command)

       ("TAB" . nil)

       ("TAB TAB" . indent-for-tab-command)

       ("TAB i" . complete-symbol)
       ("TAB g" . indent-rigidly)
       ("TAB r" . indent-region)
       ("TAB s" . indent-sexp)

       (". ." . highlight-symbol-at-point)
       (". g" . unhighlight-regexp)
       (". c" . highlight-lines-matching-regexp)
       (". h" . highlight-regexp)
       (". t" . highlight-phrase)
       (". p" . isearch-forward-symbol-at-point)
       (". u" . isearch-forward-symbol)
       (". e" . isearch-forward-word)

       ("'" . xah-fill-or-unfill)

       (", t" . xref-find-definitions)
       (", n" . xref-pop-marker-stack)

       ;; - / ; = [
       ("\\" . toggle-input-method)
       ;; `

       ("a" . mark-whole-buffer)
       ("b" . end-of-buffer)

       ("c ," . xah-open-in-external-app)
       ("c ." . find-file)
       ("c c" . bookmark-bmenu-list)
       ("c e" . ibuffer)
       ("c f" . xah-open-recently-closed)
       ("c g" . xah-open-in-terminal)
       ("c h" . recentf-open-files)
       ("c j" . xah-copy-file-path)
       ("c l" . bookmark-set)
       ("c n" . xah-new-empty-buffer)
       ("c o" . xah-show-in-desktop)
       ("c p" . xah-open-last-closed)
       ("c r" . bookmark-jump)
       ("c s" . write-file)
       ("c u" . xah-open-file-at-cursor)
       ("c x" . set-buffer-file-coding-system)
       ("c y" . xah-list-recently-closed)
       ("c z" . revert-buffer-with-coding-system)

       ;; set-buffer-process-coding-system
       ;; set-file-name-coding-system
       ;; set-keyboard-coding-system
       ;; set-language-environment
       ;; set-next-selection-coding-system
       ;; set-selection-coding-system
       ;; set-terminal-coding-system
       ;; universal-coding-system-argument

       ("d" . beginning-of-buffer)

       ("e 6" . xah-insert-white-corner-bracket『』)

       ("e a" . xah-insert-double-angle-bracket《》)
       ("e b" . xah-insert-black-lenticular-bracket【】)

       ("e c r" . expand-region-abbrevs)
       ("e c t" . edit-abbrevs)
       ("e c u" . expand-abbrev)

       ("e c g" . add-mode-abbrev)
       ("e c c" . add-global-abbrev)
       ("e c m" . inverse-add-mode-abbrev)
       ("e c w" . inverse-add-global-abbrev)

       ("e c f" . unexpand-abbrev)

       ("e c h" . expand-jump-to-previous-slot)
       ("e c n" . expand-jump-to-next-slot)
       ("e c y" . abbrev-prefix-mark)

       ("e d" . xah-insert-double-curly-quote“”)
       ("e e" . xah-insert-unicode)
       ("e f" . xah-insert-emacs-quote)
       ("e g" . xah-insert-ascii-double-quote)

       ("e h" . xah-insert-brace)
       ("e i" . xah-insert-curly-single-quote‘’)
       ("e j" . insert-char)
       ("e k" . xah-insert-markdown-quote)
       ("e l" . xah-insert-seperator)
       ("e m" . xah-insert-corner-bracket「」)
       ("e n" . xah-insert-square-bracket)
       ("e o" . xah-insert-ascii-single-quote)
       ("e p" . xah-insert-single-angle-quote‹›)
       ("e q" . xah-insert-deco-angle-fat-bracket❰❱)
       ("e r" . xah-insert-tortoise-shell-bracket〔〕)
       ("e s" . xah-insert-ascii-angle-bracket)
       ("e t" . xah-insert-paren)
       ("e u" . xah-insert-date)
       ("e v" . xah-insert-markdown-triple-quote)
       ("e w" . xah-insert-angle-bracket〈〉)
       ("e x" . xah-insert-white-lenticular-bracket〖〗)
       ("e y" . xah-insert-double-angle-quote«»)
       ("e z" . xah-insert-deco-angle-bracket❮❯)

       ("f" . xah-search-current-word)
       ("g" . xah-close-current-buffer)

       ("h a" . apropos-command)
       ("h b" . describe-bindings)
       ("h c" . describe-char)
       ("h d" . apropos-documentation)
       ("h e" . view-echo-area-messages)
       ("h f" . describe-face)
       ("h g" . info-lookup-symbol)
       ;; ("h h" . describe-function)
       ("h i" . info)
       ("h j" . man)
       ("h k" . describe-key)
       ("h l" . view-lossage)
       ("h m" . describe-mode)
       ("h n" . describe-variable)
       ("h o" . describe-language-environment)
       ;; p q
       ("h r" . apropos-variable)
       ("h s" . describe-syntax)
       ("h t" . describe-function)
       ("h u" . elisp-index-search)
       ("h v" . apropos-value)
       ("h x" . describe-command) ; emacs 28

       ("h z" . describe-coding-system)

       ("i SPC" . set-mark-command)
       ("i w" . kill-line)
       ("i i" . exchange-point-and-mark)
       ("i m" . xah-pop-local-mark-ring)
       ("i j" . xah-show-kill-ring)
       ("i d" . xah-delete-current-text-block)
       ("i h" . xah-select-block)
       ("i t" . xah-select-text-in-quote)
       ("i s" . xah-select-line)

       ("j" . xah-copy-all-or-region)

       ("k b" . xah-upcase-sentence)
       ("k c" . enlarge-window)
       ("k h" . delete-other-windows)
       ("k m" . split-window-below)
       ("k n" . balance-windows)
       ("k s" . ispell-word)
       ("k t" . delete-window)
       ("k w" . split-window-right)

       ("l" . recenter-top-bottom)
       ("m" . dired-jump)

       ;; commands here are harmless. they don't modify text etc. they turn on modes, change display, prompt, start shell, etc.

       ("n SPC SPC" . whitespace-mode)

       ("n SPC t" . visual-line-mode)
       ("n SPC h" . toggle-word-wrap)
       ("n SPC n" . variable-pitch-mode)
       ("n SPC s" . toggle-truncate-lines)
       ("n SPC d" . display-line-numbers-mode)

       ("n SPC ," . global-hl-line-mode)

       ("n ," . abbrev-mode)
       ("n ." . toggle-frame-maximized) ; xxwindow
       ("n 1" . set-input-method)
       ("n 6" . calendar)
       ("n 7" . calc)
       ("n 9" . nil)
       ("n 0" . nil)

       ("n <up>" . xah-page-up)
       ("n <down>" . xah-page-down)

       ("n a" . global-text-scale-adjust)
       ("n b" . toggle-debug-on-error)
       ("n c" . toggle-case-fold-search)
       ("n d" . nil)
       ("n e" . eshell)
       ("n f" . shell-command-on-region)
       ("n g" . shell-command)
       ("n h" . widen)
       ("n i" . make-frame-command)
       ("n j" . flyspell-buffer)
       ("n k" . menu-bar-open)
       ("n l" . nil)
       ("n m" . jump-to-register) ; bad orig
       ("n n" . xah-narrow-to-region)
       ("n o" . nil)
       ("n p" . read-only-mode)
       ("n q" . nil)
       ("n r" . count-words)
       ("n s" . count-matches)
       ("n t" . narrow-to-defun)
       ("n u" . shell)
       ("n v" . nil)
       ("n w" . eww) ; xxmode
       ("n x" . save-some-buffers)
       ("n y" . nil)
       ("n z" . abort-recursive-edit)

       ("o" . query-replace)
       ("p" . query-replace)

       ("q" . xah-cut-all-or-region)

       ;; roughly text replacement related

       ("r ," . apply-macro-to-region-lines)
       ("r ." . kmacro-start-macro)

       ("r 3" . number-to-register)
       ("r 4" . increment-register)
       ("r RET" . nil)
       ("r SPC" . nil)

       ("r a" . nil)
       ("r b" . nil)
       ("r c" . string-rectangle)
       ("r d" . delete-rectangle)
       ("r e" . call-last-kbd-macro)
       ("r f" . nil)
       ("r g" . nil)
       ("r h" . xah-change-bracket-pairs)
       ("r i" . xah-space-to-newline)
       ("r j" . copy-rectangle-to-register)
       ("r k" . yank-rectangle)
       ("r l" . clear-rectangle)

       ("r m e" . xah-append-to-register-1)
       ("r m j" . xah-copy-to-register-1)
       ("r m k" . xah-paste-from-register-1)
       ("r m u" . xah-clear-register-1)

       ("r n" . rectangle-number-lines)
       ("r o" . open-rectangle)
       ("r p" . kmacro-end-macro)
       ("r q" . kill-rectangle)

       ("r r" . rectangle-mark-mode)

       ("r s e" . xah-slash-to-backslash)
       ("r s h" . xah-slash-to-double-backslash)
       ("r s m" . xah-double-backslash-to-slash)
       ("r s s" . xah-double-backslash-to-single)
       ("r s t" . xah-double-backslash)
       ("r s u" . xah-backslash-to-slash)

       ;; kmacro-end-and-call-macro
       ("r t RET" . kmacro-edit-macro)
       ("r t SPC" . kmacro-step-edit-macro)
       ("r t TAB" . kmacro-insert-counter)
       ;; kmacro-start-macro-or-insert-counter

       ("r t a" . kmacro-add-counter)
       ("r t b" . kmacro-bind-to-key)
       ("r t c" . kmacro-set-counter)
       ("r t d" . kmacro-redisplay)
       ("r t e" . edit-kbd-macro)
       ("r t f" . kmacro-set-format)
       ("r t g" . kmacro-delete-ring-head)
       ("r t h" . kmacro-edit-macro-repeat)
       ("r t i" . kmacro-call-ring-2nd-repeat)
       ("r t j" . kmacro-cycle-ring-next)
       ("r t k" . kmacro-end-or-call-macro-repeat)
       ("r t l" . kmacro-edit-lossage)

       ("r t n" . kmacro-name-last-macro)
       ("r t p" . kmacro-cycle-ring-previous)
       ("r t q" . kbd-macro-query)
       ("r t t" . kmacro-swap-ring)
       ("r t u" . nil)

       ("r t v" . kmacro-view-macro-repeat)
       ("r t x" . kmacro-to-register)
       ("r u" . xah-quote-lines)
       ("r v" . nil)
       ("r w" . nil)
       ("r x" . nil)
       ("r y" . delete-whitespace-rectangle)
       ("r z" . nil)

       ("s" . save-buffer)

       ;; most frequently used
       ("t <up>"  . xah-move-block-up)
       ("t <down>"  . xah-move-block-down)

       ("t ," . sort-numeric-fields)
       ("t ." . xah-sort-lines)
       ("t a" . nil)
       ("t b" . xah-backward-punct)
       ("t c" . copy-matching-lines)
       ("t d" . mark-defun)
       ("t e" . list-matching-lines)
       ("t f" . move-to-column)
       ("t g" . xah-goto-line)
       ("t h" . repeat-complex-command)
       ("t i" . delete-non-matching-lines)
       ("t j" . copy-to-register)
       ("t k" . insert-register)
       ("t l" . xah-cycle-hyphen-lowline-space)
       ("t m" . xah-make-backup-and-save)
       ("t n" . xah-goto-char)
       ("t o" . xah-clean-whitespace)
       ("t p" . query-replace-regexp)
       ("t q" . xah-cut-text-in-quote)
       ("t r" . xah-escape-quotes)
       ("t s" . xah-reformat-to-sentence-lines)
       ("t t" . repeat)
       ("t u" . kill-matching-lines)
       ("t v" . nil)
       ("t w" . xah-next-window-or-frame)
       ("t x" . xah-title-case-region-or-line)
       ("t y" . delete-duplicate-lines)
       ("t z" . nil)
       ("u" . switch-to-buffer)
       ("v" . universal-argument)

       ;; dangerous map. run program, delete file, etc
       ("w d" . xah-delete-current-file-make-backup)
       ("w ." . eval-buffer)
       ("w e" . eval-defun)
       ("w m" . eval-last-sexp)
       ("w p" . eval-expression)
       ("w u" . eval-region)
       ("w q" . save-buffers-kill-terminal)
       ("w w" . delete-frame)
       ("w j" . xah-run-current-file)

       ("x" . xah-toggle-previous-letter-case)

       ("y" . nil)

       ;; vc command keys subject to change. need a frequency stat of the commands.

       ("z b" . vc-root-diff)      ; D
       ("z c" . vc-update)         ; git pull, +
       ("z d" . vc-annotate)       ; g
       ("z f" . vc-revert)         ; u
       ("z g" . vc-push)           ; git push, P
       ("z h" . vc-diff)           ; git diff, =
       ("z l" . vc-print-root-log) ; L
       ("z m" . vc-dir)            ; git status, C-x v d
       ("z n" . vc-print-log)      ; git log, l
       ("z r" . vc-merge)          ; m
       ("z t" . vc-register)       ; git add, i
       ("z z" . vc-next-action)    ; v

       ("z 1" . vc-create-tag)            ; s
       ("z 2" . vc-insert-headers)        ; h
       ("z 4" . vc-retrieve-tag)          ; r
       ("z 5" . vc-revision-other-window) ; ~
       ("z 6" . vc-switch-backend)        ; b
       ("z 7" . vc-update-change-log)     ; a

       ;;
       ))

    (xah-fly--define-keys
     xah-fly-command-map
     '(("SPC" . xah-fly-leader-key-map)
       ("'" . xah-reformat-lines)
       ("," . xah-shrink-whitespaces)
       ("-" . delete-other-windows)
       ("." . backward-kill-word)
       ("/" . hippie-expand)
       (";" . xah-comment-dwim)
       ("[" . split-window-below)
       ("\\" . xah-cycle-hyphen-lowline-space)
       ("]" . split-window-right)
       ("`" . other-frame)

       ("a" . execute-extended-command)
       ("b" . isearch-forward)
       ("c" . previous-line)
       ("d" . xah-beginning-of-line-or-block)
       ("e" . xah-smart-delete)
       ("E" . delete-backward-char)
       ("f" . undo)
       ("g" . backward-word)
       ("h" . backward-char)
       ("i" . xah-extend-selection)

       ("j" . xah-copy-line-or-region)
       ("k" . xah-paste-or-paste-previous)
       ("l" . xah-insert-space-before)
       ("m" . xah-backward-left-bracket)
       ("n" . forward-char)
       ("o" . open-line)
       ("p" . kill-word)
       ("q" . xah-cut-line-or-region)
       ("r" . forward-word)
       ("s" . xah-end-of-line-or-block)
       ("t" . next-line)
       ("u" . xah-fly-insert-mode-activate)
       ("v" . xah-forward-right-bracket)
       ("w" . xah-next-window-or-frame)
       ("x" . xah-toggle-letter-case)
       ("y" . set-mark-command)
       ("z" . xah-goto-matching-bracket)))

    ;;
    ))

(xah-fly-define-keys)

;; s------------------------------
;; set control meta, etc keys

(defcustom xah-fly-unset-useless-key t
  "If true, unbind many obsolete or useless or redundant
  keybinding. e.g. <help>, <f1>."
  :type 'boolean)

(when xah-fly-unset-useless-key
  (global-set-key (kbd "<help>") nil)
  (global-set-key (kbd "<f1>") nil))

  (global-set-key (kbd "M-SPC") #'xah-fly-command-mode-activate)

(when xah-fly-use-meta-key

  (global-set-key (kbd "M-<home>") nil) ; beginning-of-buffer-other-window
  (global-set-key (kbd "M-<end>") nil) ; end-of-buffer-other-window

  (global-set-key (kbd "M-\\") nil) ; delete-horizontal-space
  (global-set-key (kbd "M-!") nil)  ; shell-command
  (global-set-key (kbd "M-$") nil)  ; ispell-word
  (global-set-key (kbd "M-%") nil)  ; query-replace
  (global-set-key (kbd "M-&") nil)  ; async-shell-command
  (global-set-key (kbd "M-'") nil)  ; abbrev-prefix-mark
  (global-set-key (kbd "M-(") nil)  ; insert-parentheses
  (global-set-key (kbd "M-)") nil)  ; move-past-close-and-reindent
  ;; (global-set-key (kbd "M-,") nil) ; xref-pop-marker-stack
  ;; (global-set-key (kbd "M-.") nil) ; xref-find-definitions
  (global-set-key (kbd "M-/") nil) ; dabbrev-expand
  (global-set-key (kbd "M-:") nil) ; eval-expression
  ;; (global-set-key (kbd "M-;") nil) ; comment-dwim
  (global-set-key (kbd "M-<") nil) ; beginning-of-buffer
  (global-set-key (kbd "M-=") nil) ; count-words-region
  (global-set-key (kbd "M->") nil) ; end-of-buffer
  ;; (global-set-key (kbd "M-?") nil) ; xref-find-references
  (global-set-key (kbd "M-@") nil) ; mark-word
  (global-set-key (kbd "M-^") nil) ; delete-indentation
  (global-set-key (kbd "M-`") nil) ; tmm-menubar
  (global-set-key (kbd "M-a") nil) ; backward-sentence
  (global-set-key (kbd "M-b") nil) ; backward-word
  (global-set-key (kbd "M-c") nil) ; capitalize-word
  (global-set-key (kbd "M-d") nil) ;  kill-word
  (global-set-key (kbd "M-e") nil) ; forward-sentence
  (global-set-key (kbd "M-f") nil) ; forward-word
  (global-set-key (kbd "M-g") nil) ; Prefix Command
  (global-set-key (kbd "M-h") nil) ; mark-paragraph
  (global-set-key (kbd "M-i") nil) ; tab-to-tab-stop
  (global-set-key (kbd "M-j") nil) ; default-indent-new-line
  (global-set-key (kbd "M-k") nil) ; kill-sentence
  (global-set-key (kbd "M-l") nil) ; downcase-word
  (global-set-key (kbd "M-m") nil) ; back-to-indentation
  (global-set-key (kbd "M-o") nil) ; facemenu-keymap
  (global-set-key (kbd "M-q") nil) ; fill-paragraph
  (global-set-key (kbd "M-r") nil) ; move-to-window-line-top-bottom
  ;; (global-set-key (kbd "M-s") nil) ; Prefix Command
  (global-set-key (kbd "M-t") nil) ; transpose-words
  (global-set-key (kbd "M-u") nil) ; upcase-word
  (global-set-key (kbd "M-v") nil) ; scroll-down-command
  (global-set-key (kbd "M-w") nil) ; kill-ring-save
  ;; (global-set-key (kbd "M-x") nil) ; execute-extended-command
  ;; (global-set-key (kbd "M-y") nil) ; yank-pop
  (global-set-key (kbd "M-z") nil)   ; zap-to-char
  (global-set-key (kbd "M-{") nil)   ; backward-paragraph
  (global-set-key (kbd "M-|") nil)   ; shell-command-on-region
  (global-set-key (kbd "M-}") nil)   ; forward-paragraph
  (global-set-key (kbd "M-~") nil)   ; not-modified
  (global-set-key (kbd "M-DEL") nil) ; backward-kill-word

  ;;
  )

(when xah-fly-remove-control-x-keys (global-set-key (kbd "C-x") nil))

(when xah-fly-use-control-key

  (global-set-key (kbd "C-<tab>") #'xah-next-user-buffer)
  (global-set-key (kbd "C-S-<tab>") #'xah-previous-user-buffer)

  (global-set-key (kbd "C-<prior>") #'xah-previous-user-buffer)
  (global-set-key (kbd "C-<next>") #'xah-next-user-buffer)

  (global-set-key (kbd "C-2") #'pop-global-mark)
  (global-set-key (kbd "C-3") #'previous-error)
  (global-set-key (kbd "C-4") #'next-error)

  (global-set-key (kbd "C-7") #'xah-previous-user-buffer)
  (global-set-key (kbd "C-8") #'xah-next-user-buffer)
  (global-set-key (kbd "C-9") #'xah-page-up)
  (global-set-key (kbd "C-0") #'xah-page-down)

  (global-set-key (kbd "C--") #'text-scale-decrease)
  (global-set-key (kbd "C-=") #'text-scale-increase)

  (global-set-key (kbd "C-SPC") #'xah-fly-command-mode-activate)

  (global-set-key (kbd "C-S-n") #'make-frame-command)
  (global-set-key (kbd "C-S-s") #'write-file)
  (global-set-key (kbd "C-S-t") #'xah-open-last-closed)

  (global-set-key (kbd "C-a") #'mark-whole-buffer)

  (global-set-key (kbd "C-n") #'xah-new-empty-buffer)
  (global-set-key (kbd "C-o") #'find-file)

  (global-set-key (kbd "C-s") #'save-buffer)
  (global-set-key (kbd "C-t") #'hippie-expand)
  (global-set-key (kbd "C-v") #'yank)
  (global-set-key (kbd "C-w") #'xah-close-current-buffer)
  (global-set-key (kbd "C-y") #'undo-redo)
  (global-set-key (kbd "C-z") #'undo)
  ;;
  )

  (global-set-key (kbd "<f7>") 'xah-fly-leader-key-map)

;; s------------------------------

(when (< emacs-major-version 28)
  (defalias 'execute-extended-command-for-buffer #'execute-extended-command))

;; s------------------------------
;;;; misc

;; the following have keys in gnu emacs, but i decided not to give them a key, because either they are rarely used (say, 95% of emacs users use them less than once a month ), or there is a more efficient command/workflow with key in xah-fly-keys

;; C-x $   →   set-selective-display
;; C-x *   →   calc-dispatch
;; C-x -   →   shrink-window-if-larger-than-buffer
;; C-x .   →   set-fill-prefix
;; C-x 4 .   →   find-tag-other-window
;; C-x 4 0   →   kill-buffer-and-window
;; C-x 4 C-o   →   display-buffer
;; C-x 4 a   →   add-change-log-entry-other-window
;; C-x 4 b   →   switch-to-buffer-other-window
;; C-x 4 c   →   clone-indirect-buffer-other-window
;; C-x 4 d   →   dired-other-window
;; C-x 4 f   →   find-file-other-window
;; C-x 4 m   →   compose-mail-other-window
;; C-x 4 r   →   find-file-read-only-other-window
;; C-x 5 .		xref-find-definitions-other-frame
;; C-x 5 1		delete-other-frames
;; C-x 5 5		other-frame-prefix
;; C-x 5 C-o	display-buffer-other-frame
;; C-x 5 b		switch-to-buffer-other-frame
;; C-x 5 c		clone-frame
;; C-x 5 d		dired-other-frame
;; C-x 5 f		find-file-other-frame
;; C-x 5 m		compose-mail-other-frame
;; C-x 5 p		project-other-frame-command
;; C-x 5 r		find-file-read-only-other-frame
;; C-x 5 u		undelete-frame
;; C-x 6 2   →   2C-two-columns
;; C-x 6 b   →   2C-associate-buffer
;; C-x 6 s   →   2C-split
;; C-x ;   →   comment-set-column
;; C-x <   →   scroll-left
;; C-x =   →   what-cursor-position, use describe-char instead
;; C-x >   →   scroll-right
;; C-x C-d   →   list-directory
;; C-x C-l   →   downcase-region
;; C-x C-n   →   set-goal-column
;; C-x C-o   →   delete-blank-lines
;; C-x C-p   →   mark-page
;; C-x C-r   →   find-file-read-only
;; C-x C-t   →   transpose-lines
;; C-x C-u   →   upcase-region
;; C-x C-v   →   find-alternate-file
;; C-x C-z   →   suspend-frame
;; C-x DEL   →   backward-kill-sentence
;; C-x ESC   →   Prefix Command
;; C-x [   →   backward-page
;; C-x ]   →   forward-page
;; C-x f   →   set-fill-column
;; C-x i   →   insert-file
;; C-x k   →   kill-buffer
;; C-x l   →   count-lines-page
;; C-x m   →   compose-mail
;; C-x n g		goto-line-relative
;; C-x n p		narrow-to-page
;; C-x r M		bookmark-set-no-overwrite
;; C-x r M-w	copy-rectangle-as-kill
;; C-x r SPC	point-to-register
;; C-x r f		frameset-to-register
;; C-x r j		jump-to-register
;; C-x r w		window-configuration-to-register
;; C-x {   →   shrink-window-horizontally
;; C-x }   →   enlarge-window-horizontally

;; s------------------------------

(defvar xah-fly-insert-state-p t "non-nil means insertion mode is on.")

(defun xah-fly--update-key-map ()
  (setq xah-fly-key-map (if xah-fly-insert-state-p
                            xah-fly-insert-map
                          xah-fly-command-map)))

(defun xah-fly-keys-set-layout (Layout)
  "Set a keyboard layout.
Argument must be one of the key name in `xah-fly-layout-diagrams'
Created: 2021-05-19
Version: 2024-05-23"
  (interactive
   (list
    (completing-read
     "Choose a layout: "
     (seq-sort 'string< (hash-table-keys xah-fly-layout-diagrams))
     nil t)))
  (let ((xold xah-fly-key-current-layout)
        (xkeydiagram (gethash Layout xah-fly-layout-diagrams)))
    (when (not xkeydiagram)
      (user-error
       "xah-fly-keys-set-layout error: layout must be one of string:\n%s"
       (mapconcat 'identity (hash-table-keys xah-fly-layout-diagrams) "\n")))
    (setq xah-fly-key-current-layout Layout)
    (setq
     xah-fly--key-convert-table
     (xah-fly-create-key-conv-table
      (gethash "dvorak" xah-fly-layout-diagrams)
      xkeydiagram))
    (when (not (equal xold Layout)) (xah-fly-define-keys))))

(defun xah-fly-space-key ()
  "Switch to command mode if the char before cursor is a space.
experimental
Version: 2018-05-07"
  (interactive)
  (if (eq (char-before ) 32)
      (xah-fly-command-mode-activate)
    (insert " ")))

(defun xah-fly-command-mode-init ()
  "Set command mode keys.
Version: 2022-07-06"
  (interactive)
  (setq xah-fly-insert-state-p nil)
  (xah-fly--update-key-map)
  (when xah-fly--deactivate-command-mode-func
    (funcall xah-fly--deactivate-command-mode-func))
  (setq xah-fly--deactivate-command-mode-func
        (set-transient-map xah-fly-command-map (lambda () t)))
  (modify-all-frames-parameters (list (cons 'cursor-type 'box)))
  (if xah-fly-command-mode-cursor-color
      (set-face-background 'cursor xah-fly-command-mode-cursor-color))
  (force-mode-line-update))

(defun xah-fly-insert-mode-init (&optional no-indication)
  "Enter insertion mode."
  (interactive)
  (setq xah-fly-insert-state-p t)
  (xah-fly--update-key-map)
  (funcall xah-fly--deactivate-command-mode-func)
  (unless no-indication
    (modify-all-frames-parameters '((cursor-type . bar)))
    (if xah-fly-insert-mode-cursor-color
        (set-face-background 'cursor xah-fly-insert-mode-cursor-color)))
  (force-mode-line-update))

(defun xah-fly-mode-toggle ()
  "Switch between {insertion, command} modes."
  (interactive)
  (if xah-fly-insert-state-p
      (xah-fly-command-mode-activate)
    (xah-fly-insert-mode-activate)))

(defun xah-fly-save-buffer-if-file ()
  "Save current buffer if it is a file."
  (interactive)
  (when buffer-file-name
    (save-buffer)))

(defun xah-fly-command-mode-activate ()
  "Activate command mode and run `xah-fly-command-mode-activate-hook'
Version: 2017-07-07"
  (interactive)
  (xah-fly-command-mode-init)
  (when xah-fly-command-mode-hl-line (progn (global-hl-line-mode 1)))
  (run-hooks 'xah-fly-command-mode-activate-hook))

(defun xah-fly-command-mode-activate-no-hook ()
  "Activate command mode. Does not run `xah-fly-command-mode-activate-hook'
Version: 2017-07-07"
  (interactive)
  (xah-fly-command-mode-init))

(defun xah-fly-insert-mode-activate ()
  "Activate insertion mode.
Version: 2017-07-07"
  (interactive)
  (xah-fly-insert-mode-init)
  (when xah-fly-command-mode-hl-line (progn (global-hl-line-mode 0)))
  (run-hooks 'xah-fly-insert-mode-activate-hook))

(defun xah-fly-insert-mode-activate-newline ()
  "Activate insertion mode, insert newline below."
  (interactive)
  (xah-fly-insert-mode-activate)
  (open-line 1))

;; s------------------------------

;;;###autoload
(define-minor-mode xah-fly-keys
  "A modal keybinding set, like vim, but based on ergonomic principles, like Dvorak layout.

URL `http://xahlee.info/emacs/misc/xah-fly-keys.html'"
  :global t
  :lighter "XFK"
  :keymap xah-fly-insert-map
  (delete-selection-mode 1)
  (setq shift-select-mode nil)
  (if xah-fly-keys
      (progn
        (when (fboundp 'which-key-mode) (which-key-mode 1))
        (add-hook 'minibuffer-setup-hook 'xah-fly-insert-mode-activate)
        (add-hook 'minibuffer-exit-hook 'xah-fly-command-mode-activate)
        (add-hook 'isearch-mode-end-hook 'xah-fly-command-mode-activate)
        (xah-fly-command-mode-activate))
    (progn
      (when (fboundp 'which-key-mode) (which-key-mode 0))
      (remove-hook 'minibuffer-setup-hook 'xah-fly-insert-mode-activate)
      (remove-hook 'minibuffer-exit-hook 'xah-fly-command-mode-activate)
      (remove-hook 'isearch-mode-end-hook 'xah-fly-command-mode-activate)
      (xah-fly-insert-mode-init :no-indication))))

(provide 'xah-fly-keys)

;;; xah-fly-keys.el ends here
