#+TITLE: Configuration
#+STARTUP: content

* General
#+begin_src emacs-lisp
;;; configuration.el --- Main configuration -*- lexical-binding: t; -*-

;; Store backup files in a central directory
(setq backup-directory-alist '(("." . "~/.emacs-backups")))

;; Disable #file# auto-save files and save visited files directly instead
(setq auto-save-default nil)
(setq auto-save-visited-interval 5)
(auto-save-visited-mode 1)

;; Disable the terminal bell
(setq ring-bell-function 'ignore)

;; Disable the menu bar
(menu-bar-mode -1)

;; Enable fido-mode with vertical display
(fido-vertical-mode 1)

;; M-RET accepts minibuffer input verbatim (no forced completion)
(define-key icomplete-fido-mode-map (kbd "M-RET") 'icomplete-fido-exit)

;; Enable mouse support in terminal
(xterm-mouse-mode 1)

;; tmux with extended-keys-format csi-u encodes shifted keys as distinct
;; escape sequences (e.g. ESC[32;2u for Shift+Space). Emacs 29's xterm.el
;; handles many CSI u sequences but not these, so we decode them manually.
;; Without csi-u, terminals send the same byte for Space and Shift+Space,
;; so Emacs never sees the shift — and S-space has no default binding.
;; We map S-space to plain space so it behaves as expected.
;;
;; To diagnose other broken keys: press C-h k then the key combo. Emacs
;; will show the raw escape sequence. The CSI u format is ESC[keycode;mod u
;; where mod is: 2=Shift 3=Alt 5=Ctrl 6=C-S 7=C-M 8=C-M-S.
;; Then add: (define-key input-decode-map "\e[keycode;modu" [S-keyname])
(define-key input-decode-map "\e[32;2u" (kbd "SPC"))
(define-key input-decode-map "\e[127;2u" [S-backspace])

;; Shorter prompts
(setopt use-short-answers t)

;; Automatically revert buffers when files change on disk
(global-auto-revert-mode 1)

;; Copy to system clipboard via OSC 52 (works through tmux)
(setq interprogram-cut-function
      (lambda (text &rest _)
        (let ((b64 (base64-encode-string (encode-coding-string text 'utf-8) t)))
          (send-string-to-terminal (concat "\e]52;c;" b64 "\e\\")))))

;; Keep point in same position relative to window when scrolling
(setq scroll-preserve-screen-position 1)

;; Preserve case pattern of matched text during replacement
(setq case-replace t)
#+end_src

* Display

#+begin_src emacs-lisp
;; Use a space character for the vertical window border
(set-display-table-slot standard-display-table 'vertical-border ?\s)

;; Keep N lines visible above/below cursor when scrolling (like vim scrolloff)
(setq scroll-margin 4)
(setq scroll-conservatively 101)

;; Relative line numbers in programming buffers
(setq display-line-numbers-type 't)
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'org-mode-hook #'display-line-numbers-mode)
#+end_src

* Tab bar

#+begin_src emacs-lisp
(tab-bar-mode 1)
(setq tab-bar-close-button-show nil)
(setq tab-bar-new-button-show nil)
(setq tab-bar-separator " ")
(setq tab-bar-auto-width-max '(220 30))
(setq tab-bar-new-tab-choice "*scratch*")
(setq tab-bar-new-tab-to 'rightmost)
(setq tab-bar-tab-name-format-function
      (lambda (tab i)
        (propertize (format " %d: %s " i (alist-get 'name tab))
                    'face (if (eq (car tab) 'current-tab)
                              'tab-bar-tab
                            'tab-bar-tab-inactive))))
(set-face-attribute 'tab-bar nil
                    :background "gray20" :foreground "gray50"
                    :box nil)
(set-face-attribute 'tab-bar-tab nil
                    :background "orange" :foreground "black"
                    :box nil)
(set-face-attribute 'tab-bar-tab-inactive nil
                    :background "gray40" :foreground "white"
                    :box nil)
#+end_src

* Tab line

#+begin_src emacs-lisp
(global-tab-line-mode 1)
(setq tab-line-close-button-show nil)
(setq tab-line-new-button-show nil)
(setq tab-line-separator " ")
(setq tab-line-switch-cycling t)
(set-face-attribute 'tab-line nil
                    :background "gray20" :foreground "gray50"
                    :box nil :height 1.0)
(set-face-attribute 'tab-line-tab nil
                    :background "orange" :foreground "black"
                    :box nil)
(set-face-attribute 'tab-line-tab-inactive nil
                    :background "gray40" :foreground "white"
                    :box nil)
(set-face-attribute 'tab-line-tab-current nil
                    :background "orange" :foreground "black"
                    :box nil)
(set-face-attribute 'tab-line-tab-modified nil
                    :foreground "blue"
                    :box nil)
#+end_src

* Mode line

Custom mode line with colored indicators for buffer state:
- Modified buffer (file-backed): red highlight on the modified flag
- File changed on disk: blue =DISK= indicator
- A periodic timer refreshes the mode line so disk changes appear even when idle

#+begin_src emacs-lisp
(set-face-attribute 'mode-line nil ; Set active mode line colour
                    :foreground "white"
                    :background "blue")

(setq-default mode-line-format
              '((:eval (arf-mode-line-tag))
                " "
                (:eval (if (and buffer-file-name (buffer-modified-p))
                           (propertize (format-mode-line mode-line-modified)
                                       'face '(:background "red" :foreground "white"))
                         (format-mode-line mode-line-modified)))
                (:eval (when (and buffer-file-name
                                  (not (verify-visited-file-modtime)))
                         (propertize " DISK " 'face '(:background "blue" :foreground "white"))))
                mode-line-remote
                " "
                mode-line-buffer-identification
                "   "
                mode-line-position
                (vc-mode vc-mode)
                "  "
                mode-line-modes
                " "))

(when (bound-and-true-p circ/mode-line-timer)
  (cancel-timer circ/mode-line-timer))
(defvar circ/mode-line-timer
  (run-with-timer 5 5 #'force-mode-line-update t))
#+end_src

* Buffers

#+begin_src emacs-lisp
(defun circ/resize-window-to (width)
  "Return a function that resizes a window to WIDTH columns."
  (lambda (win)
    (let ((fixed (window-parameter win 'window-size-fixed)))
      (set-window-parameter win 'window-size-fixed nil)
      (window-resize win (- width (window-width win)) t)
      (set-window-parameter win 'window-size-fixed fixed))))

(setq circ/side-window-options
      `((side . right)
        (slot . 0)
        (window-width . 80)
        (body-function . ,(circ/resize-window-to 80))))

(setq display-buffer-alist
      `(("^\\*Help\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (window-width . 80)
         (body-function . ,(circ/resize-window-to 80)))
        ("^\\*vc-dir\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (slot . 0)
         (window-height . 0.2)
         (window-width . 70)
         (body-function . ,(circ/resize-window-to 70)))
        ("^\\*vc-log\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (slot . 0)
         (window-height . 0.5)
         (window-width . 70)
         (body-function . ,(circ/resize-window-to 70)))
        ("^\\*vc-diff\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (window-height . 0.8)
         (slot . 1)
         (window-width . 70)
         (body-function . ,(circ/resize-window-to 70)))
        ("^\\*vc-change-log\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (slot . 1)
         (window-height . 0.8)
         (window-width . 70)
         (body-function . ,(circ/resize-window-to 70)))
        ("^\\*log-edit-files\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (window-height . 0.5)
         (slot . 1)
         (window-width . 70)
         (body-function . ,(circ/resize-window-to 70)))
        ("^\\*Occur\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (window-width . 70)
         (body-function . ,(circ/resize-window-to 70)))
        ("^\\*Org Select\\*$"
         (display-buffer-in-side-window)
         (side . bottom)
         (window-height . 15))
        ("^ \\*Org todo\\*$"
         (display-buffer-in-side-window)
         (side . bottom)
         (window-height . 999))
        ("^\\*Capture\\*$"
         (display-buffer-in-side-window)
         (side . bottom)
         (window-height . 15))
        ("^\\*Calendar\\*$"
         (display-buffer-in-side-window)
         (side . bottom)
         (window-height . 20))
        ("^\\*xref\\*$"
         (display-buffer-in-side-window)
         (side . right)
         (window-width . 80)
         (body-function . ,(circ/resize-window-to 80)))
        ("^CAPTURE-"
         (display-buffer-in-side-window)
         (side . bottom)
         (window-height . 15))
        ("^\\*arf-keybindings\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (window-width . 51)
         (body-function . ,(circ/resize-window-to 51)))
        ("^\\*vterm-input\\*"
         (display-buffer-reuse-window display-buffer-below-selected)
         (window-height . 20))
        ("^\\*eldoc\\*$"
         (display-buffer-reuse-window display-buffer-in-side-window)
         (side . right)
         (window-width . 80)
         (body-function . ,(circ/resize-window-to 80)))))
#+end_src

* Sessions

#+begin_src emacs-lisp
;; Save and restore sessions (buffers, window layouts, tabs)
(desktop-save-mode 1)
(setq desktop-restore-frames t)
(setq desktop-restore-eager t)
#+end_src

* Dired

#+begin_src emacs-lisp
(setq dired-kill-when-opening-new-dired-buffer t)
(setq dired-dwim-target t)
(setq dired-listing-switches "-alh")
(setq wdired-allow-to-change-permissions t)
#+end_src

* VC
** Diff Faces

#+begin_src emacs-lisp
(custom-set-faces
 '(diff-added ((t (:foreground "#66cc66" :background "#1a331a"))))
 '(diff-removed ((t (:foreground "#cc6666" :background "#331a1a"))))
 '(diff-changed ((t (:foreground "#cccc66" :background "#33331a"))))
 '(diff-refine-added ((t (:foreground "#88ee88" :background "#264d26" :weight bold))))
 '(diff-refine-removed ((t (:foreground "#ee8888" :background "#4d2626" :weight bold))))
 '(diff-refine-changed ((t (:foreground "#eeee88" :background "#4d4d26" :weight bold))))
 '(diff-header ((t (:foreground "#cc9933"))))
 '(diff-file-header ((t (:foreground "#ccaa44" :weight bold))))
 '(diff-hunk-header ((t (:foreground "#6699cc"))))
 '(diff-context ((t (:foreground "#aaaaaa")))))
#+end_src

* Eglot
#+begin_src emacs-lisp
(defun circ/completion-in-region-to-minibuffer (start end collection &optional predicate)
  "Route `completion-in-region' through the minibuffer so fido can handle it."
  (if (minibufferp)
      (completion--in-region start end collection predicate)
    (let* ((initial (buffer-substring-no-properties start end))
           (candidates (all-completions initial collection predicate))
           (choice (completing-read "Complete: " candidates nil nil initial)))
      (when choice
        (delete-region start end)
        (insert choice)))))

(setq completion-in-region-function #'circ/completion-in-region-to-minibuffer)
(setq eldoc-echo-area-use-multiline-p 3)

(add-hook 'python-mode-hook #'eglot-ensure)
#+end_src

* XREF
#+begin_src emacs-lisp
(add-hook 'xref--xref-buffer-mode-hook (lambda () (hl-line-mode +1)))
#+end_src

* Server

#+begin_src emacs-lisp
(require 'server)
(unless (server-running-p)
  (server-start))
#+end_src

* Abbreviations
#+begin_src emacs-lisp
(setq-default abbrev-mode t)

;; Regex to allow matching with a ; prefix. Copy it to new tables if you add new modes to abbrev mode.
(abbrev-table-put global-abbrev-table :regexp
                  "\\(?:^\\|\\s-+\\)\\(?1:[;]\\w+\\)")
#+end_src

** Org Mode
#+begin_src emacs-lisp
(abbrev-table-put org-mode-abbrev-table :regexp
                  "\\(?:^\\|\\s-+\\)\\(?1:[;]\\w+\\)")

(define-skeleton org-src-skeleton
  "Insert an org source block."
  "Language: "
  "#+begin_src " str \n
  _ \n
  "#+end_src")
(define-abbrev org-mode-abbrev-table ";src" "" 'org-src-skeleton)

(define-skeleton org-src-el-skeleton
  "Insert an org emacs lisp source block."
  nil
  "#+begin_src emacs-lisp" \n
  _ \n
  "#+end_src")
(define-abbrev org-mode-abbrev-table ";srcel" "" 'org-src-el-skeleton)

(define-skeleton org-properties
  "Insert org mode properties section"
  nil
  ":PROPERTIES:" \n
  _ \n
  ":END:")
(define-abbrev org-mode-abbrev-table ";prop" "" 'org-properties)

(define-skeleton org-category
  "Insert org property for category"
  "Category: "
  ":CATEGORY: " str)
(define-abbrev org-mode-abbrev-table ";cat" "" 'org-category)

;; Demo: multiple prompts
(define-skeleton org-link-skeleton
  "Insert an org link with description."
  "URL: "
  "[[" str "][" (skeleton-read "Description: ") "]]")
(define-abbrev org-mode-abbrev-table ";link" "" 'org-link-skeleton)

;; Demo: getting value from a function
(define-skeleton org-date-skeleton
  "Insert an org timestamp with today's date."
  nil
  "<" (format-time-string "%Y-%m-%d %a") ">")
(define-abbrev org-mode-abbrev-table ";date" "" 'org-date-skeleton)

(define-abbrev org-mode-abbrev-table ";title" "#+TITLE:")
#+end_src

* Whitespace
#+begin_src emacs-lisp
(require 'whitespace)
(setq whitespace-style '(face empty tabs trailing))
(global-whitespace-mode t)
(setq whitespace-line-column 80)

;; Disable in certain modes
(setq whitespace-global-modes '((not erc-mode)
                                (not org-mode)))
#+end_src
* balance-window fix
=balance-windows= resizes side windows even though they have a fixed size.
Temporarily mark side windows as size-preserved during balancing so they
are left alone, then remove the preservation so manual resizing still works.

#+begin_src emacs-lisp
(defun circ/balance-windows-preserve-sides (orig-fn &rest args)
  "Advice around `balance-windows' to leave side windows alone."
  (let (sides)
    ;; Mark all side windows as size-preserved
    (walk-windows
     (lambda (w)
       (when (window-parameter w 'window-side)
         (push w sides)
         (window-preserve-size w t t)
         (window-preserve-size w nil t)))
     nil (selected-frame))
    (unwind-protect
        (apply orig-fn args)
      ;; Remove preservation so manual resize still works
      (dolist (w sides)
        (when (window-live-p w)
          (window-preserve-size w t nil)
          (window-preserve-size w nil nil))))))

(advice-add 'balance-windows :around #'circ/balance-windows-preserve-sides)
#+end_src

* Maximise windows
#+begin_src emacs-lisp
(defun circ/maximise-window-vertical (&optional window)
  (interactive)
  (setq window (window-normalize-window window))
  (window-resize
   window (window-max-delta window nil nil nil nil nil window-resize-pixelwise)
   nil nil window-resize-pixelwise))

(defun circ/maximise-window-horizontal (&optional window)
  (interactive)
  (setq window (window-normalize-window window))
  (window-resize
   window (window-max-delta window t nil nil nil nil window-resize-pixelwise)
   t nil window-resize-pixelwise))
#+end_src
* Minimize windows

#+begin_src emacs-lisp
(defun circ/minimise-window-vertical (&optional window)
  (interactive)
  (setq window (window-normalize-window window))
  (window-resize
   window
   (- (window-min-delta window nil nil nil nil nil window-resize-pixelwise))
   nil nil window-resize-pixelwise))

(defun circ/minimise-window-horizontal (&optional window)
  (interactive)
  (setq window (window-normalize-window window))
  (window-resize
   window
   (- (window-min-delta window t nil nil nil nil window-resize-pixelwise))
   t nil window-resize-pixelwise))
#+end_src

* Repeat Replace Index
Highlight region. Run funcion. Replaces instances of {} with index.

#+begin_src emacs-lisp
(defun circ/repeat-replace-index ()
  "Asks for a starting index and a step function, then replaces
all instances of `{}' in the hightlighted region."
  (interactive)
  (let ((index (string-to-number (read-string "Starting index: " "1")))
        (step-function (car (read-from-string
                             (read-string "Step function (n is current index): "
                                          "(+ n 1)"))))
        (transform-function (car (read-from-string
                                  (read-string "Transform function (n is current index): "
                                               "(+ n 0)"))))
        (index-format (read-string "Index Format: " "%d"))
        (region (buffer-substring (region-beginning) (region-end)))
        (replacement-string nil))
    (save-excursion
      (with-temp-buffer
        (insert region)
        (goto-char (point-min))
        (while (search-forward "{}" nil t)
          (delete-region (match-beginning 0) (match-end 0))
          (goto-char (match-beginning 0))
          (setq n index)
          (setq n (eval transform-function))
          (insert (format index-format n))
          (setq n index)
          (setq index (eval step-function)))
        (setq replacement-string (buffer-substring (point-min) (point-max))))
      (delete-region (region-beginning) (region-end))
      (goto-char (region-beginning))
      (insert replacement-string))))
#+end_src

* Highlight
#+begin_src emacs-lisp
(set-face-attribute 'highlight nil
		    :background "blue3")
#+end_src

* No-Break Space
#+begin_src emacs-lisp
(set-face-attribute 'nobreak-space nil :underline nil :background nil)
#+end_src
