;; -*- lexical-binding: t -*- ;; Packages and Custom initialization ;; Letting Custom run *before* initializing packages seems to result ;; in packages resetting some of their variables, eg page-break-lines ;; resets global-page-break-lines-mode to nil. Cue Custom shrugging, ;; "changed outside Customize". ;; NB: starting from Emacs 27, package-initialize is automatically ;; called before loading the user's init file, unless ;; package-enable-at-startup is set to nil in the early init file. (when (< emacs-major-version 27) (package-initialize)) (setq custom-file "~/.emacs-custom.el") (load custom-file) ;; Key bindings ;; C-h is a special snowflake in many situations; this is the most ;; reliable way I found to consistently get C-h to do what DEL does. ;; ;; Likewise, C-M-h is re-bound by some major modes (CC, Python, Perl), ;; so this is the simplest way I know of to make sure C-M-h sticks as ;; "backward-kill-word". ;; ;; Same story with M-h (mark-paragraph) which gets re-bound by eg ;; markdown-mode and nxml-mode. ;; ;; NB: help and mark-defun are still accessible using H instead of h, ;; except in a terminal. (define-key input-decode-map (kbd "C-h") (kbd "DEL")) (define-key input-decode-map (kbd "C-M-h") (kbd "M-DEL")) (global-set-key (kbd "C-x C-b") 'ibuffer) ;; Hopefully these will be easier to remember than TeX commands: (require 'quail) (quail-define-package "my/input-method" "symbols" "𝒰" t "Input arbitrary symbols with other arbitrary symbols.") (mapc (lambda (item) (quail-defrule (car item) (cdr item) "my/input-method")) (list ;; Punctuation '("..." ?…) ;; Math symbols '("~~" ?β‰ˆ) '("~~_" ?β‰Š) '("~=" ?β‰…) '("~_" ?≃) '("=_" ?≑) '("^=" ?≙) '(":=" ?≔) '("-->" ?β†’) '("-/>" ?↛) '("==>" ?β‡’) '("=/>" ?⇏) '("<--" ?←) '("" ?↔) '("<=>" ?⇔) ;; Emojis '("\\o/" ?πŸ™Œ) '("\\m/" ?🀘) ;; Pictograms '("/!\\" ?⚠))) ;; C-c [[:alpha:]] is reserved for users - let's make good use of it. (defun my/make-toggle-input-method (input-method) (lambda () (:documentation (format "Toggle `%s' input method." input-method)) (interactive) ;; `current-input-method' is a string; if INPUT-METHOD is a ;; symbol, neither eq, eql nor equal would return t. (if (string= current-input-method input-method) (deactivate-input-method) (set-input-method input-method t)))) (defun my/set-tab-width (&optional arg) (interactive "P") (let ((new-width (cond (arg (prefix-numeric-value arg)) ((= tab-width 4) 8) (4))) (old-width tab-width)) ;; TODO: for some reason, set-variable takes effect immediately, ;; but setq(-local)? do not: I need to move the cursor before tabs ;; are re-drawn. (set-variable 'tab-width new-width) (message "changed from %s to %s" old-width new-width))) (defun my/kill-ring-filename () (interactive) (kill-new (or (buffer-file-name) default-directory))) (defun my/kill-ring-pipe-region (command) (interactive (list (read-shell-command "Shell command on region: "))) (let ((input (funcall region-extract-function nil))) (with-temp-buffer (insert input) (call-process-region (point-min) (point-max) shell-file-name t t nil shell-command-switch command) (kill-new (buffer-string))))) (defun my/kill-ring-shell (command) (interactive (list (read-shell-command "Shell command: "))) (with-temp-buffer (call-process-shell-command command nil t) (kill-new (buffer-string)))) (defun my/make-project-wide (f) "Make a function which will run F from the project's root directory." (lambda () (:documentation (format "Run `%s' from the project's root directory." f)) (interactive) (let ((default-directory (my/project-root))) (call-interactively f)))) (global-set-key (kbd "C-c c") 'compile) (global-set-key (kbd "C-c e f") 'auto-fill-mode) (global-set-key (kbd "C-c i t") (my/make-toggle-input-method 'TeX)) (global-set-key (kbd "C-c i u") (my/make-toggle-input-method 'my/input-method)) (global-set-key (kbd "C-c k f") 'my/kill-ring-filename) (global-set-key (kbd "C-c k |") 'my/kill-ring-pipe-region) (global-set-key (kbd "C-c k !") 'my/kill-ring-shell) (global-set-key (kbd "C-c m") 'man) (global-set-key (kbd "C-c p c") (my/make-project-wide 'compile)) (global-set-key (kbd "C-c p !") (my/make-project-wide 'shell-command)) (global-set-key (kbd "C-c p &") (my/make-project-wide 'async-shell-command)) (global-set-key (kbd "C-c p f") 'project-find-file) (global-set-key (kbd "C-c t") 'toggle-truncate-lines) (global-set-key (kbd "C-c v") 'visual-line-mode) (global-set-key (kbd "C-c w c") 'whitespace-cleanup) (global-set-key (kbd "C-c w f") 'page-break-lines-mode) (global-set-key (kbd "C-c w m") 'whitespace-mode) (global-set-key (kbd "C-c w t") 'my/set-tab-width) (rg-enable-default-bindings) ; Uses the C-c s prefix. ;; What's life without a little risk? (setq disabled-command-function nil) ;; Window management (when window-system (load-theme 'eighters t) ;; Bindings ala Terminator (global-set-key [C-tab] 'other-window) (global-set-key (kbd "C-S-o") 'split-window-below) (global-set-key (kbd "C-S-e") 'split-window-right) (global-set-key (kbd "C-+") 'text-scale-adjust) (global-set-key (kbd "C--") 'text-scale-adjust) (global-set-key (kbd "C-0") 'text-scale-adjust) (global-set-key (kbd "C-S-") 'enlarge-window) (global-set-key (kbd "C-S-") 'shrink-window) (global-set-key (kbd "C-S-") 'enlarge-window-horizontally) (global-set-key (kbd "C-S-") 'shrink-window-horizontally)) ;; Online packages configuration ;; So long, Will Mengarini. (delight 'abbrev-mode nil 'abbrev) (delight 'auto-fill-function "⏎" t) (delight 'auto-revert-mode "⟳" 'autorevert) (delight 'auto-revert-tail-mode "–" 'autorevert) (delight 'eldoc-mode "πŸ“–" 'eldoc) (delight 'footnote-mode "ΒΉ" 'footnote) (delight 'flyspell-mode (propertize "πŸ–‹" 'face 'flyspell-incorrect) 'flyspell) (delight 'hi-lock-mode nil 'hi-lock) (delight 'hs-minor-mode "…" 'hideshow) (delight 'isearch-mode "πŸ”" 'isearch) (delight 'org-indent-mode "Β»" 'org-indent) (delight 'magit-blame-mode "πŸ‘‰" 'magit-blame) (delight 'mml-mode "πŸ“§" 'mml) (delight 'page-break-lines-mode nil 'page-break-lines) (delight 'scroll-lock-mode "πŸ“œ" 'scroll-lock) (delight 'text-scale-mode '(:eval (if (>= text-scale-mode-amount 0) "πŸ—š" "πŸ—›")) 'face-remap) (delight 'visual-line-mode "β€Έ" t) (delight 'whitespace-mode nil 'whitespace) (delight 'with-editor-mode "⸎" 'with-editor) ;; TODO: Narrow (βŒ–, β›Ά) (if (< emacs-major-version 27) (delight 'compilation-in-progress "βš™" 'compile) (message "TODO: tweak compilation lighter")) (defun my/magit-mode-hook () (when window-system (local-set-key [C-tab] 'other-window) (local-set-key (kbd "C-i") 'magit-section-cycle) (local-set-key (kbd "") 'magit-section-toggle))) (add-hook 'magit-mode-hook 'my/magit-mode-hook) (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh t) ;; Don't use Customize here, since that would set the variable's value ;; in stone, and I would miss out on future updates by Magit. (add-hook 'git-commit-setup-hook 'git-commit-turn-on-flyspell) (defun my/revision-at-point () (cond ;; TODO: add vc support. ((derived-mode-p 'magit-mode) (magit-branch-or-commit-at-point)))) (eval-when-compile ;; Load rx's pcase pattern. (require 'rx)) (defun my/action-stamp-at-point (rev) (interactive (list (let* ((rev (my/revision-at-point)) (prompt (if rev (format "Revision? (%s) " rev) "Revision? "))) (read-string prompt nil nil rev)))) (let* ((cmd "git show --no-patch --date=unix --format='%ad!%ae'") (git-info (shell-command-to-string (format "%s %s" cmd rev)))) (pcase git-info ((rx (let timestamp-str (+ digit)) "!" (let mail (+ anychar)) "\n") (let* ((timestamp (string-to-number timestamp-str)) (date (format-time-string "%FT%TZ" timestamp t)) (action-stamp (format "%s!%s" date mail))) (kill-new action-stamp) (message action-stamp)))))) ;; rg re-binds C-n and C-p. I loathe arrow keys for regular movement, ;; and n and p are laggy because they visit the matched files. ;; Since I can't blame anyone for telling me "It's 2019; use the arrow ;; keys and buy an SSD already", I'll just add this kludge… (define-key rg-mode-map (kbd "C-p") nil) (define-key rg-mode-map (kbd "C-n") nil) ;; Major modes configuration (defun my/c-modes-hook () (c-set-style "bsd") (c-set-offset 'arglist-close 0)) (add-hook 'c-mode-common-hook 'my/c-modes-hook) (defun my/python-hook () (setq-local forward-sexp-function nil)) (add-hook 'python-mode-hook 'my/python-hook) (defun my/compilation-notify (buffer results) (let* ((title (buffer-name buffer)) (status (if (string-equal results "finished\n") "success" "failure")) (icon (format "%s/icons/compilation-%s.png" user-emacs-directory status))) (require 'notifications) (notifications-notify :title title :body results :app-icon icon :timeout 3000))) (add-to-list 'compilation-finish-functions 'my/compilation-notify) (defun my/make-tabless (f) "Make a function which will run F with `indent-tabs-mode' disabled." (lambda () (:documentation (format "Run `%s' with `indent-tabs-mode' set to nil." f)) (interactive) (let ((indent-tabs-mode nil)) (call-interactively f)))) (defun my/makefile-hook () ;; I would rather align backslashes with spaces rather than tabs; ;; however, I would also like indent-tabs-mode to remain non-nil. (local-set-key (kbd "C-c C-\\") (my/make-tabless 'makefile-backslash-region)) (local-set-key (kbd "M-q") (my/make-tabless 'fill-paragraph))) (add-hook 'makefile-mode-hook 'my/makefile-hook) (defun my/shell-hook () (setq truncate-lines nil) (setq-local font-lock-comment-face 'default) (setq-local font-lock-string-face 'default) (setq-local recenter-positions '(top middle bottom))) (add-hook 'shell-mode-hook 'my/shell-hook) ;; What I mean: ;; (defun my/erc-hook () ;; (add-to-list 'erc-modules 'log) ;; (delq 'fill erc-modules) ;; (erc-update-modules)) ;; ;; That cannot work because erc-update-modules only iterates over ;; erc-modules, so it will not act on the `fill' module. ;; ;; I do *not* want to maintain an exhaustive and manually curated list ;; of ERC modules; I just want to add/remove a few ones. Customizing ;; erc-{log,fill}-mode does not work: the contents of erc-modules ;; take precedence. ;; ;; My best attempt at solving this is thus abusing erc-modules's ;; setter function, which will iterate over items in the old value, ;; and disable those that are absent from the new one. (defun my/erc-hook () (let ((new-modules (delete-dups (remq 'fill (cons 'log erc-modules))))) (customize-set-variable 'erc-modules new-modules))) (add-hook 'erc-mode-hook 'my/erc-hook) (add-hook 'dired-mode-hook 'diff-hl-dired-mode-unless-remote) (add-to-list 'ibuffer-saved-filter-groups '("my/ibuffer-groups" ("REPL" (or (derived-mode . comint-mode) (mode . lisp-interaction-mode))) ("Programming" (derived-mode . prog-mode)) ("Folders" (mode . dired-mode)) ("Chat" (mode . erc-mode)) ("Documentation" (or (mode . Info-mode) (mode . Man-mode) (mode . help-mode))))) (add-hook 'ibuffer-mode-hook (lambda () (ibuffer-switch-to-saved-filter-groups "my/ibuffer-groups"))) (eval-after-load 'org-keys '(progn (define-key org-mode-map (kbd "C-j") 'org-return) (define-key org-mode-map (kbd "RET") 'org-return-indent))) ;; Helper functions and miscellaneous settings. (defun my/froggify () (ispell-change-dictionary "fr") (setq-local colon-double-space nil) (setq-local sentence-end-double-space nil) (setq-local fill-nobreak-predicate (cons 'fill-french-nobreak-p fill-nobreak-predicate)) (setq-local my/froggified t)) (defun my/unfroggify () (ispell-change-dictionary "default") (setq-local colon-double-space t) (setq-local sentence-end-double-space t) (setq-local fill-nobreak-predicate (remq 'fill-french-nobreak-p fill-nobreak-predicate)) (setq-local my/froggified nil)) (defun my/croak () (interactive) (if (and (boundp 'my/froggified) my/froggified) (my/unfroggify) (my/froggify))) ;; Font stuff 🀷🀦. Emacs comes with sensible defaults (e.g. the ;; default fontset includes Symbola for various subgroups of the ;; "symbol" script), but no color font by default. (when (>= emacs-major-version 27) ;; Prefer a color font for emojis. (set-fontset-font t 'symbol "Noto Color Emoji" nil 'prepend) ;; Make sure the default font does not get overzealous (βš βš™). (setq use-default-font-for-symbols nil)) (defun my/project-root () (when-let ((project (project-current))) (car (project-roots project)))) (defun my/project-name () (when-let ((root (my/project-root))) (when (not (file-equal-p root "~")) (file-name-nondirectory (string-trim-right root "/"))))) (defun my/connection-name () (when-let ((method (file-remote-p default-directory 'method))) (if (string-match-p "sudo" method) method (format "%s:%s" method (file-remote-p default-directory 'host))))) (defun my/frame-title-format () (let ((prefix ;; Messing with match data during redisplay is dangerous ;; (cf. bug#33697). (save-match-data ;; For some reason, calling filename-parsing functions ;; while TRAMP is busy opens the gates to Infinite ;; Minibuffer Recursion Hell. Cautiously side-step that. (or (my/connection-name) (my/project-name))))) (concat (when prefix (format "[%s] " prefix)) "%b"))) (setq frame-title-format '(:eval (my/frame-title-format))) (setq-default paragraph-start (concat "[ ]*- \\|" paragraph-start)) ;; TODO: decruftify mode-line (e.g. remove superflous parens)