;;; init.el --- my emacs configuration -*- lexical-binding: t; -*- (add-to-list 'load-path (expand-file-name (concat user-emacs-directory "site-lisp"))) (setq garbage-collection-messages t) (add-hook 'emacs-startup-hook #'(lambda () (message (format "Initialised in %s seconds with %s garbage collections." (emacs-init-time) gcs-done)) ;;; reset garbage collector (setq gc-cons-threshold 800000 gc-cons-percentage 0.2))) (setopt user-mail-address "noa@gaiwan.org") (setopt indent-tabs-mode nil) ;; properly distinguish these chords from their ascii legacy (define-key input-decode-map [?\C-m] [C-m]) (use-package tubthumping-theme :config (load-theme 'tubthumping t)) ;; I make my caps lock a menu key, so i can open the command palette with it (define-key context-menu-mode-map (kbd "") nil) (global-unset-key (kbd "M-x")) (global-set-key (kbd "") 'execute-extended-command) (setopt minibuffer-depth-indicate-mode t) ;;; Minibuffer candidate completion ;; Vertico is a package for a nice minibuffer completion experience. It displays a vertical list of candidates. It integrates well with the emacs ecosystem and lets me use other packages that also play nicely. (use-package vertico :ensure t) (setopt vertico-mode t) ;; We want vertico to take up a maximum of 12 lines on the display. My screen is quite small, so that's fine, but if i had a bigger screen, i might want to look into setting a percentage or increasing this. (setopt vertico-count 12) ;; We also want to be able to jump to the bottom of the list by moving up from the top of the list, and the opposite. I've rarely made use of this functionality and i don't know if it's actually a best practice from an interaction perspective, but i'm going to keep it on until it causes an issue for me. (setopt vertico-cycle t) ;; Multiform mode will allow us to set different layouts for different completion categories. For example, after pressing a prefix key, i can press C-h to view a list of all possible following keys. For this, i want vertico to display a grid of choices, rather than one completion per line. (setopt vertico-multiform-mode t) ;; And of course, i want to be able to interact with vertico with the mouse. (setopt vertico-mouse-mode t) ;; When completing a filename, i want to be able to easily delete directories in one fell swoop, instead of character by character or word by word. Usually C- would be fine, but if directories have a hyphen or space in their name, i have to press multiple times, which is almost never desirable. (bind-key (kbd "RET") #'vertico-directory-enter 'vertico-map) (bind-key (kbd "") #'vertico-directory-delete-char 'vertico-map) (bind-key (kbd "") #'vertico-directory-delete-word 'vertico-map) ;; Tidy shadowed file names ;; :hook (rfn-eshadow-update-overlay . vertico-directory-tidy) (setopt read-buffer-completion-ignore-case t) (setopt read-file-name-completion-ignore-case t) (setopt completion-ignore-case t) (use-package orderless :ensure t :custom (completion-styles '(orderless basic)) (completion-category-defaults nil) (completion-category-overrides '((file (styles partial-completion))))) (setopt confirm-nonexistent-file-or-buffer 'after-completion) (defun noa/mode-line-modified () (cond (buffer-read-only "RO") ((buffer-modified-p) "**") (t "RW"))) ;;; Replace the mode line with a header line ;; First, we set the mode line to nil. On my graphical display, this collapses it so all i get is a thin black line separating the buffer from the echo area. (setq-default mode-line-format nil) ;; But the mode line still holds some useful information that i want to see. I would rather that be in the header line, because to me it makes sense for this kind of metadata to be /above/ the buffer it is describing. (setq-default header-line-format '( ;; First, in white on black text, i want the information about the state of the file. This will show three hyphens in the top left corner of the header line. The first two hyphens mean that the file is both writable and unchanged. If the buffer has been changed, they will change to two asterisks. If the buffer is read only, they will change two percentage symbols. And if the buffer is read only and has been changed, the first will change to a percentage symbol, and the second will change to an asterisk. The final hyphen represents that the file is local, specifically that the default-directory variable is local. If it is remote, an at symbol will be displayed instead. (:propertize ("" mode-line-modified mode-line-remote) face highlight ) ;; Next, we want to display the buffer name. For buffers which belong to files, this will usually be the file name, but it is likely to be something more informative for special buffers. " %b" ;; Below that, show a line and column coördinate. There are special minor modes that will enable or disable this for the default mode line, but i ignore that and put the formatting code here directly. The docstring for the mode-line-format variable suggests that the column might not be displayed correctly in some situations without enabling the minor mode, but i haven't noticed that yet so i don't bother. This column number is zero-indexed; a capital c would make it one-indexed. For now i stick with zero-indexed as that's the emacs default and i'm not sure which is better. I guess it makes a bit more sense that the first character on a line is labeled "1". ":%l,%c" " " ;; I don't know exactly what this variable covers, so i keep it here so that if something shows up i know that it gets put here. Because i have a global mode line in my tab bar, some of the things that would otherwise be here (like the time, battery percentage, and notifications for chat buffers) don't show up. mode-line-misc-info mode-line-end-spaces ) ) (defun pulse-line (&rest _) "Pulse the current line." (pulse-momentary-highlight-one-line (point))) (dolist (command '(scroll-up-command scroll-down-command recenter-top-bottom other-window)) (advice-add command :after #'pulse-line)) ;;; Jabber (use-package jabber :ensure t) ;; I don't actually use xmpp as xmpp that much. But i do use it to connect to irc, and this package lets me do that. Unfortunately, it's not a particularly well-behaved package; by default it clobbers some keybindings and floods the echo area with unhelpful messages. (setopt jabber-account-list '(("noa@hmm.st"))) ;; So now what we're going to do is get it to stop showing a bunch of channels in the mode line, because there will always be new activity and i want to drop in when i feel like it, and not always have a reminder of that fact. The defun below is copied from jabber-activity-show-p-default but with an extra condition plopped in. (defcustom noa/jabber-activity-dont-show '("#tildetown%town@irc.hmm.st" "#meta%tilde.chat@irc.hmm.st" "hmm@conference.hmm.st") "List of JIDs not to show in the modeline." :group 'jabber-activity)(defun noa/jabber-activity-show-p (jid) "Return non-nil if JID should be hidden. A JID should be hidden when there is an invisible buffer for JID, when JID is not in `noa/jabber-activity-dont-show', and when JID is not in `jabber-activity-banned'." (let ((buffer (jabber-activity-find-buffer-name jid))) (and (buffer-live-p buffer) (not (get-buffer-window buffer 'visible)) (not (cl-dolist (entry jabber-activity-banned) (when (string-match entry jid) (cl-return t)))) (not (cl-dolist (entry noa/jabber-activity-dont-show) (when (string-match entry jid) (cl-return t)))))))(setopt jabber-activity-show-p #'noa/jabber-activity-show-p) ;; I'm on a laptop, so whenever i shut it i get disconnected. Jabber can query auth-source for my password, so automatically reconnecting is useful and doesn't need me to do anything. (setopt jabber-auto-reconnect t) ;; Because my xmpp server supports message history, i don't need to worry about exiting without seeing all messages, as they'll be there when i get back. (setopt jabber-activity-query-unread nil) ;; The default buffer names are a bit ugly to look at, so i change them to a similar format as eww. Which is still pretty ugly, welcome to emacs (setopt jabber-chat-buffer-format "*%n | jabber*" jabber-groupchat-buffer-format "*%n | jabber*") ;; As alluded to above, jabber.el also has a terrible terrible habit of sending a message to the echo area for every change in online state of my contacts, and every single message in any channel. Obviously this gets very annoying, especially if i'm using the minibuffer at that time. Thank you to acdw for pointing me towards these helpful hooks to remove. (remove-hook 'jabber-alert-muc-hooks #'jabber-muc-echo) (remove-hook 'jabber-alert-presence-hooks #'jabber-presence-echo) ;; Also stop jabber from clobbering the dired-jump binding and instead use something on C-c like a good child. (with-eval-after-load 'jabber (keymap-global-set "C-x C-j" #'dired-jump)) ;; Finally, have a binding to jump to a buffer in which there's been some new activity. Better than always using switch-to-buffer to get there. (keymap-global-set "C-c C-j" #'jabber-activity-switch-to) (use-package nov :ensure t :mode ("\\.epub\\'" . nov-mode)) ;; also check out jinx https://github.com/minad/jinx (use-package spell-fu :ensure t :hook (text-mode . spell-fu-mode)) ;; consult-buffer replaces the buffer menu. as well as listing buffers, it lists bookmarks and recent files. (use-package consult :ensure t :bind (([remap switch-to-buffer] . consult-buffer) ;; also contains file history, etc ([remap yank-pop] . consult-yank-pop) ;; like normal yank-pop but with live preview ([remap goto-line] . consult-goto-line))) (use-package embark :ensure t :bind (("C-" . embark-act) ("C-" . embark-export) ([remap describe-bindings] . embark-bindings) ("M-." . embark-dwim)) :hook (eldoc-documentation-functions . embark-eldoc-first-target) :custom (eldoc-documentation-strategy 'eldoc-documentation-compose-eagerly) (embark-indicators '(embark-minimal-indicator embark-highlight-indicator embark-isearch-highlight-indicator)) ) (setq prefix-help-command #'embark-prefix-help-command) (defun embark-which-key-indicator () "An embark indicator that displays keymaps using which-key. The which-key help message will show the type and value of the current target followed by an ellipsis if there are further targets." (lambda (&optional keymap targets prefix) (if (null keymap) (which-key--hide-popup-ignore-command) (which-key--show-keymap (if (eq (plist-get (car targets) :type) 'embark-become) "Become" (format "Act on %s '%s'%s" (plist-get (car targets) :type) (embark--truncate-target (plist-get (car targets) :target)) (if (cdr targets) "…" ""))) (if prefix (pcase (lookup-key keymap prefix 'accept-default) ((and (pred keymapp) km) km) (_ (key-binding prefix 'accept-default))) keymap) nil nil t (lambda (binding) (not (string-suffix-p "-argument" (cdr binding)))))))) ;; (setopt embark-indicators ;; '(embark--vertico-indicator ;; ;; embark-which-key-indicator ;; embark-minimal-indicator ;; embark-highlight-indicator ;; embark-isearch-highlight-indicator)) (defun embark-hide-which-key-indicator (fn &rest args) "Hide the which-key indicator immediately when using the completing-read prompter." (which-key--hide-popup-ignore-command) (let ((embark-indicators (remq #'embark-which-key-indicator embark-indicators))) (apply fn args))) ;; (advice-add #'embark-completing-read-prompter ;; :around #'embark-hide-which-key-indicator) (use-package embark-consult :ensure t ; only need to install it, embark loads it after consult if found :hook (embark-collect-mode . consult-preview-at-point-mode)) (use-package marginalia :ensure t :after vertico :custom (marginalia-mode t) (marginalia-max-relative-age most-positive-fixnum) (marginalia-align 'right)) ;; My keyboard has a tab key and an i key. For legacy reasons, by default emacs converts C-i to mean the same thing as the tab key, but i don't really want that. The tab key is called and it gets translated to TAB. C-i is TAB, but i'd rather it by C-i. That's what this decode line does. (define-key input-decode-map [?\C-i] [C-i]) ;; Now that tab and C-i are properly distinguished, i can bind C-i to completion at point. (global-set-key (kbd "") 'completion-at-point) ;; I also want to make the completion at point function a bit more friendly than the default, so i ask consult to provide the completion functionality. (setopt completion-in-region-function 'consult-completion-in-region) (use-package org-contacts :ensure t :pin gnu :custom (org-contacts-files org-agenda-files) ;; Org contacts seems to require birthdays to have a year. However, i don't know the year of a lot of people's birthdays, so i set the year to 0000. This looks bad when the birthday comes up in the agenda. I figured that i don't usually need to know how old someone is going to be, so i change the format of how the birthday appears in the agenda so that i don't get the wrong age. (org-contacts-birthday-format "Birthday: %l") ) ;; select the help window so that i can easily close it again with q (setopt help-window-select t) (use-package helpful :ensure t :bind ( ([remap describe-function] . helpful-callable) ([remap describe-variable] . helpful-variable) ([remap describe-key] . helpful-key) ([remap describe-command] . helpful-command) ("C-c C-d" . helpful-at-point) ("C-h F" . helpful-function) )) (use-package window :custom (switch-to-buffer-obey-display-actions t)) (use-package org-fc :disabled :ensure t :custom (org-fc-directories (expand-file-name "~/data/notes/"))) (use-package artbollocks-mode :disabled t :ensure t) (use-package org :custom (org-use-sub-superscripts "{}") ;; If we enable this, emphasis markers will be hidden for a more word processor feel. This has the downside of meaning you have to delete a hidden character to get rid of bold or italic text. I don't have much of a problem with seeing the emphasis markers so i'm willing to put up with any aesthetic shortcomings for a better user experience. The package org-appear solves this by hiding them, but showing them when the point is over them, but i don't think having a whole package just for that is worth it. (org-hide-emphasis-markers nil) (org-startup-with-inline-images t) (org-image-actual-width '(300)) (org-auto-align-tags nil) (org-tags-column 0) (org-catch-invisible-edits 'show-and-error) (org-special-ctrl-a/e t) (org-insert-heading-respect-content t) (org-ellipsis "…") (org-display-custom-times t) (org-time-stamp-custom-formats '("%Y-%m-%d" . "%Y-%m-%d %H:%M")) (org-extend-today-until 4) (org-adapt-indentation nil) (org-log-done 'time) (org-return-follows-link t) (org-agenda-files '("~/data/notes/notes.org")) (org-capture-templates `(("j" "Journal" entry (file+datetree "~/data/notes/notes.org") "* %?\n" :empty-lines 1) ("w" "Website" entry (file+datetree "~/data/notes/notes.org") "* %a\n%?\n" :empty-lines 1) ("c" "Contact" entry (file+datetree "~/data/notes/notes.org") "* %^{Name} :PROPERTIES: :ADDRESS: %^{Address} :BIRTHDAY: %^{yyyy-mm-dd} :EMAIL: %^{EMAIL}p :NOTE: %^{NOTE} :END:" :empty-lines 1))) (org-agenda-tags-column t) (org-agenda-block-separator ?─) (org-agenda-time-grid '((daily today require-timed) (800 1000 1200 1400 1600 1800 2000) " ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄")) (org-agenda-current-time-string "◀── now ─────────────────────────────────────────────────") :custom-face ;;(org-ellipsis ((t (:inherit default :box nil)))) :bind (("C-c c" . org-capture) ("C-c a" . org-agenda)) ) (use-package org-modern :ensure t :after org :hook (org-mode . org-modern-mode) (org-agenda-finalize . org-modern-agenda) :custom ;; There are three ways to make bullet lists in org mode, which seems a bit excessive to me. I almost always only use the hyphen, but i like my bullet points to look like bullets, so here i overwrite the hyphen display to show a bullet point. While i'm at it, i overwrite the others too, because they are functionally identical so should probably look the same too. (org-modern-list '( (?+ . " • ") (?- . " • ") (?* . " • ") )) (org-modern-timestamp nil) (org-modern-star nil) (org-modern-keyword nil) (org-modern-checkbox '((88 . "☑") (45 . #("□–" 0 2 (composition ((2))))) (32 . "□"))) (org-modern-table nil)) (defun read-file-as-string (filename) "Read file contents from FILENAME." (with-temp-buffer (insert-file-contents filename) (buffer-string))) (setq noa/website-header (read-file-as-string "/home/noa/projects/org-website/templates/header.html")) (setq noa/website-footer (read-file-as-string "/home/noa/projects/org-website/templates/footer.html")) ;; The index page generation functions were taken from Dennis Ogbe. Thank you! (defun my-blog-parse-sitemap-list (l) "Convert the sitemap list in to a list of filenames." ;; LIST looks like: ;; (unordered ("[[file:uses.org][Things i use]]") ("[[file:media.org][Media Diary]]") ("[[file:tanklobsters.org][Tank lobsters]]")) (mapcar #'(lambda (i) (let ( (link (with-temp-buffer ( let ( (org-inhibit-startup nil) ) (insert (car i)) (org-mode) (goto-char (point-min)) (org-element-link-parser) ))) ) (when link (plist-get (cadr link) :path) ) ) ) (cdr l) ) ) (defun my-blog-sort-article-list (l p) "sort the article list anti-chronologically." (sort l #'(lambda (a b) (let ((date-a (org-publish-find-date a p)) (date-b (org-publish-find-date b p))) (not (time-less-p date-a date-b)))))) (defun noa/naive-org-first-paragraph (file) "Naively returns the first paragraph of FILE. The way that the first paragraph is determined is to assume that there will be an org metadata block beforehand, so look for the first two consecutive newlines and mark the following paragraph." (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) (re-search-forward "\n\n") (mark-paragraph) (let ( (beg (mark)) (end (point)) ) (buffer-substring beg end)))) (defun noa/website-sitemap (title list) "Generate the index page for my website." ;; LIST looks like: ;; (unordered ("[[file:uses.org][Things i use]]") ("[[file:media.org][Media Diary]]") ("[[file:tanklobsters.org][Tank lobsters]]")) (with-temp-buffer ;; mangle the parsed list given to us into a plain lisp list of files (let* ( (filenames (my-blog-parse-sitemap-list list)) (project-plist (assoc "website-pages" org-publish-project-alist)) (articles (my-blog-sort-article-list filenames project-plist)) ) (message (concat "PLIST: " (plist-get project-plist :base-directory))) (insert "Several parts of this website are broken as i wrangle with the monstrosity that is programming in emacs lisp. The content should still be fine, but for further cosmetics please hold <3\n\n") (dolist (file filenames) (let* ( (abspath (file-name-concat "/home/noa/data/share" file)) ;; (abspath (file-name-concat (plist-get project-plist :base-directory) file)) (relpath (file-relative-name abspath "/home/noa/data/share")) (title (org-publish-find-title file project-plist)) (date (format-time-string (car org-time-stamp-formats) (org-publish-find-date file project-plist))) (preview (noa/naive-org-first-paragraph abspath)) ) (insert (concat "* [[file:" relpath "][" title "]]\n")) (insert (concat "*" date ":*" preview )) (insert "\n") ;; (insert (concat "[[file:" relpath "][Read More...]]\n")) ) ) ;; insert a title and save (insert "#+TITLE: noa.pub\n") (buffer-string) ) ) ) (setq org-publish-project-alist `( ("website" :components ("website-pages" "website-assets")) ("website-pages" :publishing-function org-html-publish-to-html :base-directory "/home/noa/data/share" :publishing-directory "/home/noa/projects/org-website" :base-extension "org" :section-numbers nil :with-toc nil :with-drawers t :with-sub-superscript t :html-link-home "/" :html-head "𰻝\">" :html-head-include-default-style nil :html-head-include-scripts nil :html-doctype "html5" ;; :html-validation-link nil :html-preamble "" :html-postamble ,noa/website-footer :html-home/up-format "" :html-link-up "" :html-html5-fancy t :html-indent t :html-head " " :html-divs ( (preamble "header" "") (content "main" "") (postamble "footer" "")) :auto-sitemap t :sitemap-filename "/home/noa/projects/org-website/index.org" :sitemap-title "noa.pub" :sitemap-style list ;; :sitemap-format-entry :sitemap-function noa/website-sitemap :sitemap-sort-folders ignore :sitemap-sort-files anti-chronologically :sitemap-ignore-case t ) ("website-assets" :publishing-function org-publish-attachment :base-directory "/home/noa/data/share" :publishing-directory "/home/noa/projects/org-website" :base-extension "css\\|js\\|png|\\jpg" :recursive t))) ;; TODO: replace with visual-fill-column ;; https://codeberg.org/joostkremers/visual-fill-column (use-package olivetti :ensure t :hook ( (text-mode . olivetti-mode) (eww-after-render . olivetti-mode) (nov-post-html-render . olivetti-mode) (mu4e-view-mode . olivetti-mode) ) ) (use-package ffap :custom (ffap-file-name-with-spaces t) :bind ( ([remap find-file] . find-file-at-point) ([remap dired] . dired-at-point) ) ) (setopt browse-url-browser-function 'eww-browse-url browse-url-secondary-browser-function 'browse-url-default-browser url-cookie-trusted-urls '() url-cookie-untrusted-urls '(".*") shr-cookie-policy nil ;; I don't want web pages to be able to specify their own colours, because i like the colours i already have set. shr-use-colors nil shr-max-width nil ;; We can set what the maximum size of an image in a window should be. This is a fraction of the total window width or height, and if the image would be bigger than this, it'll be resized to fit. It's useful to have it smaller because emacs still sort of chokes on scrolling when there are large images in a buffer. This is the default value of this option. shr-max-image-proportion 0.9 shr-discard-aria-hidden t ;; The default name for the eww buffer is *eww*. This is unhelpful because it makes having more than one eww buffer open a bit of a chore to navigate. We can set it to 'url, 'title, or a function. I set it to 'title because marginalia already shows me the url. However, this means that i can't search for a url name when switching buffers. See the help for this variable for an example of a function which gives the page title and the url. eww-auto-rename-buffer 'title) ;; Goto address mode makes urls and email address in a buffer clickable. I want these clickable links to look like links, because that's what they are. The two mouse face variables are what face is used on hover, which at the moment i ignore. It might also be worth setting them to 'highlight. (use-package goto-addr :custom (global-goto-address-mode t) (goto-address-mail-face 'link) (goto-address-mail-mouse-face 'link) (goto-address-url-face 'link) (goto-address-url-mouse-face 'link)) ;; Abbrev mode expands one string into another string. I use it as a simple autocorrect mode. If i misspell a word, i run C-x a i g which will prompt me for what to expand the previous word into. I type the correct spelling, and whenever i make that mistake again, it will automatically be corrected. It's important to be careful not to set something that could be a typo for two words though, because otherwise it gets even more annoying. Luckily it's easy to update the abbrevs which are stored in ~/.config/emacs/abbrev_defs. M-x list-abbrevs is also a nice command which shows all the saved abbrevs and how many times they've been expanded. (add-hook 'text-mode-hook #'abbrev-mode) (use-package eldoc :delight :custom (global-eldoc-mode t)) ;; use a bar cursor and blink it and don't stop blinking it. i don't know how i feel about this yet to be honest, but it helps me know which window is active so for now i'm keeping it (setopt cursor-type 'bar blink-cursor-mode -1 blink-cursor-interval 0.7) ;; Dired is a really nice package which, as with a lot of emacs, has some dodgy defaults. Here we round off some of the sharp edges to make it more enjoyable to use. (use-package dired :custom ;; By default, dired permanently deletes files. But i have quite a bit of storage and also make bad decisions regularly, so it seems fitting to make use of the wonderful invention that is the trash. People who have used systems from the last forty years or so will likely be familiar with this innovation. (delete-by-moving-to-trash t) ;; It's not fun to be asked every time whether we want to delete a directory recursively. It's an understandable default for safety reasons, but because we are not deleting permanently but rather just moving to the trash, it's not such a concern. (dired-recursive-deletes 'always) ;; Recursive copying isn't even destructive, so i definitely don't want to be asked about that. (dired-recursive-copies 'always) ;; After we delete some files or directories, it makes sense to get rid of any buffers which are looking at those files or directories. (dired-clean-up-buffers-too nil) ;; With this set, if we have two dired buffers open next to one another, a rename operation in one will default to the directory shown in the other. In this way, we can pretend we are using some kind of norton commander like file browser instead of slumming it in emacs. (dired-dwim-target t) ;; These are some useful ls switches. We have to keep -l. To show dotfiles as well, we use -a. To sort numbers by number order instead of lumping together ones, twos, and so on, we use -v. Because we don't have colour, it's nice to have a clear indicator of what is a file and what is a directory, as well as other different things like symlinks which i never remember. By using -F, a forward slash is appended to every directory. And to get more easily understandable file sizes, we use -h, which will tell us the file size in kilobytes or megabytes rather than a huge number that means nothing to me. I won't explain the meaning of the long flag. (dired-listing-switches "-alvFh --group-directories-first")) ;;; Indentation: tabs and whitespace settings ;; In general, my rules for inserting tabs are that the tab key should insert tabs. I personally prefer tabs to spaces, because tabs work reasonably well whatever font or tab width one chooses to set, whereas spaces are the same width for everyone, except when someone uses a proportional font in which case they are narrower than expected. Furthermore, people tend to use spaces for alignment, which looks bad when you can't rely on every character being the same width. ;; However, i'm in the minority, and fighting with the very complicated emacs indentation systems is simply not fun. That said, i refuse to use a monospaced font. Luckily the minority is more than one and someone has already done the hard work for me of writing a mode to make spaces for indentation work reasonably well with a proportional font. That mode is elastic-indent-mode, and it very simply makes leading whitespace characters the same width as the characters on the line above. It's a simple solution but most of the time it does what i want. (require 'elastic-indent) (add-hook 'prog-mode-hook #'elastic-indent-mode) ;; Previously i used a function to naïvely copy the whitespace from the line above. This is the way that vi, nano, and acme all implement auto-indentation. However, for now i'm experimenting with using the built-in indentation functions again. I'm leaving this defun here for posterity. (defun noa/naive-return-and-indent () "Insert a newline and copy the indentation of the previous line, vi/nano style." (interactive) (open-line 1) (let* ((start (progn (beginning-of-line) (point))) (indent (progn (back-to-indentation) (point))) (end (progn (end-of-line) (point))) (whitespace (buffer-substring start indent))) (delete-trailing-whitespace start end) (beginning-of-line 2) (insert whitespace))) ;; We will only be trying to indent at the start of a line, and sometimes we will want to insert a standard tab character. We can also set this option to 'complete, which will run completion at point if the region is already indented. (setopt tab-always-indent nil) ;; Usually, we want indentation to be done with tabs. Some modes make more sense to use spaces to indent. Lisp is a particular example, and emacs's default behaviour of converting tabs into spaces is frankly horrific. I've taken the below code from acdw to use spaces in these modes. (defvar space-indent-modes '(emacs-lisp-mode lisp-interaction-mode lisp-mode scheme-mode python-mode) "Modes to indent with spaces, not tabs.") (add-hook 'prog-mode-hook (defun indent-tabs-mode-maybe () (setq indent-tabs-mode (if (apply #'derived-mode-p space-indent-modes) nil t)))) ;; I want to ensure that indentation is always correct. The builtin electric indent mode works /sometimes/, but the aggressive indent mode package is more reliable. (use-package aggressive-indent :ensure t) (setopt global-aggressive-indent-mode t) ;;; Interface ;; I want to make sure that various bits of the interface are hidden. but this isn't an "all gui chrome is useless" rampage. I personally think the scrollbar is useful, i like the visual indication it gives of how far i am through a file. ;; At the moment, explicitly disabling the menu bar and tool bar does nothing, because i already set there to be no lines displayed for the tool and menu bars in my early-init.el file. ;; (setopt ;; menu-bar-mode nil ;; tool-bar-mode nil ;; ) ;; Tooltips are little popups next to the mouse cursor. I think this information is helpful, but i like it to appear in a more consistent position, because i find it frustrating when popups cover parts of the ui that i wanted to see. By disabling tooltip-mode, the contents that would be in a popup is instead shown in the echo area. (setopt tooltip-mode nil) ;; I see no reason not to immediately show which chords in a key sequence i have already pressed. Emacs does, however, and instead of letting me set the value of echo-keystrokes to zero to wait zero seconds to show that information, it repurposes zero as a method of disabling the functionality altogether, and provides no special functionality for setting it to nil that would explain why that's not an acceptable method of disabling a feature. Instead, i have to deal with setting it to nearly zero, and luckily i can't tell the difference. (setopt echo-keystrokes 0.1) ;; A useful feature when programming is to show matching parentheses. Show-paren-mode is a global mode. By default it runs in all buffers except those inheriting from special mode. (setopt show-paren-mode t ;; This variable means that if there is no non-whitespace character in between the point and the paren, it will be highlighted. It's useful to highlight parentheses if the point is at the start of the line and the paren is indented. show-paren-when-point-in-periphery t ;; By default, the point has to be after a paren for it to be highlighted. But often the point will be just inside, in which case it's also helpful for the pair to be highlighted. show-paren-when-point-inside-paren t ) ;; populate and enable the context menu ;; (setopt context-menu-functions '( ;; context-menu-ffap ;; occur-context-menu ;; context-menu-region ;; context-menu-undo ;; goto-address-context-menu) ;; context-menu-mode t) (use-package mouse :custom (context-menu-mode nil)) (defun noa/helpline () (concat "[C-x C-f] Open \t" "[M-w] copy \t" "[C-w] Cut \t" "[C-s] search \t" "[C-x C-s] Save \t" "[C-y] Paste \t" "[C-/] Undo \t" "[M-x] Command \t" ) ) (use-package tab-bar :custom (tab-bar-mode t) (tab-bar-format '( ;; noa/helpline tab-bar-format-menu-bar tab-bar-format-align-right tab-bar-format-global )) ) (use-package font-lock :custom (global-font-lock-mode t) (font-lock-maximum-decoration nil)) (setopt inhibit-startup-screen t mouse-drag-and-drop-region nil mouse-yank-at-point t delete-selection-mode nil ;; deleting should be an explicit action ) (global-set-key (kbd "C-t") 'tab-new) ;; shift click to select region with the mouse. This annoyingly rings the bell for an error (global-unset-key (kbd "S-")) (global-set-key (kbd "S-") 'mouse-save-then-kill) ;;; packages (setopt package-archives '( ("gnu" . "https://elpa.gnu.org/packages/") ("nongnu" . "https://elpa.nongnu.org/nongnu/") ("melpa-stable" . "https://stable.melpa.org/packages/") ("melpa" . "https://melpa.org/packages/"))) ;;; saving ;; backups are pointless in long emacs sessions imo, but autosaves are useful (setopt remote-file-name-inhibit-auto-save t) (setopt remote-file-name-inhibit-auto-save-visited t) (setopt make-backup-files nil backup-by-copying t create-lockfiles nil auto-save-mode 1 auto-save-interval 6 ;; every six keystrokes auto-save-timeout 5 ;; every 5 seconds auto-save-default t auto-save-no-message t save-silently t version-control t ;; this will auto save to the current file auto-save-visited-mode t) (add-hook 'focus-out-hook (lambda () (interactive) (save-some-buffers t))) (add-hook 'mouse-leave-buffer-hook (lambda () (interactive) (save-some-buffers t))) (use-package keyfreq :ensure t :custom (keyfreq-mode t) (keyfreq-autosave-mode t) ) ;; C-l goes in order (setopt recenter-positions '(top middle bottom)) ;; Emacs uses choppy scrolling by default. If i scoll with my trackpad, it's nice to have it move tiny amounts at the same time as my fingers, which pixel-scroll-precision-mode allows for. This also has the benefit of making scrolling over images a little bit of a nicer experience. (setopt pixel-scroll-precision-mode t pixel-scroll-precision-use-momentum t) (use-package smartscan :bind ( ("M-n" . smartscan-symbol-go-forward) ("M-p" . smartscan-symbol-go-backward) ("" . nil) ("" . smartscan-symbol-go-forward) ) ) ;;; sentences (setopt sentence-end-double-space nil) ;;; spellcheck (setopt flyspell-mode t ispell-program-name "aspell" ispell-dictionary "en_GB" ispell-extra-args '("--sug-mode=ultra") ) ;; If i write a script, i will always run chmod +x after saving it. This command means i don't have to do that. (add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p) ;; We are on a unix system, so it makes sense to end files in the unix system way. I'm surprised this isn't the default. (setopt require-final-newline t) (setopt window-min-height 1 window-combination-resize t window-resize-pixelwise t frame-resize-pixelwise t) ;;; history (setopt history-length 250 kill-ring-max 25) (use-package savehist :custom (savehist-file "~/.config/emacs/savehist") (savehist-additional-variables '( kill-ring command-history set-variable-value-history custom-variable-history query-replace-history read-expression-history minibuffer-history read-char-history face-name-history bookmark-history file-name-history)) (savehist-mode t)) (use-package frame :custom (window-divider-mode t) (window-divider-default-right-width 1) (window-divider-default-bottom-width 1) (window-divider-default-places t) ) (use-package emacs :init ;; Add prompt indicator to `completing-read-multiple'. ;; We display [CRM], e.g., [CRM,] if the separator is a comma. (defun crm-indicator (args) (cons (format "[CRM%s] %s" (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator) (car args)) (cdr args))) (advice-add #'completing-read-multiple :filter-args #'crm-indicator) ;; Do not allow the cursor in the minibuffer prompt (setq minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt)) (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode) :custom (display-battery-mode t) (display-time-mode t) (display-time-default-load-average nil) (display-time-24hr-format t) ;; Support opening new minibuffers from inside existing minibuffers. (enable-recursive-minibuffers t) (debug-on-error t) ;; Hide commands in M-x which do not work in the current mode. (read-extended-command-predicate 'command-completion-default-include-p)) (setopt recentf-max-menu-items 25 recentf-save-file "~/.config/emacs/recentf" recentf-mode 1 bookmark-default-file "~/.config/emacs/bookmarks") ;;; miscellaneous (setopt save-place-mode 1) (setenv "PAGER" "cat") (setenv "TERM" "dumb") (setenv "GPG_AGENT_INFO" nil) (defalias 'yes-or-no-p 'y-or-n-p) (setq disabled-command-function nil) (setopt custom-file (make-temp-file "custom")) (setq inhibit-startup-echo-area-message "noa") ;; #userfreedom (use-package simple :delight visual-line-mode :custom (global-visual-line-mode t)) (setopt kill-whole-line t uniquify-after-kill-buffer-p t uniquify-buffer-name-style 'forward uniquify-ignore-buffers-re "^\\*" uniquify-separator "/") (setopt save-interprogram-paste-before-kill t mouse-yank-at-point t require-final-newline t visible-bell t load-prefer-newer t ediff-window-setup-function 'ediff-setup-windows-plain) (use-package server :disabled :config (unless (server-running-p) (server-start))) (setopt help-at-pt-display-when-idle t) (use-package ctrlf :ensure t :bind ( ([remap isearch-forward] . ctrlf-forward-default) ([remap isearch-backward] . ctrlf-backward-default) ([remap isearch-forward-regexp] . ctrlf-forward-alternate) ([remap isearch-backward-regexp] . ctrlf-backward-alternate) ([remap isearch-forward-symbol] . ctrlf-forward-symbol) ([remap isearch-forward-symbol-at-point] . ctrlf-forward-symbol-at-point) ) :custom (ctrlf-go-to-end-of-match nil "It makes more sense to go to the start of the match, because i start searching where i want to be.") ) (global-set-key (kbd "M-o") 'other-window) (global-set-key (kbd "C-x k") 'kill-this-buffer) ;; undo C-/ ;; redo C-S-/ (setopt undo-no-redo t) ;; mu4e (use-package mu4e :custom (mu4e-headers-skip-duplicates t) (mu4e-view-show-images t) (mu4e-view-show-addresses t) (mu4e-compose-format-flowed nil) (mu4e-change-filenames-when-moving t) (mu4e-use-fancy-chars nil) (mu4e-confirm-quit nil) (mu4e-headers-leave-behavior 'apply) (mu4e-headers-precise-alignment t) (mu4e-headers-fields '( ;; (:human-date . 12) (:flags . 6) (:from . 32) (:subject))) (mu4e-search-threads nil) (mu4e-hide-index-messages t) (mu4e-get-mail-command "mbsync -c ~/.config/mbsyncrc fastmail") (mu4e-maildir "~/mail") (mu4e-drafts-folder "/Drafts") (mu4e-sent-folder "/Sent") (mu4e-refile-folder "/Archive") (mu4e-trash-folder "/Trash") (mu4e-bookmarks '( (:name "Inbox" :query "maildir:/Inbox" :key ?i) (:name "Feeds" :query "maildir:/Feeds" :key ?f) (:name "Paper trail" :query "\"maildir:/Paper trail\"" :key ?p))) :bind ( :map mu4e-headers-mode-map ("d" . my-move-to-trash) :map mu4e-view-mode-map ("d" . my-move-to-trash) ) :init (fset 'my-move-to-trash "mTrash") ;; function to move mails to trash :config (setq mu4e-headers-thread-connection-prefix '("│ " . "│ ") mu4e-headers-thread-last-child-prefix '("└ " . "└ ") mu4e-headers-thread-blank-prefix '(" " . " ") mu4e-headers-thread-root-prefix '("□ " . "□ ") mu4e-headers-thread-child-prefix '("│ " . "│ ") mu4e-headers-thread-orphan-prefix '("♢ " . "♢ ") mu4e-headers-thread-duplicate-prefix '("≡ " . "≡ ") mu4e-headers-thread-first-child-prefix '("⚬ " . "⚬ ") mu4e-headers-thread-single-orphan-prefix '("♢ " . "♢ ") ) ) (setopt message-fill-column nil ;; (message-signature-file) message-signature "~noa (https://noa.pub) • I try to reply to formal emails in three sentences or fewer; excuse my brevity. • I queue replies and batch send them at intervals; excuse my untimeliness.") (use-package button :custom-face ;;(button ((t (:underline t :foreground unspecified)))) ) (use-package faces :custom-face ;; (link ((t (:underline t :foreground unspecified)))) ;; (fringe ((t (:background unspecified)))) ;; (mode-line ((t (:background unspecified :box 2)))) ;; (mode-line-active ((t (:inverse-video t)))) ;; (mode-line-inactive ((t (:background unspecified :box 2 :weight unspecified)))) ;; (help-key-binding ((t (:inherit default :background unspecified :foreground unspecified :box 1)))) ) (global-set-key (kbd "M-z") 'zap-up-to-char) (use-package markdown-mode :ensure t :mode ("\\.md\\'" . markdown-mode) ) (use-package valign :ensure t :hook (markdown-mode . valign-mode) :hook (org-mode . valign-mode) :custom (valign-fancy-bar t) (valign-max-table-size 0)) (use-package eat :ensure t :hook ( (eshell-load . eat-eshell-mode) (eshell-load . eat-eshell-visual-command-mode) ) ) (use-package which-key :disabled t :ensure t :custom (which-key-popup-type 'side-window) (which-key-side-window-location 'bottom) (which-key-side-window-max-height 12) (which-key-persistent-popup nil) (which-key-show-prefix 'echo) (which-key-idle-delay 0) (which-key-mode t) ) (use-package fixed-pitch :custom (fixed-pitch-dont-change-cursor t) (fixed-pitch-blacklist-hooks '( prog-mode-hook comint-mode-hook )) (fixed-pitch-whitelist-hooks '( calendar-mode-hook dired-mode-hook magit-mode-hook profiler-report-mode-hook which-key-init-buffer-hook jabber-roster-mode-hook mu4e-headers-mode-hook )) ) ;; My current favourite font is sn pro, which feels like comic sans for grown ups. It's friendly but consistent and well thought out. However, it's also a proportional font, which obviously is the right way to do things, but emacs is very old and comes from a time before the innovation of legibility. As a result, there are some things that require a monospaced font, so i set one here. I chose go mono for two reasons: the first is because i think it looks really nice; the second is because it has serifs and is very visually distinct from sn pro, so i can notice and shame those buffers which require a fixed width font to operate properly. (custom-set-faces '(fixed-pitch ((t (:family "Go Mono" :height 110)))) '(variable-pitch ((t (:family "SN Pro" :height 110)))) ) ;; For some frustrating reason, emacs does not respect fontconfig font settings. What this means in practice is that emacs by default draws cjk characters with the korean variant. Luckily emacs has its own obscure and poorly documented way of doing things, so i can iterate over the relevant charsets and set the font specifically for those characters. (dolist (charset '(han cjk-misc)) (set-fontset-font t charset (font-spec :family "Noto Sans CJK SC"))) ;; Describe a key based on a string like "C-SPC" (defun describe-key-shortcut (shortcut) (interactive "MShortcut: ") (describe-key (kbd shortcut))) ;; Update the calendar. We want weeks to start on a monday, the first day of the week. Holidays should be highlighted, and the date format should put the year first. (setopt calendar-week-start-day 1 calendar-mark-holidays-flag t calendar-date-style 'iso) ;;; TOUCHSCREEN ;; this should be obsolete in emacs 30 ;; Copyright 2024-present Naheel Azawy. All rights reserved. (defvar touchscreen-last-time) (defvar touchscreen-last-pos-pixel) (defvar touchscreen-last-dist 0) (defvar touchscreen-begin-char) (defun touchscreen-time () "Time in seconds." (time-convert (current-time) 'integer)) (defun touchscreen-handle-touch-begin (input) "Handle touch begining at input INPUT." (interactive "e") (let* ((event (nth 1 input)) (pos-pixel (nth 3 event)) (pos-char (nth 6 event)) (win (nth 1 event))) ;; (message (format "%s" input)) (if (not (equal (selected-window) win)) ;; switch window (select-window win)) ;; set globals (setq touchscreen-last-time (touchscreen-time)) (setq touchscreen-last-pos-pixel pos-pixel) (setq touchscreen-begin-char pos-char) )) (defun touchscreen-handle-touch-update (input) "Handle touch update at input INPUT." (interactive "e") (let* ((event (nth 0 (nth 1 input))) (pos-pixel (nth 3 event)) (pos-char (nth 6 event)) (diff-time (- (touchscreen-time) touchscreen-last-time)) (diff-pixel (- (cdr touchscreen-last-pos-pixel) (cdr pos-pixel))) (diff-char (abs (- touchscreen-begin-char pos-char)))) (if (= (length (nth 1 input)) 2) ;; pinch zoom (let* ((event2 (nth 1 (nth 1 input))) (pos-pixel2 (nth 3 event2)) (dist (sqrt (+ (expt (- (car pos-pixel2) (car pos-pixel)) 2) (expt (- (cdr pos-pixel2) (cdr pos-pixel)) 2)))) (dist-diff (- dist touchscreen-last-dist))) (setq touchscreen-last-dist dist) (if (> dist-diff 0) (text-scale-increase 0.1) (if (< dist-diff 0) (text-scale-decrease 0.1))) ) (if (> diff-time 1) ;; TODO: set marker on long press (goto-char pos-char)) (if (> diff-char 1) ;; scroll (progn (move-to-window-line nil) (if (> diff-pixel 0) (pixel-scroll-pixel-up diff-pixel) (if (< diff-pixel 0) (pixel-scroll-pixel-down (* -1 diff-pixel)))) (setq touchscreen-last-time (touchscreen-time)) (setq touchscreen-last-pos-pixel pos-pixel)) )))) (defun touchscreen-handle-touch-end (input) "Handle touch end at input INPUT." (interactive "e") (let* ((event (nth 1 input)) (pos-char (nth 6 event))) (if (= touchscreen-begin-char pos-char) ;; move cursor (goto-char pos-char)))) (global-set-key [touchscreen-begin] #'touchscreen-handle-touch-begin) (global-set-key [touchscreen-update] #'touchscreen-handle-touch-update) (global-set-key [touchscreen-end] #'touchscreen-handle-touch-end)