Permalink
Cannot retrieve contributors at this time
8435 lines (6658 sloc)
264 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;;; init.el --- Göktuğ's Emacs configuration. -*- lexical-binding: t; coding: utf-8 -*- | |
| ;; Author: Göktuğ Kayaalp <self@gkayaalp.com> | |
| ;; Version: ETERNAL | |
| ;; URL: https://www.gkayaalp.com/emacs.html | |
| ;; Package-Requires: ((emacs "27.0.50")) | |
| ;;; Commentary: | |
| ;; This file is the initialisation file for my personal setup. It's | |
| ;; split into sections using the form feed characters and outline-mode | |
| ;; section headers. | |
| ;; I encourage anybody to take all the bits they'd like from this file | |
| ;; (or any other file in this whole configuration tree, for that | |
| ;; matter), but I strongly *discourage* it's use as a whole package of | |
| ;; software, as it's tailored for my workflow and my general computing | |
| ;; environment, so it probably won't work for you. Still, you can | |
| ;; totally do that if you want, as this whole tree contains all the | |
| ;; required scripts and configuration files to instantiante a working | |
| ;; environment nearly identical to mine. | |
| ;;; Code: | |
| ;; The subsequent sections constitute the Emacs initialisation code. | |
| ;;; Prelude: | |
| ;; Report load time. | |
| (defvar gk-emacs-initialisation-started-time nil | |
| "Time at which ‘user-init-file’ started loading.") | |
| (setq gk-emacs-initialisation-started-time (current-time)) | |
| (defvar gk-emacs-initialisation-completed-time nil | |
| "Time at which ‘user-init-file’ finished loading.") | |
| (unless noninteractive | |
| (add-hook | |
| 'after-init-hook | |
| (lambda () | |
| (setq gk-emacs-initialisation-completed-time (current-time) | |
| initial-scratch-message (format-time-string | |
| (concat ";;\n;; Welcome to Emacs, with cadadr’s mutila^Wcustomisations!\n" | |
| ";; Initialisation completed, took %-S.%3N seconds.\n;;\n\n\n") | |
| (time-subtract | |
| gk-emacs-initialisation-completed-time | |
| gk-emacs-initialisation-started-time))) | |
| (run-with-timer | |
| 1.5 nil | |
| (lambda () (message | |
| ;; remove the trailing newline | |
| (substring initial-scratch-message | |
| 0 (- (length initial-scratch-message) 3)))))))) | |
| (when (version< emacs-version "28.0") | |
| (error "This configuration requires a recent build of Emacs master")) | |
| ;; Use elisp directory listing program. This needs to be set before | |
| ;; loading ls-lisp.el. | |
| (defvar ls-lisp-use-insert-directory-program nil) | |
| ;;;; Loadpaths: | |
| (load (locate-user-emacs-file "loadpaths")) | |
| ;;;; Requires: | |
| ;; Load all requirements up before running initialisation code. This | |
| ;; slows down initialisation and increases initial memory use, but | |
| ;; otherwise running commands may take a longer time when features | |
| ;; need to be loaded for running them. | |
| ;; This list should be sorted alphabetically. | |
| (eval-when-compile (require 'cl)) | |
| (require 'ace-jump-mode) | |
| (require 'anaconda-mode) | |
| (require 'ansi-color) | |
| (require 'apropos) | |
| (require 'auth-source) | |
| (require 'autoinsert) | |
| (require 'bbdb) | |
| (require 'bbdb-vcard) | |
| (require 'bookmark) | |
| (require 'boxquote) | |
| (require 'browse-url) | |
| (require 'calendar) | |
| (require 'cc-mode) | |
| (require 'comint) | |
| (require 'compile) | |
| (require 'copyright) | |
| (require 'dart-mode) | |
| (require 'dash) | |
| (require 'debug) | |
| (require 'deft) | |
| (require 'desktop) | |
| (require 'diff) | |
| (require 'diminish) | |
| (require 'dired) | |
| (require 'dired-narrow) | |
| (require 'dired-subtree) | |
| (require 'dired-x) | |
| (require 'doc-view) | |
| (require 'doifetch) | |
| (require 'dollar) | |
| (require 'ebib) | |
| (require 'eglot) | |
| (require 'eldoc) | |
| (require 'elfeed) | |
| (require 'elpher) | |
| (require 'epa) | |
| (require 'epa-mail) | |
| (require 'epg) | |
| (require 'eros) | |
| (require 'eshell) | |
| (require 'em-hist) | |
| (require 'ess-r-mode) | |
| (require 'etags) | |
| (require 'eval-sexp-fu) | |
| (require 'eww) | |
| (require 'f) | |
| (require 'face-remap) ; buffer-face-mode | |
| (require 'ffap) | |
| (require 'files) | |
| (require 'flymake-python-pyflakes) | |
| (require 'flyspell) | |
| (require 'forecast) | |
| (require 'gemini-mode) | |
| (require 'geoclue) | |
| (require 'git-commit) | |
| (require 'git-gutter) | |
| (require 'git-gutter-fringe) | |
| (require 'gk-greek) | |
| (require 'gk-unilat) | |
| (require 'goto-addr) | |
| (require 'goto-last-change) | |
| (require 'haskell-mode) | |
| (require 'highlight-indent-guides) | |
| (require 'highlight-parentheses) | |
| (require 'hl-line) | |
| (require 'ibuffer) | |
| (require 'ibuffer-vc) | |
| (require 'ido) | |
| (require 'ido-vertical-mode) | |
| (require 'image) | |
| (require 'image-dired) | |
| (require 'imenu) | |
| (require 'inf-lisp) | |
| (require 'inf-ruby) | |
| (require 'info-look) | |
| (require 'ispell) | |
| (require 'js) | |
| (require 'log-edit) | |
| (require 'lorem-ipsum) | |
| (require 'lua-mode) | |
| (require 'ls-lisp) | |
| (require 'magit) | |
| (require 'magit-todos) | |
| (require 'mail-source) | |
| (require 'mairix) | |
| (require 'markdown-mode) | |
| (require 'message) | |
| (require 'mm-url) | |
| (require 'multiple-cursors) | |
| (require 'netrc) | |
| (require 'nnfolder) | |
| (require 'nsm) | |
| (require 'org) | |
| (require 'org-attach-screenshot) | |
| (require 'org-capture) | |
| (require 'org-checklist) | |
| (require 'org-ebib) | |
| (require 'org-eldoc) | |
| (require 'org-habit) | |
| (require 'org-id) | |
| (require 'org-inlinetask) | |
| (require 'org-mobile) | |
| (require 'org-num) | |
| (require 'org-protocol) | |
| (require 'org-tempo) ; <s, <q &c | |
| (require 'org-variable-pitch) | |
| (require 'org-zotxt) | |
| (require 'outline) | |
| (require 'ox) | |
| (require 'ox-beamer) | |
| (require 'ox-hugo) | |
| (require 'ox-latex) | |
| (require 'ox-odt) | |
| (require 'ox-org) | |
| (require 'ox-publish) | |
| (require 'paredit) | |
| (require 'paren-face) | |
| (require 'parse-time) | |
| (require 'pass-listing) | |
| (require 'pdf-tools) | |
| (require 'pdf-annot) | |
| (require 'pdf-cache) | |
| (require 'pdf-isearch) | |
| (require 'pdf-misc) | |
| (require 'pdf-outline) | |
| (require 'pdf-sync) | |
| (require 'perl-mode) | |
| (require 'persistent-scratch) | |
| (require 'pip-requirements) | |
| (require 'pixel-scroll) | |
| (require 'pp) | |
| (require 'project) | |
| (require 'pydoc-info) | |
| (require 'python) | |
| (require 'pythonic) | |
| (require 'quail) | |
| (require 'rect) | |
| (require 'rmail) | |
| (require 'rmailsum) | |
| (require 'ruby-mode) | |
| (require 'rx) | |
| (require 's) | |
| (require 'saveplace) | |
| (require 'savehist) | |
| (require 'scheme) | |
| (require 'sendmail) | |
| (require 'seq) | |
| (require 'shell) | |
| (require 'shr) | |
| (require 'simple) | |
| (require 'skewer-mode) | |
| (require 'skewer-css) | |
| (require 'skewer-html) | |
| (require 'smex) | |
| (require 'smtpmail) | |
| (require 'so-long) | |
| (require 'subr-x) | |
| (require 'switch-window) | |
| (require 'textile-mode) | |
| (require 'thingatpt) | |
| (require 'thinks) | |
| (require 'time) | |
| (require 'tramp) | |
| (require 'tramp-cache) | |
| (require 'turkish) | |
| (require 'undo-tree) | |
| (require 'uniquify) | |
| (require 'url) | |
| (require 'valign) | |
| (require 'vc) | |
| (require 'vc-cvs) | |
| (require 'vc-rcs) | |
| (require 'wdired) | |
| (require 'which-key) | |
| (require 'whitespace) | |
| (require 'whole-line-or-region) | |
| (require 'windmove) | |
| (require 'winner) | |
| (require 'zencoding-mode) | |
| (require 'yasnippet) | |
| ;;; Files and directories: | |
| (defconst gk-dropbox-dir | |
| (expand-file-name "~/fil") | |
| "Directory of Dropbox.") | |
| (defun dropbox (path) | |
| "Return ~/Dropbox + PATH." | |
| (expand-file-name path gk-dropbox-dir)) | |
| (defconst gk-syndir | |
| (expand-file-name "~/syn") | |
| "Directory for syncing.") | |
| (setf image-dired-dir (locate-user-emacs-file "etc/image-dired") | |
| url-configuration-directory (locate-user-emacs-file "etc/url") | |
| auto-save-list-file-prefix (locate-user-emacs-file | |
| "etc/auto-save-list/saves-") | |
| bookmark-default-file (dropbox "bookmarks.el") | |
| bbdb-file (expand-file-name "~/Notes/bbdb-contacts.el") | |
| savehist-file (locate-user-emacs-file "etc/history") | |
| eww-bookmarks-directory (dropbox ".") | |
| save-place-file (locate-user-emacs-file "etc/places") | |
| tramp-persistency-file-name (locate-user-emacs-file "etc/tramp") | |
| custom-file (locate-user-emacs-file "etc/custom.el") | |
| nsm-settings-file (locate-user-emacs-file "etc/network-security.data") | |
| mc/list-file (locate-user-emacs-file "etc/mc-lists.el")) | |
| (defvar gk-website-settings | |
| (expand-file-name "~/Documents/not/www/publish") | |
| "Settings for publishing http://www.gkayaaalp.com.") | |
| ;;; Utility libraries: | |
| ;;;; Utility functions: | |
| (defun gk-backup-file-name (directory extension) | |
| (let ((filename (concat directory | |
| (format-time-string "%d-%m-%Y" (current-time)))) | |
| (extension (concat "." extension))) | |
| (while (file-exists-p (concat filename extension)) | |
| (setq filename (concat filename "+"))) | |
| (concat filename extension))) | |
| (defun gk-apropos-at-point-or-region () | |
| (interactive) | |
| (let ((default (if (region-active-p) | |
| (buffer-substring (region-beginning) (region-end)) | |
| (thing-at-point 'word)))) | |
| (apropos (read-string "Search for command or function (word list or regexp): " | |
| default nil default)))) | |
| (defun gk-indent-defun () | |
| (interactive) | |
| (save-excursion | |
| (mark-defun) | |
| (indent-region (region-beginning) (region-end)))) | |
| (defun gk-which-mode (buffer) | |
| (interactive "bShow major mode for buffer") | |
| (with-current-buffer (get-buffer buffer) | |
| (set-register ?M (symbol-name major-mode)) | |
| (message "Major mode for buffer '%s' is '%s'. C-x r i M to insert it." | |
| buffer | |
| major-mode))) | |
| (defun gk-comment-dwim (arg) | |
| "If region is active, or at the end of the line, call ‘comment-dwim’. | |
| Pass ARG to ‘comment-dwim’ in that case. | |
| Otherwise comment-out the whole line, or ARG lines." | |
| (interactive "*P") | |
| (cond | |
| ((save-excursion | |
| (beginning-of-line) | |
| (looking-at (concat "[[:blank:]]*" comment-start))) | |
| (uncomment-region (point-at-bol) (point-at-eol))) | |
| ((or (looking-at "[[:blank:]]*$") | |
| (region-active-p)) | |
| (comment-dwim arg)) | |
| (t (save-excursion (comment-line arg))))) | |
| (defun gk-reformat-date (format date) | |
| "Parse DATE, then apply FORMAT to it. | |
| For the format, see `format-time-string'." | |
| (format-time-string format (date-to-time date))) | |
| (defun gk-executable-ensure (command &optional silent) | |
| "Err-out if COMMAND is not found." | |
| (if-let* ((ex (executable-find command))) | |
| ex | |
| (when (not silent) | |
| (warn "Program is absent: %s" command)))) | |
| (defun gk-get-file-contents (file) | |
| "Get the contents of FILE as a string." | |
| (with-temp-buffer | |
| (insert-file-contents file) | |
| (buffer-substring (point-min) (point-max)))) | |
| (defun gk-deadvice (sym) | |
| "Remove all the advice functions from the function named SYM." | |
| (interactive | |
| (list | |
| (let* ((sym (ignore-errors (intern (thing-at-point 'symbol)))) | |
| (fn (and (fboundp sym) sym))) | |
| (read-command (concat "Remove advices from function" | |
| (if fn | |
| (format " (default: %S): " sym) | |
| ": ")) | |
| fn)))) | |
| (advice-mapc | |
| (lambda (x y) | |
| (ignore y) | |
| (remove-function (symbol-function sym) x)) | |
| sym)) | |
| (defun gk-gui-p () | |
| (or window-system (daemonp))) | |
| (defun gk-swap-windows (&optional arg) | |
| "Swap the buffer of the selected window with that of the next one. | |
| When ARG is a positive number, repeat that many times." | |
| (interactive "p") | |
| (dotimes (i (or arg 1)) | |
| (ignore i) | |
| (let ((next (window-buffer (next-window))) | |
| (this (current-buffer))) | |
| (unless (equal this next) | |
| (switch-to-buffer next nil t) | |
| (switch-to-buffer-other-window this))))) | |
| (defun gk-copy-buffer-file-name () | |
| "Push the buffer's file name to the ‘kill-ring’." | |
| (interactive) | |
| (if-let* ((fil (buffer-file-name))) | |
| (with-temp-buffer | |
| (insert fil) | |
| (clipboard-kill-ring-save (point-min) (point-max)) | |
| (message fil)) | |
| (error "Buffer not visiting a file."))) | |
| (defun gk-copy-last-message () | |
| "Copy-as-kill the last echoed message." | |
| (interactive) | |
| (with-current-buffer (messages-buffer) | |
| (save-excursion | |
| (goto-char (point-max)) | |
| (forward-line -1) | |
| (clipboard-kill-ring-save | |
| (line-beginning-position) (line-end-position))))) | |
| (defun gk-copy-as-markdown-code-snippet (beg end) | |
| "Copy region, prepend four spaces to every line." | |
| (interactive "r") | |
| (let ((snip (buffer-substring beg end))) | |
| (with-temp-buffer | |
| (insert snip) | |
| (goto-char (point-min)) | |
| (while (re-search-forward "^" nil t) | |
| (replace-match " ")) | |
| (clipboard-kill-ring-save (point-min) (point-max))))) | |
| (defun gk-maybe-expand-abbrev-or-space () | |
| (interactive) | |
| (when (null (expand-abbrev)) | |
| (insert " "))) | |
| (defun gk-numeronym (name &optional insert-p) | |
| "Generate a numeronym of NAME, an arbitrary string. | |
| A numeronym is the initial letter, the length of the name in | |
| characters, and the last letter, | |
| i.e. i18n -> internationalisation. | |
| If INSERT-P is non-nil or called interactively with prefix-arg, | |
| insert the numeronym at point." | |
| (interactive (list (read-string "Enter the name to be numeronymified: ") | |
| ;; convert to bool | |
| (not (not current-prefix-arg)))) | |
| (let ((len (length name)) | |
| nym) | |
| (unless (>= len 2) (user-error "The name must be at least three characters long")) | |
| (setf nym (format "%c%d%c" (aref name 0) (- len 2) (aref name (1- len)))) | |
| (message nym) | |
| (when insert-p (insert nym)))) | |
| (defun gk-unbind-key (keyseq) | |
| "Unset the KEYSEQ in ‘gk-minor-mode-map’." | |
| (interactive "kKey sequence to unset: ") | |
| (define-key gk-minor-mode-map keyseq nil) | |
| (message "Done.")) | |
| (defun gk-delete-buffer-file () | |
| "Delete the file visited in the current buffer." | |
| (interactive) | |
| (if-let* ((f (buffer-file-name))) | |
| (when (yes-or-no-p | |
| (format | |
| "Delete file ‘%s’, visited by buffer ‘%s’" f (buffer-name))) | |
| (delete-file f delete-by-moving-to-trash) | |
| (message "Deleted %s." f)) | |
| (user-error "Buffer ‘%s’ is not visiting a file" (buffer-name)))) | |
| (defun gk-copy-buffer-file (file dest) | |
| "Copy the file visited in the current buffer." | |
| (interactive | |
| (list (buffer-file-name) | |
| (read-file-name (format "Copy %s to: " (buffer-file-name))))) | |
| (copy-file file dest nil t t t)) | |
| (defun gk-rename-buffer-file (dest) | |
| "Rename the file visited in the current buffer." | |
| (interactive | |
| (list (read-file-name (format "Rename %s to: " (buffer-file-name))))) | |
| (rename-file (buffer-file-name) dest) | |
| (find-alternate-file dest)) | |
| (defun gk-truncate-and-fill-string (len s) | |
| (let ((slen (length s))) | |
| (if (> slen len) | |
| (s-truncate len s) | |
| (concat s (make-string (- len slen) ?\ ))))) | |
| (defun gk-find-file (arg) | |
| "Like ‘find-file’ but find file at point if ARG is non-nil." | |
| (interactive "P") | |
| ;; See http://lists.gnu.org/archive/html/help-gnu-emacs/2018-04/msg00280.html | |
| (let ((current-prefix-arg nil)) | |
| (call-interactively (if arg #'ffap #'find-file)))) | |
| (defun // (&rest args) | |
| (apply #'/ (mapcar #'float args))) | |
| (define-obsolete-function-alias 'gk-update-package-load-paths | |
| 'gk-update-user-site-paths "2020-09-23") | |
| (defun gk-send-desktop-notification (summary message &optional icon) | |
| "Show a notification on the desktop." | |
| (unless (gk-gui-p) | |
| (error "Cannot send desktop notification in non-GUI session")) | |
| (make-process | |
| :name "gk-desktop-notification" | |
| :buffer (get-buffer-create " *Desktop Notifications*") | |
| :command | |
| (cond | |
| ((executable-find "notify-send") | |
| (list "notify-send" (concat "[Emacs] " summary) "-i" (or icon "") message)) | |
| ((executable-find "kdialog") | |
| (list "kdialog" "--passivepopup" message "10" | |
| "--title" (concat "[Emacs] " summary)))))) | |
| (defun gk-existing-file-name-or-nil (filename) | |
| (when (file-exists-p filename) | |
| filename)) | |
| (defun gk-insert-today (&optional full) | |
| "Insert today's date into the current buffer, before point. | |
| FULL is the processed prefix argument from the interactive call. | |
| With no prefix arguments, insert YYYY-MM-DD (ISO 8601 date). | |
| With one prefix argument, insert YYYY-MM-DD (ISO 8601 date) with | |
| HH:MM:SS. With two prefix arguments, insert a full ISO 8601 date | |
| together with current time and timezone information." | |
| (interactive "p") | |
| (insert | |
| (format-time-string | |
| (case full | |
| (1 "%F") ;ISO date format | |
| (4 "%F %T") ;ISO date format with time w/ seconds | |
| (16 "%FT%T%z") ;full ISO 8601 | |
| )))) | |
| (defun gk-toggle-wrap (&optional arg) | |
| "Toggle word wrap and line truncation. | |
| Without a prefix ARG, toggle the latter off and the former on. | |
| With a positive prefix, turn both on. With a negative prefix, | |
| turn both off. With a zero prefix, toggle both." | |
| (interactive "p") | |
| (cond ((or (null arg) (= arg 1)) | |
| (toggle-truncate-lines -1) | |
| (toggle-word-wrap +1)) | |
| ((= arg 0) | |
| (toggle-truncate-lines (if truncate-lines -1 +1)) | |
| (toggle-word-wrap (if word-wrap -1 +1))) | |
| ((> arg 1) | |
| (toggle-truncate-lines +1) | |
| (toggle-word-wrap +1)) | |
| ((< arg 0) | |
| (toggle-truncate-lines -1) | |
| (toggle-word-wrap -1))) | |
| (message "truncate-lines: %S; word-wap: %S" truncate-lines word-wrap)) | |
| (defun gk-view-emacs-proc-file () | |
| "Open the Emacs process status file under /proc." | |
| (interactive) | |
| (find-file (format "/proc/%d/status" (emacs-pid)))) | |
| (defun gk-ellipsize-file-or-directory-name (name maxlen) | |
| "Ellipsize the directory part of a file NAME. | |
| If NAME is larget than MAXLEN, ellipsise the directory part, | |
| preserving, ‘file-name-nondirectory’ if it's a file or the last | |
| directory name if a directory, returning the ellipsized string as | |
| the result." | |
| (if (> (length name) maxlen) | |
| (if (or (file-directory-p name) | |
| (save-match-data (string-match "/$" name))) | |
| (let* ((bits (split-string name "/" t)) | |
| (head (butlast bits)) | |
| (tail (car (last bits)))) | |
| (concat | |
| (unless (equal (car bits) "~") "/") | |
| (substring (mapconcat #'identity head "/") 0 | |
| (- (- maxlen 4) (length bits))) | |
| ".../" tail "/")) | |
| (let ((fnod (file-name-nondirectory name))) | |
| (concat | |
| (substring (file-name-directory name) 0 | |
| (- (- maxlen 4) (length fnod))) | |
| ".../" fnod))) | |
| name)) | |
| (defun gk-next-theme () | |
| "Switch to the next theme in ‘custom-known-themes’. | |
| If exhausted, disable themes. If run again thereafter, wrap to | |
| the beginning of the list." | |
| (interactive) | |
| (let* ((ct (or (car custom-enabled-themes) | |
| (car custom-known-themes))) | |
| (next (cadr (memq ct custom-known-themes)))) | |
| (when (memq next '(user changed)) | |
| (setq next nil)) | |
| (dolist (theme custom-enabled-themes) | |
| (disable-theme theme)) | |
| (if next | |
| (progn | |
| (load-theme next t) | |
| (message "Loaded theme ‘%S’" next)) | |
| (message "All themes disabled")))) | |
| (cl-defun gk-flash-current-line (&optional buffer &key (seconds 0.5)) | |
| "Flash current line briefly for SECONDS in BUFFER. | |
| BUFFER defaults to current buffer, and SECONDS to 1." | |
| (interactive) | |
| (unless hl-line-mode | |
| (let ((buf (or buffer (current-buffer)))) | |
| (hl-line-mode +1) | |
| (run-with-idle-timer | |
| seconds nil | |
| ($ (with-current-buffer buf | |
| (hl-line-mode -1))))))) | |
| (defun gk-empty-kill-ring () | |
| "Empty the kill ring." | |
| (interactive) | |
| (when kill-ring | |
| (setq kill-ring nil) | |
| (garbage-collect))) | |
| (defun gk-bol () | |
| "Alternate between the first and the indentation on a line." | |
| (interactive) | |
| (cond | |
| ;; If this is an eshell buffer and we’re at a prompt line, jump to | |
| ;; prompt position. | |
| ((and (eq major-mode 'eshell-mode) | |
| (save-excursion | |
| (goto-char (line-beginning-position)) | |
| (looking-at eshell-prompt-regexp))) | |
| (goto-char (line-end-position)) | |
| (eshell-previous-prompt 0)) | |
| ;; Otherwise, alternate bol/indentation. | |
| (t | |
| (let ((bolf (if visual-line-mode #'beginning-of-visual-line | |
| #'beginning-of-line)) | |
| (p (point))) | |
| ;; We do this to prevent any flicker happening between | |
| ;; ‘back-to-indentation’ and ‘bolf‘ when going to | |
| ;; ‘beginning-of-line’. | |
| (goto-char | |
| (save-excursion | |
| ;; If visual-line-mode is on and we're on a continuation line, | |
| ;; go to the beginning of the continuation line. | |
| ;; | |
| ;; XXX: sometimes this goes to the previous line because of | |
| ;; word-wrapping | |
| (if (and visual-line-mode | |
| (>= (- p (line-beginning-position)) | |
| (window-width))) | |
| (funcall bolf) | |
| ;; Else, do the toggling. | |
| (progn | |
| ;; Go back to indentation. | |
| (back-to-indentation) | |
| ;; If we didn't move, we were already at the indentation. | |
| ;; Go to the beginning of the line. | |
| (when (= p (point)) | |
| (funcall bolf)))) | |
| ;; Return the point. | |
| (point))))))) | |
| (defvar gk-insert-todo-comment--history nil) | |
| (cl-pushnew 'gk-insert-todo-comment--history savehist-additional-variables) | |
| (defvar gk-insert-todo-comment-keywords '("TODO" "XXX" "HACK" "FIXME")) | |
| (defvar gk-insert-todo-comment-default (car gk-insert-todo-comment-keywords)) | |
| (defun gk-insert-todo-comment (keyword) | |
| "Insert a TODO comment with date." | |
| (interactive | |
| (list | |
| (completing-read | |
| (format "Todo keyword to use (default: %s): " | |
| gk-insert-todo-comment-default) | |
| gk-insert-todo-comment-keywords | |
| nil nil nil 'gk-insert-todo-comment--history "TODO" t))) | |
| (gk-comment-dwim nil) | |
| (insert keyword) | |
| (insert (format-time-string "(%F): "))) | |
| ;; Adapted from https://www.reddit.com/r/emacs/comments/bwm94g/weekly_tipstricketc_thread/eq09l4k/ | |
| (defun gk-search-forward-1 (char &optional count) | |
| "Search forward for CHAR COUNT times in current line." | |
| (interactive | |
| (list (read-char "1> ") | |
| current-prefix-arg)) | |
| (forward-char) | |
| (unwind-protect | |
| (search-forward (char-to-string char) (line-end-position) nil (or count 1)) | |
| (backward-char) | |
| (point))) | |
| (defun gk-search-backward-1 (char &optional count) | |
| "Search backward for CHAR COUNT times in current line." | |
| (interactive | |
| (list (read-char "1> ") | |
| current-prefix-arg)) | |
| (backward-char) | |
| (unwind-protect | |
| (search-backward (char-to-string char) (line-beginning-position) nil | |
| (or count 1)) | |
| (forward-char) | |
| (point))) | |
| (defun gk-build-emacs-master () | |
| "Run Emacs git build wrapper script." | |
| (interactive) | |
| (let ((compilation-buffer-name-function ($ [_] "*Build Emacs Master*"))) | |
| (compile "build-emacs-master.sh" t))) | |
| (defun gk-visit-user-init-file () | |
| "Visit ‘user-init-file’, reuse window if useful. | |
| Flash the current line after that." | |
| (interactive) | |
| (let ((file (file-truename (expand-file-name user-init-file)))) | |
| ;; If viewing the file, only flash current line. | |
| (unless (string= file (buffer-file-name (window-buffer))) | |
| ;; Otherwise, open it, or if the current frame already has a | |
| ;; window displaying it, switch to it. | |
| (select-window | |
| (display-buffer | |
| (find-file-noselect file) | |
| '(display-buffer-reuse-window . ((reusable-frames . nil) | |
| (inhibit-same-window . t))))))) | |
| (gk-flash-current-line)) | |
| (defun gk-decode-xml-entities-in-region (beginning end) | |
| (interactive "r") | |
| (let ((str (buffer-substring (region-beginning) (region-end)))) | |
| (with-temp-buffer | |
| (save-excursion (insert str)) | |
| (message (substring-no-properties (xml-parse-string)))))) | |
| (defun gk-base64-decode-url (beg end) | |
| "Base64-decode the region between BEG and END, assume URL encoding. | |
| This basically calls ‘base64-decode-region’ with the third | |
| argument as t, so refer to that command for further details." | |
| (interactive "r") | |
| (base64-decode-region beg end t)) | |
| (defun gk-new-journal-entry () | |
| (interactive) | |
| (find-file (format-time-string (gk-org-dir-file "Journal/%F"))) | |
| (gk-insert-today 16)) | |
| (defun gk-cancel-last-timer () | |
| "Cancel the most recently created timer." | |
| (interactive) | |
| (cancel-timer (car timer-list))) | |
| (defun gk-jump-to-window () | |
| "Jump to a window, completing from window’s buffer name." | |
| (interactive) | |
| (let ((winbufs | |
| (mapcar | |
| ($ (cons (buffer-name (window-buffer $1)) $1)) | |
| (window-list)))) | |
| (select-window | |
| (gk-assoca | |
| (completing-read "Window with buffer: " winbufs) | |
| winbufs)))) | |
| (defun gk-append-to-list (list-name elt) | |
| "Append ELT to list names LIST-NAME." | |
| (set list-name (apply #'append (symbol-value list-name) (list elt)))) | |
| (defun concise (search-term) | |
| "Search in The Concise Oxford Dictionary of Linguistics (Matthews, 2003). | |
| The file (a plain text extract of the ebook or PDF) is assumed to | |
| be bookmarked as \"concise\". SEARCH-TERM is used to run an | |
| ‘occur’ search in the file. | |
| Matthews, P. H. (2003). The Concise Oxford Dictionary of | |
| Linguistics (2nd ed.). Oxford University Press." | |
| (interactive | |
| (list | |
| (string-trim | |
| (read-string | |
| "Search term (in the Concise Dictionary (Matthews, 2003): ")))) | |
| (when (string-empty-p search-term) | |
| (user-error "Empty search")) | |
| (with-current-buffer | |
| (find-file-noselect (bookmark-get-filename "concise")) | |
| (occur search-term))) | |
| (defun gk-maximize (&optional buffer) | |
| "Make maximized frame with a sole window displaying BUFFER." | |
| (interactive | |
| (list | |
| (read-buffer "Select buffer to maximise in new frame: " | |
| (current-buffer)))) | |
| (let* ((buf (or buffer (current-buffer))) | |
| (frame (make-frame)) | |
| (win (car (window-list frame)))) | |
| (set-window-buffer win buf) | |
| (toggle-frame-maximized frame) | |
| (raise-frame frame))) | |
| (defun gk-flip--1 (fn buf) | |
| "Subroutine of ‘gk-flip’." | |
| (delete-other-windows) | |
| (funcall fn) | |
| (other-window 1) | |
| (switch-to-buffer buf) | |
| (other-window 1)) | |
| (defun gk-flip--2 (&rest dirs) | |
| "Subroutine of ‘gk-flip’." | |
| (when-let* ((w (cl-remove-if #'null (mapcar #'window-in-direction dirs)))) | |
| (apply #'window-buffer w))) | |
| (defun gk-flip () | |
| "Flip horizontal and vertical split when there are two windows." | |
| (interactive) | |
| (unless (= 2 (length (window-list))) | |
| (user-error "Can’t flip unless there are exactly two windows")) | |
| ;; Attempt flipping horizontal to vertical. | |
| (if-let* ((other-buffer (gk-flip--2 'left 'right))) | |
| (gk-flip--1 #'split-window-vertically | |
| other-buffer) | |
| ;; If not possible, attempt vertical to horizontal. | |
| (let* ((other-buffer (gk-flip--2 'above 'below))) | |
| (gk-flip--1 #'split-window-horizontally | |
| other-buffer)))) | |
| (defun gk-news () | |
| "Open ‘rmail’ and ‘elfeed’, update both." | |
| (interactive) | |
| (delete-other-windows) | |
| (rmail) | |
| (split-window-sensibly) | |
| (other-window 1) | |
| (elfeed) | |
| (elfeed-search-fetch nil) | |
| (gk-fetch-mail)) | |
| (defun gk-save-string-as-kill (str) | |
| "Push STR on kill ring, syncing with the clipboard." | |
| (with-temp-buffer | |
| (insert str) | |
| (clipboard-kill-ring-save (point-min) (point-max)) | |
| (message "Copied %s" str))) | |
| ;; Adapted from: https://christiantietze.de/posts/2021/06/emacs-center-window-on-current-monitor/ | |
| (defun gk-frame-recenter (&optional frame) | |
| "Center a frame on the current display." | |
| (interactive) | |
| (unless (eq 'maximised (frame-parameter nil 'fullscreen)) | |
| (let* ((w (frame-pixel-width frame)) | |
| (h (frame-pixel-height frame)) | |
| (cw (caddr (frame-monitor-workarea frame))) | |
| (ch (cadddr (frame-monitor-workarea frame))) | |
| (center (list (/ (- cw w) 2) (/ (- ch h) 2)))) | |
| (apply 'set-frame-position (flatten-list (list frame center)))))) | |
| (defun gk-face-and-font-family-at-point (point) | |
| "Face identifier and font family at point. | |
| Interactively, POINT is the current location of the point in the | |
| current buffer; the information will be revealed in the echo | |
| area. | |
| When called from Lisp, a plist with the same information is | |
| returned, where the face name is a symbol and the font family is | |
| a string, the family name." | |
| (interactive (list (point))) | |
| (let* ((face (save-excursion (goto-char (point)) (face-at-point))) | |
| (ffam (font-get (font-at point) :family))) | |
| (when (called-interactively-p) | |
| (message "At %s:%d: face is ‘%s’; font is ‘%s’." | |
| (buffer-name) point face ffam)) | |
| (list :family ffam :face face))) | |
| ;;;; Generic advices: | |
| (defun gk-ad-stay-here (fun &rest args) | |
| "Stay in the current buffer when running FUN. | |
| Pass ARGS to FUN." | |
| (save-window-excursion | |
| (apply fun args))) | |
| (defun gk-protect-frame-focus (f &rest args) | |
| "Generic :around advice to reclaim frame focus. | |
| Some interactions with the OS, e.g. sending a link to the browser | |
| may result in Emacs losing focus. This is very rude of the | |
| OS. This function is a generic :around-advice that runs the given | |
| function and then reclaims focus after some time so the user can | |
| continue interacting with Emacs." | |
| (let ((frame (selected-frame))) | |
| (apply f args) | |
| (sit-for .3) | |
| (x-focus-frame frame))) | |
| ;;;; Recompilation: | |
| ;; This bit of code helps with recompilation. Various files external to | |
| ;; the configuration tree are loaded during the initialisation process. | |
| ;; Here we define a function called =gk-load= which makes note of each | |
| ;; file it loads in the variable =gk-loaded-files=, which is then used by | |
| ;; =gk-recompile= to determine which files need to be compiled to boost | |
| ;; the load speed next time. This way, =gk-recompile= does not need a | |
| ;; manually curated list of files to be compiled, like it did up until | |
| ;; now. | |
| (defvar gk-loaded-files nil) | |
| (defun gk-load (&rest args) | |
| "Identical to ‘load’, but makes note of files. | |
| This function passes its arguments untouched to ‘load’, but | |
| conses the car of ARGS to ‘gk-loaded-files’. The contents of | |
| that variable is then to be used to byte compile all the files | |
| explicitly loaded in this config without manually listing their | |
| names." | |
| (when (apply #'load args) | |
| (pushnew (expand-file-name (car args)) gk-loaded-files))) | |
| (defun gk-recompile (&optional force) | |
| "Recompile my configuration. | |
| If FORCE is non-nil, force compilation, i.e. compile even if | |
| up-to-date." | |
| (interactive "p") | |
| (if (member 'native-compile features) | |
| (let ((files (cons gk-elisp-site-dir | |
| (mapcar ($ (concat $1 ".el")) | |
| (remove-if-not #'file-exists-p gk-loaded-files))))) | |
| (native-compile-async files t)) | |
| (mapcar ($ (byte-recompile-file $1 (> force 1) 0)) | |
| (remove-if-not #'file-exists-p gk-loaded-files)) | |
| (byte-recompile-directory (locate-user-emacs-file "lisp/site") 0 (> force 4)))) | |
| ;;;; Footnotes: | |
| ;; Interact with plain-text footnotes. These are bound to keys and | |
| ;; mouse clicks later on in this file. | |
| (defun gk-find-text-footnote-definition () | |
| (interactive) | |
| (when (looking-at "[[(]?\\([0-9*]+\\)[\])]?") | |
| (push-mark (point)) | |
| (goto-char (point-max)) | |
| (re-search-backward (concat "^" (match-string 1) "[^1234567890]")))) | |
| (defun gk-find-text-footnote-definition--mouse (&optional event) | |
| "Find footnote definition according to plain text conventions." | |
| (interactive "@e") | |
| (when event (goto-char (cadadr event))) | |
| (gk-find-text-footnote-definition)) | |
| ;;;; Scripts: | |
| ;; These are functions to help with Unixy tasks, which act like shell | |
| ;; scripts. | |
| (defun gk-serve-directory (&optional dir port) | |
| (interactive (list (read-directory-name "Directory to serve: " | |
| default-directory) | |
| (read-number "Port: " 8000))) | |
| (let ((default-directory dir)) | |
| (async-shell-command (format "python2 -m SimpleHTTPServer %d" | |
| port)))) | |
| (defun gk-sudo (cmd) | |
| "Run CMD as superuser." | |
| (interactive (list (read-shell-command "Shell command (sudo): "))) | |
| (with-temp-buffer | |
| (cd (concat "/sudo::" (expand-file-name default-directory))) | |
| (prog1 | |
| (shell-command cmd (current-buffer)) | |
| (cd default-directory) | |
| (when (called-interactively-p 'any) | |
| ;; The command output can include ‘%’ which may cause message | |
| ;; to signal error. | |
| (message "%s" (buffer-string)))))) | |
| ;; Adapted from https://crowding.github.io/blog/2014/08/16/replace-less-with-emacs/ | |
| (defun gk-less--proc-sentinel (proc string) | |
| (ignore proc string)) | |
| (defun gk-less--postprocess (proc) | |
| (goto-char (point-min)) | |
| (cond | |
| ;; Man pages: | |
| ((save-excursion (search-forward "" nil t)) | |
| (Man-fontify-manpage)) | |
| ;; Diffs: | |
| ((save-excursion | |
| (and (looking-at "^diff") | |
| (re-search-forward "^---" nil t) | |
| (re-search-forward "^@@" nil t))) | |
| (diff-mode)) | |
| (:else | |
| (special-mode)))) | |
| (defun gk-less--proc-filter (proc string) | |
| (let ((buf (process-buffer proc)) | |
| (mark (process-mark proc))) | |
| (with-current-buffer buf | |
| (let ((buffer-read-only nil)) | |
| ;; make sure point stays at top of window while process output | |
| ;; accumulates | |
| (save-excursion | |
| (goto-char mark) | |
| (insert string) | |
| (ansi-color-filter-region mark (point)) | |
| (set-marker mark (point))) | |
| ;; Post-processing the buffer: | |
| (unless (process-live-p proc) | |
| (gk-less--postprocess proc)))))) | |
| (defun gk-less (fifo) | |
| "Companion function for ‘extras/eless.sh’." | |
| (let ((buf (generate-new-buffer "*pager*"))) | |
| (make-process | |
| :name "gk-pager" :buffer buf :command `("cat" ,fifo) | |
| :sentinel #'gk-less--proc-sentinel | |
| :filter #'gk-less--proc-filter) | |
| (display-buffer buf))) | |
| (setenv "PAGER" (locate-user-emacs-file "extras/eless.sh")) | |
| (defalias 'dmesg | |
| (defun gk-dmesg (&optional lines) | |
| (interactive "P") | |
| (async-shell-command (format "dmesg | tail -n %d" (or lines 10))))) | |
| (defun gk-screen-brightness (n) | |
| "Set screen brightness to N tenths of max. | |
| 10 >= N >= 1." | |
| (interactive | |
| (list (read-number "Brightness interval [1--10]: " 5))) | |
| (unless (>= 10 n 1) | |
| (user-error "Brightness interval not in range 10 >= N >= 1")) | |
| (with-current-buffer | |
| (find-file-noselect | |
| "/sudo::/sys/class/backlight/intel_backlight/brightness") | |
| (erase-buffer) | |
| (insert | |
| (number-to-string | |
| (* n | |
| (/ | |
| (string-to-number | |
| (with-temp-buffer | |
| (insert-file-contents | |
| "/sys/class/backlight/intel_backlight/max_brightness") | |
| (buffer-string))) | |
| 10)))) | |
| (save-buffer))) | |
| ;;;; Diff regions: | |
| ;; Diffing two regions. | |
| ;; Adapted from: https://gist.github.com/zdavkeos/1279865. | |
| ;; To compare two regions, select the first region and run | |
| ;; =gk-diff-region=. The region is now copied to a seperate diff-ing | |
| ;; buffer. Next, navigate to the next region in question (even in | |
| ;; another file). Mark the region and run =gk-diff-region-now=, the diff | |
| ;; of the two regions will be displayed by ediff. | |
| ;; You can re-select the first region at any time by re-calling | |
| ;; =gk-diff-region=. | |
| (defun gk-diff-region () | |
| "Select a region to compare" | |
| (interactive) | |
| (when (use-region-p) ; there is a region | |
| (let ((buf (get-buffer-create "*Diff-region A*"))) | |
| (with-current-buffer buf | |
| (erase-buffer)) | |
| (append-to-buffer buf (region-beginning) (region-end)))) | |
| (message "Now select other region to compare and run `diff-region-now`")) | |
| (defun gk-diff-region-now () | |
| "Compare current region with region already selected by `diff-region`" | |
| (interactive) | |
| (when (use-region-p) | |
| (let ((bufa (get-buffer-create "*Diff-region A*")) | |
| (bufb (get-buffer-create "*Diff-region B*"))) | |
| (with-current-buffer bufb | |
| (erase-buffer)) | |
| (append-to-buffer bufb (region-beginning) (region-end)) | |
| (ediff-buffers bufa bufb)))) | |
| ;;;; Illustrative Hex Colour Codes: | |
| ;; This section defines a face that can render hexadecimal colour | |
| ;; codes with the colour they denote as their background; and a | |
| ;; function to set it up meant for major mode hooks. | |
| ;; Adapted from http://www.emacswiki.org/emacs/HexColour. | |
| (defvar gk-hexcolour-keywords | |
| '(("#[abcdefABCDEF[:digit:]]\\{3,6\\}" | |
| (0 (let ((colour (match-string-no-properties 0))) | |
| (if (or (= (length colour) 4) | |
| (= (length colour) 7)) | |
| (put-text-property | |
| (match-beginning 0) | |
| (match-end 0) | |
| 'face (list :background (match-string-no-properties 0) | |
| :foreground | |
| (if (>= (apply | |
| '+ (x-color-values | |
| (match-string-no-properties 0))) | |
| (* (apply '+ (x-color-values "white")) .6)) | |
| "black" ; light bg, dark text | |
| "white" ; dark bg, light text | |
| ))))) | |
| append)))) | |
| (defun gk-hexcolour-add-to-font-lock () | |
| (interactive) | |
| (font-lock-add-keywords nil gk-hexcolour-keywords t)) | |
| ;;;; Testing init file: | |
| ;; This section provides a command, ‘gk-test-init’, for | |
| ;; uninteractively loading emacs configuration in a subprocess and | |
| ;; seeing if it does indeed load. | |
| (defconst gk-emacs-executable | |
| (executable-find "emacs")) | |
| (defvar gk-load-test-file | |
| (expand-file-name (locate-user-emacs-file "etc/load-test.el"))) | |
| (defvar gk-load-test-output-buffer-name | |
| "*Startup File Test*") | |
| (defvar gk-load-test-process-name | |
| "*Startup Test Process*") | |
| (defun gk-test-init () | |
| (interactive) | |
| (compile (mapconcat | |
| #'identity | |
| (list gk-emacs-executable "-Q" "--batch" "-l" gk-load-test-file) | |
| " "))) | |
| ;;;; Utility macros: | |
| ;; Some lisp macros for this file. | |
| (defmacro when-fbound (proc &rest args) | |
| "Run proc if bound. | |
| \(when-fbound PROC ARGS...)" | |
| `(when (fboundp (quote ,proc)) | |
| (,proc ,@args))) | |
| (defmacro gk-interactively (&rest body) | |
| "Wrap the BODY in an interactive lambda form. | |
| Return the lambda. It has as its sole argument a catch-all ‘_’." | |
| `(lambda (&rest _) | |
| ,(if (stringp (car body)) | |
| (pop body) | |
| "Not documented.") | |
| (interactive) | |
| ,@body)) | |
| (defmacro gk-with-new-frame (parameters &rest body) | |
| "Create a new frame and run BODY in it. | |
| PARAMETERS is passed to ‘make-frame’. | |
| The new frame is bound to the lexically scoped variable | |
| ‘new-frame’ inside BODY. | |
| The newly created frame is centred and the mouse pointer is put | |
| at the centre of the newly created frame. This only happens when | |
| ‘display-graphic-p’ is truthy." | |
| (declare (indent defun)) | |
| (let ((frame (gensym))) | |
| `(let ((,frame (make-frame ,parameters))) | |
| (raise-frame ,frame) | |
| (select-frame-set-input-focus ,frame) | |
| (select-window (frame-first-window ,frame)) | |
| (when (display-graphic-p) | |
| ;; Center frame | |
| (set-frame-position | |
| ,frame | |
| (/ (- (x-display-pixel-width) (window-pixel-width)) 2) | |
| ;; XXX(2020-09-15): for some reason this works better than | |
| ;; dividing by 2 on my Linux Mint 20 with Cinnamon. | |
| (floor (/ (- (x-display-pixel-height) (window-pixel-height)) 2.5))) | |
| ;; Move mouse into the new frame | |
| (set-mouse-absolute-pixel-position | |
| (/ (x-display-pixel-width) 2) | |
| (/ (x-display-pixel-height) 2))) | |
| (let ((new-frame ,frame)) ,@body)))) | |
| (defmacro setc (variable value) | |
| "Exactly like setq, but handles custom." | |
| `(funcall (or (get ',variable 'custom-set) 'set-default) ',variable ,value)) | |
| ;;;; Association lists: | |
| ;; Helper functions for association lists. | |
| (defun dissoc (key list &optional test-fn) | |
| "Delete pairs whose car is equal to KEY from LIST. | |
| TEST-FN defaults to ‘equal’." | |
| (dissoc--1 key list (or test-fn #'equal) nil)) | |
| (defun dissoc--1 (key list test-fn arg) | |
| (let ((p (car list)) | |
| (r (cdr list))) | |
| (if list | |
| (if (funcall test-fn (car p) key) | |
| (dissoc--1 key r test-fn arg) | |
| (dissoc--1 key r test-fn (append arg (list p)))) | |
| arg))) | |
| (defmacro dissoc! (key sym test-fn) | |
| "Call ‘dissoc’ with args and set SYM to result." | |
| `(setq ,sym (dissoc ,key ,sym ,test-fn))) | |
| (defun gk-assoca (keyseq list) | |
| "Arbitrary depth multi-level alist query. | |
| KEYSEQ is the list of keys to look up in the LIST. The first key | |
| from KEYSEQ is looked up in the LIST, then the next key from | |
| KEYSEQ is looked up in the CDR of the return value of that | |
| operation, and so on until all the KEYSEQ is exhausted. The | |
| resultant value is returned, or nil, in case one or more keys are | |
| not found in the LIST. | |
| If KEYSEQ is a symbol, then it's treated as if it were a | |
| singleton list." | |
| (let ((ks (if (listp keyseq) keyseq (list keyseq))) | |
| (ret list)) | |
| (dolist (k ks ret) | |
| (setq ret (cdr (assoc k ret)))))) | |
| (define-obsolete-function-alias 'assoca 'gk-assoca "2021-10-14") | |
| ;;;; Global modes: | |
| ;; This module provides utilities for global modes, like turning them on | |
| ;; and off collectively with a single command, registering and | |
| ;; unregistering them, disabling default modes etc. | |
| ;; All the modes listed in =gk-global-modes= are toggled on with an | |
| ;; =after-init-hook=, so modifications to this variable that happen up | |
| ;; until the execution of the named hook will actually determine which | |
| ;; modes are turned on. | |
| ;; =gk-disabled-modes= is a list of modes to disable. | |
| ;; Each of this lists contain symbols, actually =*-mode= functions. The | |
| ;; ones in the former will be called with =+1= as the argument, and ones | |
| ;; in the latter with =-1=. | |
| ;; Do not use this as a hook, add to =after-init-hook= instead. | |
| (defvar gk-global-modes nil "List of global modes to be enabled.") | |
| (defvar gk-disabled-modes nil "List of disabled global modes.") | |
| (defvar gk-toggle-global-modes nil) | |
| (defun gk-toggle-global-modes () | |
| "Enable or disable the modes listed in `gk-global-modes' at once." | |
| (interactive) | |
| (setf gk-toggle-global-modes (not gk-toggle-global-modes)) | |
| (let (errors) | |
| ;; Enable global modes | |
| (dolist (mode gk-global-modes) | |
| (condition-case e | |
| (funcall mode (if gk-toggle-global-modes 1 -1)) | |
| (error (push `(,mode ,e) errors)))) | |
| ;; Disable modes in gk-disabled-modes | |
| (dolist (mode gk-disabled-modes) | |
| (condition-case e | |
| (funcall mode (if gk-toggle-global-modes -1 1)) | |
| (error (push `(,mode ,e) errors)))) | |
| (when errors | |
| (warn "Following errors occurred when activating global modes:\n%S" | |
| errors)))) | |
| (add-hook 'after-init-hook 'gk-toggle-global-modes) | |
| ;;;; Things: | |
| ;; In this section are defined a suite of functions to work with | |
| ;; ‘things’ in buffers, à la ‘thing-at-point’. | |
| (defmacro gk-make-thing-marker (thing) | |
| (let ((thingname (symbol-name thing))) | |
| `(defun ,(intern (concat "gk-mark-" thingname)) () | |
| ,(concat "Mark the " thingname " under cursor.") | |
| (interactive) | |
| (let ((b (bounds-of-thing-at-point (quote ,thing)))) | |
| (set-mark (point)) | |
| (goto-char (car b)) | |
| (push-mark (cdr b) t t))))) | |
| (defvar gk-things '(list sexp defun filename url email word paragraph | |
| sentence whitespace line page symbol) | |
| "A list of known things") | |
| (dolist (thing gk-things) | |
| (eval `(gk-make-thing-marker ,thing))) | |
| (defun gk-mark-thing () | |
| "Interactively find some THING to mark." | |
| (interactive) | |
| (funcall | |
| (intern | |
| (concat | |
| "gk-mark-" | |
| (completing-read | |
| "What to mark (hit TAB to complete): " | |
| (mapcar #'symbol-name gk-things) | |
| nil t))))) | |
| ;;;; Projects: | |
| ;; Functionality for opening and working with projects. | |
| (defvar gk-projects-directory (expand-file-name "~/co") | |
| "Directory where software projects are located.") | |
| (defvar gk-projects-use-eshell nil | |
| "Whether to use ‘eshell’ for project shells. | |
| If nil, use ‘shell’ instead.") | |
| (defvar gk-project-compile--hist nil) | |
| (defvar gk-project-compile-default-command "make test" | |
| "Default command for ‘gk-project-compile’.") | |
| (defun gk-project-compile (command) | |
| (interactive | |
| (list | |
| (read-shell-command | |
| "Run project compile command: " | |
| gk-project-compile-default-command | |
| gk-project-compile--hist))) | |
| (if-let* ((projbuf (get-buffer (gk-assoca 'gk-project (frame-parameters))))) | |
| (with-current-buffer projbuf | |
| (compile command)) | |
| (user-error "Not a project frame"))) | |
| (defun gk-create-project (name vcs parent-tree) | |
| "Create a new project. | |
| NAME is the project name, and the project path is located in the | |
| directory at PARENT-TREE + NAME. PARENT-TREE defaults to | |
| ‘gk-projects-directory’. | |
| If VCS is non-nil (and the name of a version control system | |
| included in ‘vc-handled-backends’), a new repository with the | |
| selected VCS is initialised under the new project directory. | |
| The value of NAME is used directly in the project directory name, | |
| so make sure it does not include unnecessary slashes or | |
| problematic characters." | |
| (interactive (list (read-string "Project name (will be project path basename): ") | |
| (vc-read-backend "VCS, empty for none: ") | |
| (read-directory-name "Parent directory for project subtree: " | |
| (concat gk-projects-directory "/")))) | |
| (let ((project-tree (expand-file-name name parent-tree))) | |
| (condition-case e | |
| (make-directory project-tree) | |
| ('file-already-exists (message (apply #'format "%s: %s" (cdr e))))) | |
| (when vcs | |
| (let ((default-directory project-tree)) | |
| (vc-create-repo vcs))) | |
| (gk-open-project project-tree))) | |
| (defun gk-open-project (path &optional use-this-frame) | |
| "Open a project folder. | |
| Dired buffer to the left, magit (or VC if not git) to the | |
| right. Start a shell with name ‘*XXX shell*’ where XXX is the | |
| basename of the PATH. | |
| PATH is the path to the project. | |
| If USE-THIS-FRAME is non-nil, or called interactively with a | |
| non-zero prefix argument, use the current frame, instead of | |
| creating a new one." | |
| (interactive | |
| (list | |
| (f-slash | |
| (read-directory-name | |
| (if current-prefix-arg | |
| "Project to open (*in _current_ frame*): " | |
| "Project to open (in new frame): ") | |
| (f-slash (expand-file-name "~")) | |
| nil t)) | |
| (not (not current-prefix-arg)))) | |
| (let* ((vcs | |
| (cond | |
| ((and | |
| (fboundp 'magit-status) | |
| (file-exists-p (expand-file-name ".git" path))) | |
| #'magit-status) | |
| ((or (mapcar #'vc-backend (gk-directory-files path))) | |
| #'vc-dir))) | |
| ;; This should be fairly duplicate-proof... | |
| (project-name (concat | |
| (user-login-name) | |
| "@" | |
| (system-name) | |
| ":" | |
| ;; remove trailing slash(es) | |
| (replace-regexp-in-string "/+\\'" "" path))) | |
| (shell-name (format "*%s shell*" project-name)) | |
| (frame-params `((fullscreen . maximized) | |
| (gk-project . ,project-name) | |
| (gk-project-dir . ,path) | |
| (gk-project-shell . ,shell-name) | |
| (gk-project-vcs . ,vcs)))) | |
| (cond (use-this-frame | |
| (pcase-dolist (`(,param . ,val) frame-params) | |
| (set-frame-parameter nil param val)) | |
| (gk--open-project-1 vcs path shell-name)) | |
| (t | |
| (gk-with-new-frame frame-params | |
| (gk--open-project-1 vcs path shell-name)))))) | |
| (defun gk--open-project-1 (vcs path shell-name) | |
| "Subroutine of ‘gk-open-project’." | |
| (delete-other-windows) | |
| (dired path) | |
| (split-window-sensibly) | |
| (other-window 1) | |
| (funcall vcs path)) | |
| (defun gk-frame-parameters () | |
| "Get my frame parameters." | |
| (cl-remove-if-not | |
| ($ (s-starts-with? "gk-" (symbol-name (car $1)))) | |
| (frame-parameters))) | |
| ;; Popup shell: | |
| (defun gk--get-shell-for-frame (&optional arg-for-shell frame) | |
| "Get a shell for current frame, depending on whether it’s a project frame. | |
| Subroutine for ‘gk-pop-shell’ and ‘gk-display-shell’." | |
| (save-window-excursion | |
| (let* ((prefix-arg arg-for-shell) | |
| (project-shell (frame-parameter frame 'gk-project-shell)) | |
| (eshell-buffer-name (or project-shell | |
| eshell-buffer-name)) | |
| (default-directory (or (frame-parameter frame 'gk-project-dir) | |
| default-directory))) | |
| (if gk-projects-use-eshell | |
| (eshell) | |
| (shell project-shell))))) | |
| (defun gk-pop-shell (arg) | |
| "Pop a shell in a side window. | |
| Pass arg to ‘shell’. If already in a side window that displays a | |
| shell, toggle the side window. | |
| If there is a project shell associated to the frame, just show | |
| that instead." | |
| (interactive "P") | |
| (if (and (gk-assoca 'window-side (window-parameters)) | |
| (equal major-mode | |
| (if gk-projects-use-eshell | |
| 'eshell-mode | |
| 'shell-mode))) | |
| (window-toggle-side-windows) | |
| (when-let* ((win (display-buffer-in-side-window | |
| (gk--get-shell-for-frame arg) | |
| '((side . bottom))))) | |
| (select-window win)))) | |
| ;; Home view | |
| (defun gk-home () | |
| "Take me to the home view." | |
| (interactive) | |
| ;; Close side windows off first because they can’t be the only | |
| ;; window. | |
| (when (window-with-parameter 'window-side) | |
| (window-toggle-side-windows)) | |
| (delete-other-windows) | |
| (if (gk-assoca 'gk-project-shell (frame-parameters)) | |
| (let* ((fparam (frame-parameters)) | |
| (vcs (gk-assoca 'gk-project-vcs fparam)) | |
| (dir (gk-assoca 'gk-project-dir fparam))) | |
| (dired dir) | |
| (split-window-sensibly) | |
| (other-window 1) | |
| (funcall vcs dir)) | |
| (other-window 1) | |
| (if initial-buffer-choice | |
| (ignore-errors (find-file initial-buffer-choice)) | |
| (switch-to-buffer "*scratch*")) | |
| (gk-flash-current-line))) | |
| ;;;; i3wm: | |
| (defun gk-i3wm-get-current-workspace-id () | |
| "Return focused workspace number and name as a cons cell." | |
| (let* ((workspaces | |
| (with-temp-buffer | |
| (call-process "i3-msg" nil t nil "-t" "get_workspaces") | |
| (goto-char (point-min)) | |
| (json-parse-buffer))) | |
| (focused (seq-filter ($ (eq t (gethash "focused" $1))) | |
| workspaces))) | |
| (unless (eq 1 (length focused)) | |
| (error "Unreachable state: multiple focused workspaces")) | |
| (cons (gethash "num" (car focused)) | |
| (gethash "name" (car focused))))) | |
| ;;;; Reading setup: | |
| (defvar gk-reading-modes | |
| '(doc-view-mode pdf-view-mode eww-mode) | |
| "Modes more likely to be used for reading documents. | |
| Used by ‘gk-reading-setup’ in order to select a buffer that | |
| contains a document to be read in a smart manner so that there is | |
| no need to switch windows unnecessarily.") | |
| (defvar gk-reading-notes-file | |
| (expand-file-name "~/Notes/Reading.org") | |
| "Default reading notes file. | |
| Used in ‘gk-reading-setup’.") | |
| (defun gk-reading-setup () | |
| "Put windows into a reading setup. | |
| Try to find a potential window containing a document to be read | |
| \(see ‘gk-reading-modes’), give it a big window, and open | |
| ‘gk-reading-notes-file’ in a smaller window below it." | |
| (interactive) | |
| ;; Find a suitable window which probably contains the document I | |
| ;; want to read. If not found, the current window will be used. | |
| (pcase (cl-remove-if-not | |
| ($ (memq (buffer-local-value 'major-mode $1) | |
| gk-reading-modes)) | |
| (mapcar #'window-buffer (window-list))) | |
| ((or `(,buffer) | |
| `(,buffer . ,buffers)) | |
| (select-window | |
| (display-buffer-reuse-window buffer '((reusable-frames . nil)))))) | |
| (delete-other-windows) | |
| (display-buffer-below-selected | |
| (find-file-noselect gk-reading-notes-file) | |
| `((window-height . ,(/ (window-height) 3)))) | |
| ;; XXX(2020-04-02): if point remains in the ‘pdf-view-mode’ window, | |
| ;; ‘pdf-view-mode’ behaves funny. It can be remedied via calling | |
| ;; | |
| ;; (other-window 1) | |
| ;; (redisplay t) | |
| ;; (other-window 1) | |
| ;; | |
| ;; but having focus on ‘gk-reading-notes-file’ is both simpler and | |
| ;; kinda more logical (if reading a new document I’d probably set up | |
| ;; the notes buffer first. | |
| (other-window 1)) | |
| ;;;; Screenshots: | |
| ;; Adapted from: https://www.reddit.com/r/emacs/comments/idz35e/g2c2c6y/ | |
| (defvar gk-save-screenshot-dir | |
| (expand-file-name | |
| "emacs-screenshots" | |
| (if (fboundp 'xdg-user-dir) | |
| (xdg-user-dir "PICTURES") | |
| (expand-file-name "~/Pictures"))) | |
| "Where to save screenshots.") | |
| (defvar gk-save-screenshot-default-output-file-name-template | |
| "%F%T%z.png" | |
| "Default basename template for ‘gk-save-screenshot’. | |
| This string is passed to ‘format-time-string’ and then | |
| concatenated to ‘gk-save-screenshot-dir’ using | |
| ‘expand-file-name’.") | |
| (defun gk-save-screenshot (output-file) | |
| "Save a screenshot of the selected frame as an SVG image. | |
| Save the output to OUTPUT-FILE. When called interactively, this | |
| is read from the minibuffer, and a default value using the date | |
| and time is provided. | |
| Output file type is inferred from OUTPUT-FILE’s extension, which | |
| must be one of ‘svg’, ‘pdf’, ‘ps’, ‘png’. | |
| The default file path is constructed using | |
| ‘gk-save-screenshot-dir’ to determine the directory to save and | |
| ‘gk-save-screenshot-default-output-file-name-template’ to | |
| generate a default file name." | |
| (interactive | |
| (list (read-file-name | |
| "Screenshot file name: " | |
| gk-save-screenshot-dir | |
| nil nil | |
| (format-time-string | |
| gk-save-screenshot-default-output-file-name-template)))) | |
| ;; deps check | |
| (unless (fboundp 'x-export-frames) | |
| (user-error | |
| "This function depends on `x-export-frames’ which not available")) | |
| ;; ensure output directory | |
| (unless (file-directory-p gk-save-screenshot-dir) | |
| (make-directory gk-save-screenshot-dir t)) | |
| ;; guess output file type | |
| (let ((type (downcase (file-name-extension output-file))) | |
| data) | |
| (setq type | |
| (cond ((string= type "svg") 'svg) | |
| ((string= type "pdf") 'pdf) | |
| ((string= type "ps") 'postscript) | |
| ((string= type "png") 'png) | |
| (t | |
| (user-error | |
| "Output file’s extension should be one of svg, pdf, ps or png" | |
| type)))) | |
| ;; write data | |
| (setq data (x-export-frames nil type)) | |
| (with-temp-file output-file | |
| (insert data)) | |
| (kill-new output-file) | |
| (message output-file))) | |
| ;;;; Window layouts: | |
| (defun gk-layouts-3col () | |
| "Three column layout. | |
| Tries to preserve the order of window buffers and active window." | |
| (interactive) | |
| ;; Record active window buffer. | |
| (let ((cbuf (current-buffer))) | |
| ;; Switch to leftmost window. | |
| (ignore-errors (cl-loop do (windmove-left))) | |
| (let ((buffers | |
| (mapcar #'window-buffer (-take 3 (window-list)))) | |
| (width (/ (frame-width) 3))) | |
| (delete-other-windows) | |
| (split-window-horizontally width) | |
| (other-window 1) | |
| (split-window-horizontally) | |
| (other-window -1) | |
| (dolist (b buffers) | |
| (switch-to-buffer b) | |
| (other-window 1))) | |
| ;; Switch to previously visible buffer’s window. | |
| (select-window (get-buffer-window cbuf)))) | |
| (defun gk-layouts-3col-and-follow () | |
| "Call ‘gk-layouts-3col’ and then ‘follow-mode’." | |
| (interactive) | |
| ;; Only extend the current window. | |
| (delete-other-windows) | |
| (gk-layouts-3col) | |
| ;; Go to leftmost window, ‘windmove-left’ will signal on leftmost | |
| ;; window. | |
| (ignore-errors (while t (windmove-left))) | |
| (follow-mode)) | |
| (defun gk-layouts-main-and-sidekicks (&optional arg) | |
| "One horizontal split, the right window split in two. | |
| Tries to preserve the order of window buffers and active window. | |
| If ARG is non-nil, or if called with a prefix argument, the left | |
| column will be split into two instead." | |
| (interactive "P") | |
| ;; Record active window buffer. | |
| (let ((cbuf (current-buffer))) | |
| ;; Switch to leftmost window. | |
| (ignore-errors (cl-loop do (windmove-left))) | |
| (let ((buffers | |
| (mapcar #'window-buffer (-take 3 (window-list))))) | |
| (delete-other-windows) | |
| (split-window-horizontally) | |
| (unless arg (other-window 1)) | |
| (split-window-vertically) | |
| (other-window -1) | |
| (dolist (b buffers) | |
| (switch-to-buffer b) | |
| (other-window 1))) | |
| ;; Switch to previously visible buffer’s window. | |
| (select-window (get-buffer-window cbuf)))) | |
| ;;; The GK minor mode: | |
| ;; The GK minor mode is at the heart of this configuration. Almost | |
| ;; all keybindings, except unmapping some keys from the global map, | |
| ;; and except bindings in specific modes, should be done with this | |
| ;; minor modes keymap. This minor mode is active everywhere, except | |
| ;; the Minibuffer and the Fundamental mode buffers. | |
| (defgroup GK nil | |
| "Group for my configuration." | |
| :group 'emacs | |
| :prefix "gk-") | |
| (defvar gk-minor-mode-map | |
| (make-sparse-keymap) | |
| "Where to put all my bindings.") | |
| (defvar gk-minor-mode-prefix-map | |
| (make-sparse-keymap) | |
| "Prefix map for my bindings.") | |
| (fset 'gk-minor-mode-prefix-map gk-minor-mode-prefix-map) | |
| (defvar gk-minor-mode-prefix "\C-c" | |
| "Keymap prefix for `gk-minor-mode'.") | |
| (define-minor-mode gk-minor-mode | |
| "Global minor mode for customisations. | |
| \\{gk-minor-mode-map}" | |
| nil "" gk-minor-mode-map | |
| (let ((map gk-minor-mode-map)) | |
| (define-key map gk-minor-mode-prefix #'gk-minor-mode-prefix-map))) | |
| (define-globalized-minor-mode global-gk-minor-mode gk-minor-mode | |
| gk-minor-mode) | |
| ;;; Customisations: | |
| ;;;; Fonts: | |
| ;; Default fonts to use in this config. | |
| (defconst gk-default-fonts-plist | |
| (list :serif "DejaVu Serif Condensed" | |
| :sans "DejaVu Sans Condensed" | |
| :mono "Iosevka Cadadrish Sans" | |
| :cjk "Noto Serif CJK JP" | |
| :emoji "Noto Color Emoji" | |
| :forecast-moon-phase (or (and (gk-gui-p) | |
| (font-info "Quivira") | |
| "Quivira") | |
| "DejaVu Sans")) | |
| "A plist, default fonts.") | |
| ;; Set up so that there's 80-85 chars width for half-sized horizontal | |
| ;; windows. | |
| (defconst gk-font-default-height 110) | |
| (defconst gk-font-variable-pitch-height 110) | |
| (defun gk-font (type) | |
| "Get default font for TYPE, a keyword. | |
| nil if absent." | |
| (plist-get gk-default-fonts-plist type)) | |
| ;;;; Outline: | |
| ;; Utility function for setting up outline minor mode. | |
| (defun gk-turn-on-outline-minor-mode (headline-begin headline-end prefix) | |
| "Turn on the `outline-minor-mode'. | |
| Set locally the variable `outline-regexp' to HEADLINE-BEGIN. | |
| Set locally the variable `outline-heading-end-regexp' to HEADLINE-END. | |
| Set locally the variable `outline-minor-mode-prefix' to PREFIX." | |
| (setq-local outline-regexp headline-begin) | |
| (setq-local outline-heading-end-regexp headline-end) | |
| (setq-local outline-minor-mode-prefix (kbd prefix)) | |
| (outline-minor-mode +1) | |
| (local-set-key outline-minor-mode-prefix outline-mode-prefix-map)) | |
| ;; Mainly for ‘C-c C-u’ in Org mode. | |
| (define-advice outline-up-heading | |
| (:around (fn &rest args) previous-heading-on-toplevel) | |
| "Move to previous heading if at toplevel." | |
| (condition-case e | |
| (call-interactively fn) | |
| ('error (org-previous-visible-heading (car args))))) | |
| ;;;; Backups: | |
| ;; This section sets up file backups created when editing. Backups | |
| ;; are put in a designated directory, and are made generously. Better | |
| ;; safe than sorry. | |
| (setf | |
| ;; Make a backup of a file the first time it is saved. | |
| make-backup-files t | |
| ;; Make backup first, then copy to the original. | |
| backup-by-copying nil | |
| ;; Version-numbered backups. | |
| version-control t | |
| ;; Keep a lot of copies. Only not version-controlled files (see | |
| ;; ‘vc-make-backup-files’. | |
| kept-old-versions 10000 | |
| kept-new-versions kept-old-versions | |
| backup-directory-alist | |
| `(("/ssh:.*" . ".") | |
| ("." . ,(expand-file-name "~/.backups")))) | |
| ;;;; Comint: | |
| ;; Settings for interpreter buffers. | |
| ;;;;; Common: | |
| ;; Settings and keybindings common to all comint buffers. | |
| ;; DEATH TO ALL FUCKING COLOURS! | |
| ;; FUCK ALL COLOURS! | |
| ;; THANK GOD FOR THIS VARIABLE! | |
| ;; THANK YOU WHOMEVER ADDED THIS! | |
| ;; NOW COLOUR WHATEVER THE FUCK YOU WANT YOU BRAIN | |
| ;; DEAD DEVELOPERS OF COMMAND LINE UTILITIES WITH | |
| ;; COLOURFUL OUTPUT THAT CANNOT BE DISABLED! | |
| ;; FUCK YOU ALL. | |
| (setf ansi-color-for-comint-mode 'filter) | |
| (defun gk-toggle-comint-process-echoes () | |
| "Toggle ‘comint-process-echoes’ variable." | |
| (interactive) | |
| (message | |
| (if (setq-local comint-process-echoes (not comint-process-echoes)) | |
| "Turned *on* comint echo filter." | |
| "Turned *off* comint echo filter."))) | |
| (define-key comint-mode-map "\C-c\C-e" 'gk-toggle-comint-process-echoes) | |
| (define-key comint-mode-map (kbd "C-c DEL") 'comint-clear-buffer) | |
| ;;;;; Shell mode: | |
| ;; Adapted from: https://www.emacswiki.org/emacs/ShellDirtrackByProcfs | |
| (defun gk-procfs-dirtrack (str) | |
| (prog1 str | |
| (when (string-match comint-prompt-regexp str) | |
| (let ((directory (file-symlink-p | |
| (format "/proc/%s/cwd" | |
| (process-id | |
| (get-buffer-process | |
| (current-buffer))))))) | |
| (when (file-directory-p directory) | |
| (cd directory)))))) | |
| (define-minor-mode gk-procfs-dirtrack-mode | |
| "Track shell directory by inspecting procfs." | |
| nil nil nil | |
| (cond (gk-procfs-dirtrack-mode | |
| (when (bound-and-true-p shell-dirtrack-mode) | |
| (shell-dirtrack-mode 0)) | |
| (when (bound-and-true-p dirtrack-mode) | |
| (dirtrack-mode 0)) | |
| (add-hook 'comint-preoutput-filter-functions | |
| 'gk-procfs-dirtrack nil t)) | |
| (t | |
| (remove-hook 'comint-preoutput-filter-functions | |
| 'gk-procfs-dirtrack t)))) | |
| (defun gk-display-shell (arg) | |
| "Pop a shell in a side window. | |
| Pass arg to ‘shell’. If already in a side window that displays a | |
| shell, toggle the side window. | |
| If there is a project shell associated to the frame, just show | |
| that instead." | |
| (interactive "P") | |
| (display-buffer (gk--get-shell-for-frame arg))) | |
| (defun gk-shell-mode-hook () | |
| "Hook for `shell-mode'." | |
| ;; BSD /bin/sh echoes. | |
| (when (and (not (memq system-type '(gnu gnu/linux gnu/kfreebsd))) | |
| (string-match "/k?sh$" (getenv "SHELL"))) | |
| (setq-local comint-process-echoes t)) | |
| ;; Compilation shell minor mode activates certain parts of command | |
| ;; output as clickable links to parts of files (e.g. grep -Hn). | |
| (compilation-shell-minor-mode 1) | |
| ;; ‘shell-dirtrack-mode’ fails a lot. | |
| (shell-dirtrack-mode +1) | |
| ;; (gk-procfs-dirtrack-mode +1) | |
| ) | |
| (add-hook 'shell-mode-hook 'gk-shell-mode-hook) | |
| ;;;;; Eshell: | |
| (setf | |
| eshell-ls-initial-args | |
| (list "--group-directories-first" "-Fh")) | |
| (dolist (key '(up down left right)) | |
| (define-key eshell-hist-mode-map `[,key] nil)) | |
| (define-key eshell-hist-mode-map (kbd "M-p") #'eshell-previous-matching-input-from-input) | |
| (define-key eshell-hist-mode-map (kbd "M-n") #'eshell-next-matching-input-from-input) | |
| ;;;; Dired: | |
| (setf | |
| ;; Show ls switches in modeline | |
| dired-switches-in-mode-line 'as-is) | |
| ;;;;; The hook: | |
| (defun gk-dired-hook () | |
| "Main hook for `dired-mode'." | |
| ;; C-x M-o -> toggle omitting | |
| ;; * O -> mark omitted | |
| (dired-omit-mode 1) | |
| (dired-hide-details-mode 1)) | |
| (add-hook 'dired-mode-hook #'gk-dired-hook) | |
| ;;;;; Utilities: | |
| (defun gk-dired-copy-marked-file-paths-as-kill (&optional arg) | |
| "Copy the paths of marked files into the kill ring as one big string. | |
| The string is space separated, ready for use in shell. | |
| If ARG is non-nil, or one prefix arg is given, place each file | |
| in single quotes. | |
| If two prefix arguments are given, place each file in double | |
| quotes. | |
| If called with prefix arg 0 (zero), return a null-separated list | |
| instead of space separated. | |
| If called with a negative prefix arg, return a comma-separated | |
| list. | |
| If called with three prefix args, return a colon separated list." | |
| (interactive "p") | |
| (let ((str (mapconcat | |
| (case arg | |
| ((1 0 -1 64) #'identity) | |
| ('4 ($ (concat "'" $1 "'"))) | |
| ('16 ($ (concat "\"" $1 "\"")))) | |
| (dired-get-marked-files) | |
| (case arg | |
| ((1 4 16 nil) " ") | |
| ('0 " |