#+TITLE: arf

* Preamble
#+begin_src emacs-lisp
;;; arf.el --- arf -*- lexical-binding: t; -*-
#+end_src

* Load
#+begin_src emacs-lisp
(require 'arf)
(arf-mode 1)
#+end_src

* State Definitions
#+begin_src emacs-lisp
(setq arf-state-definitions
      '((nav   :tag " NAV " :face arf-nav-face   :suppress-self-insert t)
        (write :tag " WRITE " :face arf-write-face  :suppress-self-insert nil)
	(region :tag " REGION " :face arf-region-face :suppress-self-insert t)))

(defun arf-nav-state () (interactive) (arf-enter-state 'nav))
(defun arf-write-state () (interactive) (arf-enter-state 'write))
(defun arf-region-state () (interactive) (arf-enter-state 'region))

(defface arf-nav-face
  '((t :foreground "black" :background "white" :weight bold))
  "Face for navigation state in mode line.")

(defface arf-write-face
  '((t :foreground "black" :background "green" :weight bold))
  "Face for write state in mode line.")

(defface arf-region-face
  '((t :foreground "white" :background "red"))
  "Face for region state in mode line.")

(setq arf-default-state 'nav)
#+end_src

* Helper Functions
#+begin_src emacs-lisp
(defun arf-forward-char ()
  "Move forward one character, stopping at end of line."
  (interactive)
  (when (not (eolp)) (forward-char)))

(defun arf-backward-char ()
  "Move backward one character, stopping at beginning of line."
  (interactive)
  (when (not (bolp)) (backward-char)))

(defun arf-insert-line-after ()
  (interactive)
  (end-of-line)
  (newline)
  (arf-write-state))

(defun arf-start-region ()
  (interactive)
  (set-mark-command nil)
  (arf-region-state))

(defun arf-cancel-region ()
  (interactive)
  (deactivate-mark)
  (arf-nav-state))

(defun arf-start-region-line()
  (interactive)
  (beginning-of-line)
  (set-mark-command nil)
  (forward-line 1)
  (arf-region-state))

(defun arf-region-write ()
  (interactive)
  (kill-region nil nil t)
  (arf-write-state))

(defun arf-region-yank ()
  (interactive)
  (kill-ring-save nil nil t)
  (arf-nav-state))

(defun arf-region-delete ()
  (interactive)
  (kill-region nil nil t)
  (arf-nav-state))

(defun arf-region-send-to-agent ()
  (interactive)
  (circ/agent-chat-helper-send-region)
  (arf-nav-state))

(defun arf-region-send-to-agent-with-header ()
  (interactive)
  (circ/agent-chat-helper-send-region-with-header)
  (arf-nav-state))

(defun arf-region-send-to-agent-with-absolute-header ()
  (interactive)
  (circ/agent-chat-helper-send-region-with-absolute-header)
  (arf-nav-state))

(defun arf-find-char-forward ()
  "Jump forward to the next occurrence of a character on the current line."
  (interactive)
  (let ((char (read-char "Find forward: ")))
    (forward-char)
    (search-forward (char-to-string char) (line-end-position) t)
    (backward-char)))

(defun arf-find-char-backward ()
  "Jump backward to the previous occurrence of a character on the current line."
  (interactive)
  (let ((char (read-char "Find backward: ")))
    (search-backward (char-to-string char) (line-beginning-position) t)))

(defun circ/live-replace-regexp ()
  (interactive)
  (call-interactively 'live-replace-regexp)
  (arf-nav-state))
#+end_src

* Keybinding Display Refresh
#+begin_src emacs-lisp
(defun arf--on-window-buffer-change (_frame)
  "Refresh the keybindings buffer when switching windows."
  (arf--refresh-keybindings))

(add-hook 'window-buffer-change-functions #'arf--on-window-buffer-change)
(add-hook 'window-selection-change-functions #'arf--on-window-buffer-change)
#+end_src

* Default Bindings
#+begin_src emacs-lisp
(setq arf-movement-keys
  '(("i" previous-line)
    ("k" next-line)
    ("j" arf-backward-char)
    ("l" arf-forward-char)
    ("J" beginning-of-line)
    ("L" end-of-line)
    ("u" backward-word)
    ("o" forward-word)
    ("U" backward-paragraph)
    ("O" forward-paragraph)
    ("I" circ/scroll-half-page-up)
    ("K" circ/scroll-half-page-down)))

(arf-set-bindings 'nav nil
  `(("M-;" ignore)
    ("RET" ignore)
    ("DEL" ignore)
    ("<end>" ignore)
    ("<home>" ignore)
    ,@arf-movement-keys
    ("r" arf-start-region)
    ("R" arf-start-region-line)
    ("w"
     (("w" arf-write-state)
      ("k" arf-insert-line-after)
      ("r" live-replace-regexp)))
    ("Y" yank)
    ("z" undo)
    ("Z" undo-redo)
    ("f" arf-find-char-forward)
    ("F" arf-find-char-backward)
    ("m"
     (("s" kmacro-start-macro)
      ("e" kmacro-end-macro)
      ("E" kmacro-edit-macro)
      ("m" kmacro-call-macro)
      ("n" kmacro-name-last-macro)))
    ))

(arf-set-bindings 'write nil
  '(("M-;" arf-nav-state)
    ("<end>" ignore)
    ("<home>" ignore)))

(arf-set-bindings 'region nil
  `(("M-;" arf-cancel-region)
    ("q" arf-cancel-region)
    ("TAB" exchange-point-and-mark)
    ,@arf-movement-keys
    ("w"
     (("w" arf-region-write)
      ("r" circ/live-replace-regexp)))
    ("y" arf-region-yank)
    ("d" arf-region-delete)
    ("SPC"
     (("s" arf-region-send-to-agent-with-header)
      ("S" arf-region-send-to-agent-with-absolute-header)
      ("y" arf-region-send-to-agent)))
    ))
#+end_src

* Mode-Specific Bindings
** dired
#+begin_src emacs-lisp
(defun circ/dired-goto-first-line ()
  (interactive)
  (goto-char (point-min))
  (dired-next-line 1))

(defun circ/dired-goto-last-line ()
  (interactive)
  (goto-char (point-max))
  (dired-previous-line 1))

(defun circ/dired-make-directory ()
  (interactive)
  (call-interactively 'make-directory)
  (revert-buffer))

(defun circ/dired-toggle-mark ()
  "Toggle mark on the current dired entry."
  (interactive)
  (if (eq (dired-file-marker (dired-get-filename nil t)) ?*)
      (dired-unmark 1)
    (dired-mark 1)))

(defun circ/dired-toggle-mark-all ()
  "If current entry is marked, unmark all. Otherwise, mark all."
  (interactive)
  (if (eq (dired-file-marker (dired-get-filename nil t)) ?*)
      (dired-unmark-all-marks)
    (dired-mark-files-regexp "")))

(arf-set-bindings 'nav 'dired-mode
  '(("q" quit-window)
    ("Q" kill-this-buffer)
    ("r" revert-buffer)
    ("RET" dired-find-file)
    ("j" dired-up-directory)
    ("l" dired-find-file)
    ("k" dired-next-line)
    ("i" dired-previous-line)
    ("d" ignore)
    ("e" wdired-change-to-wdired-mode)
    ("r" revert-buffer)
    ("TAB" circ/dired-toggle-mark)
    ("<backtab>" circ/dired-toggle-mark-all)
    ("c"
     (("d" circ/dired-make-directory)))
    ("x"
     (("d" dired-do-delete)
      ("c" dired-do-copy)
      ("m" dired-do-rename)
      (nil dired-do-compress)
      (nil dired-do-symlink)
      (nil dired-do-chmod)))))
#+end_src

** wdired
#+begin_src emacs-lisp
(arf-set-bindings 'nav 'wdired-mode
  '(("q" wdired-finish-edit)
    ("Q" wdired-abort-changes)))
#+end_src

** info
#+begin_src emacs-lisp
(arf-set-bindings 'nav 'Info-mode
  '(("m" Info-menu)
    ("]" Info-next)
    ("[" Info-prev)
    ("DEL" Info-up)
    ("g"
     (("b" Info-history-back)
      ("f" Info-history-forward)))
    ("TAB" Info-next-reference)
    ("S-TAB" Info-prev-reference)
    ("RET" Info-follow-nearest-node)))
#+end_src

** occur
#+begin_src emacs-lisp
(arf-set-bindings 'nav 'occur-mode
  '(("RET" occur-mode-goto-occurrence)
    ("r" revert-buffer)
    ("q" kill-current-buffer)))
#+end_src

** org
#+begin_src emacs-lisp
(arf-set-bindings 'nav 'org-mode
  '(("TAB" org-cycle)
    ("SPC"
     (("t" org-todo)
      ("T" org-todo-yesterday)
      ("p" org-set-property)
      ("P" circ/org-toggle-pinned-property)
      ("s" org-schedule)
      ("d" org-deadline)
      ("q" circ/org-set-tags)
      ("R" org-refile)
      ("A" org-archive-subtree)
      ("m"
       (("r" circ/org-file-reference)))))))
	
;; org-capture-mode is a minor mode, so arf-mode-default-states won't work
(add-hook 'org-capture-mode-hook (lambda () (arf-enter-state 'write)))
#+end_src

** org-agenda
#+begin_src emacs-lisp
(arf-set-bindings 'nav 'org-agenda-mode
  '(("RET" org-agenda-goto)
    ("I" org-agenda-previous-item)
    ("K" org-agenda-next-item)
    ("i" org-agenda-previous-line)
    ("k" org-agenda-next-line)
    ("h"
     (("b" circ/org-agenda-toggle-blocked-tasks)
      ("t" circ/org-agenda-toggle-hide-tags)
      ("c" circ/org-agenda-toggle-clock-closed)
      ("l" org-agenda-log-mode)))
    ("t" org-agenda-todo)
    ("T" org-agenda-todo-yesterday)
    ("p" org-agenda-set-property)
    ("P" circ/org-agenda-toggle-pinned-property)
    ("s" org-agenda-schedule)
    ("d" org-agenda-deadline)
    ("A" org-agenda-archive)
    ("r" org-agenda-redo)
    ("R" org-agenda-refile)
    ("f"
     (("t" circ/org-agenda-filter-tag)
      ("T" circ/org-agenda-show-all-tag)
      ("e" circ/org-agenda-filter-effort)
      ("E" circ/org-agenda-show-all-efforts)
      ("c" circ/org-agenda-filter-category)
      ("C" circ/org-agenda-show-all-categories)))))
#+end_src

** vc-dir
#+begin_src emacs-lisp
(defun circ/vc-dir-toggle-mark ()
  "Toggle mark on the current vc-dir entry."
  (interactive)
  (let ((file (vc-dir-current-file)))
    (if (member file (vc-dir-marked-files))
        (vc-dir-unmark)
      (vc-dir-mark))))

(defun circ/vc-dir-toggle-mark-all ()
  "If current entry is marked, unmark all. Otherwise, mark all."
  (interactive)
  (let ((file (vc-dir-current-file)))
    (if (member file (vc-dir-marked-files))
        (vc-dir-unmark-all-files nil)
      (vc-dir-mark-all-files nil))))

(arf-set-bindings 'nav 'vc-dir-mode
  '(("q" quit-window)
    ("i" vc-dir-previous-line)
    ("k" vc-dir-next-line)
    ("r" revert-buffer)
    ("TAB" circ/vc-dir-toggle-mark)
    ("<backtab>" circ/vc-dir-toggle-mark-all)
    (nil vc-register)
    ("x" vc-next-action)
    ("=" vc-diff)
    ("l" vc-print-log)
    ("h" vc-dir-hide-up-to-date)))
#+end_src

** ibuffer
#+begin_src emacs-lisp
(defun circ/ibuffer-toggle-mark ()
  "Toggle mark on the current ibuffer entry."
  (interactive)
  (let ((props (get-text-property (point) 'ibuffer-properties)))
    (if (and props (eq (cadr props) ibuffer-marked-char))
        (ibuffer-unmark-forward nil nil 1)
      (ibuffer-mark-forward nil nil 1))))

(defun circ/ibuffer-toggle-mark-all ()
  "If current entry is marked, unmark all.
Otherwise, mark all buffers with the same major mode as the buffer at point."
  (interactive)
  (let ((props (get-text-property (point) 'ibuffer-properties)))
    (if (and props (eq (cadr props) ibuffer-marked-char))
        (ibuffer-unmark-all-marks)
      (let* ((buf (ibuffer-current-buffer))
             (mode (and buf (buffer-live-p buf)
                        (buffer-local-value 'major-mode buf))))
        (when mode
          (ibuffer-mark-on-buffer
           (lambda (b) (eq (buffer-local-value 'major-mode b) mode))))))))

(arf-set-bindings 'nav 'ibuffer-mode
  '(("q" quit-window)
    ("Q" kill-this-buffer)
    ("RET" ibuffer-visit-buffer)
    ("k" ibuffer-forward-line)
    ("i" ibuffer-backward-line)
    ("r" ibuffer-update)
    ("TAB" circ/ibuffer-toggle-mark)
    ("<backtab>" circ/ibuffer-toggle-mark-all)
    ("x"
     (("d" ibuffer-do-delete)
      ("s" ibuffer-do-save)
      ("r" ibuffer-do-revert)))))
#+end_src

** diff
#+begin_src emacs-lisp
(arf-set-bindings 'nav 'diff-mode
  '(("q" quit-window)))
#+end_src

** log-edit
#+begin_src emacs-lisp
(add-to-list 'arf-mode-default-states '(vc-git-log-edit-mode . write))

(defun circ/log-edit-header ()
  "Show available commands in the header line."
  (setq-local header-line-format
              "C-c C-c commit  C-c C-k abort  = show diff"))

(add-hook 'log-edit-mode-hook #'circ/log-edit-header)

(arf-set-bindings 'nav 'log-edit-mode
  '(("=" log-edit-show-diff)))
#+end_src

** vterm
#+begin_src emacs-lisp
(add-to-list 'arf-mode-default-states '(vterm-mode . write))

(arf-set-bindings 'nav 'vterm-mode
  '(("w"
     (("i" circ/vterm-input-buffer)
      ("q" vterm-send-escape)))))

(arf-set-bindings 'write 'vterm-mode
  '(("M-`"
     (("i" circ/vterm-input-buffer)
      ("q" vterm-send-escape)))))
#+end_src

** vterm-input-buffer
#+begin_src emacs-lisp
(add-to-list 'arf-mode-default-states '(circ/vterm-input-buffer-mode . write))
#+end_src

** xref
#+begin_src emacs-lisp
(arf-set-bindings 'nav 'xref--xref-buffer-mode
  '(("k" xref-next-line-no-show)
    ("i" xref-prev-line-no-show)
    ("RET" xref-goto-xref)
    ("r" xref-revert-buffer)
    ("q" quit-window)))
#+end_src
