;;; 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") ;; properly distinguish these chords from their ascii legacy (define-key input-decode-map [?\C-i] [C-i]) (define-key input-decode-map [?\C-m] [C-m]) (use-package tubthumping-theme :config (load-theme 'tubthumping t)) (global-set-key (kbd "") 'backward-delete-char-untabify) ;; I make my caps lock a menu key, so i can open the command palette with it ;; https://alexschroeder.ch/wiki/2020-07-16_Emacs_everything (define-key context-menu-mode-map (kbd "") nil) (global-unset-key (kbd "M-x")) (global-set-key (kbd "") 'execute-extended-command) (setopt cursor-type 'bar minibuffer-depth-indicate-mode t) (setopt url-cookie-trusted-urls '() url-cookie-untrusted-urls '(".*")) ;;; candidate completion (use-package vertico :ensure t :custom (vertico-mode t) (vertico-count 12) (vertico-cycle t) (read-buffer-completion-ignore-case t) (read-file-name-completion-ignore-case t) (completion-ignore-case t) ) (use-package vertico-directory :after vertico :ensure nil ;; More convenient directory navigation commands :bind ( :map vertico-map ("RET" . vertico-directory-enter) ("" . vertico-directory-delete-char) ("C-" . vertico-directory-delete-word) ) ;; Tidy shadowed file names :hook (rfn-eshadow-update-overlay . vertico-directory-tidy) ) (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"))) (setq-default header-line-format '( (:propertize ( "" mode-line-modified mode-line-remote ) face highlight ) " " "%b" ":%l,%c" " " ;; (vc-mode vc-mode) ;; " " ;; mode-line-modes mode-line-misc-info mode-line-end-spaces ) ) (setq-default mode-line-format nil) (use-package nano-modeline :disabled t :ensure t :hook ( (prog-mode . nano-modeline-prog-mode) (text-mode . nano-modeline-text-mode) (org-mode . nano-modeline-org-mode) (pdf-view-mode . nano-modeline-pdf-mode) (mu4e-headers-mode . nano-modeline-mu4e-headers-mode) (mu4e-view-mode . nano-modeline-mu4e-message-mode) (term-mode . nano-modeline-term-mode) (xwidget-webkit-mode . nano-modeline-xwidget-mode) (messages-buffer-mode . nano-modeline-message-mode) (org-capture-mode . nano-modeline-org-capture-mode) (org-agenda-mode . nano-modeline-org-agenda-mode) ) :config (setq-default mode-line-format nil) ) (use-package beacon :ensure t :delight :custom (beacon-mode t)) (use-package jabber :ensure t :init (setq noa/jabber-activity-dont-show '( "#tildetown%town@irc.hmm.st" "#meta%tilde.chat@irc.hmm.st" )) (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)))) ) ) ) :custom (jabber-history-enabled t) (jabber-account-list '(("noa@hmm.st"))) (jabber-keepalive-interval 60) (noa/jabber-activity-show-p #'noa/jabber-activity-show-p) ) (use-package nov :ensure t :mode ("\\.epub\\'" . nov-mode)) (use-package shr :custom (shr-max-width nil)) ;; also check out jinx https://github.com/minad/jinx (use-package spell-fu :ensure t :hook (text-mode . spell-fu-mode)) (use-package minimap :ensure t :custom (minimap-width-fraction 0.1) (minimap-minimum-width 10) (minimap-window-location 'right) (minimap-update-delay 0.15)) ;; 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) ([remap yank-pop] . consult-yank-pop) ([remap goto-line] . consult-goto-line)) :custom (completion-in-region-function 'consult-completion-in-region) ) (use-package marginalia :ensure t :after vertico :custom (marginalia-mode t) (marginalia-max-relative-age most-positive-fixnum) (marginalia-align 'right)) (global-set-key (kbd "") 'completion-at-point) ;; Add extensions (use-package cape :ensure t :bind ("C-c p f" . cape-file) ("C-c p :" . cape-emoji) :init (add-hook 'completion-at-point-functions #'cape-file) (add-hook 'completion-at-point-functions #'cape-dabbrev) (add-hook 'completion-at-point-functions #'cape-elisp-block) ;;(add-hook 'completion-at-point-functions #'cape-history) ;;(add-hook 'completion-at-point-functions #'cape-keyword) ;;(add-hook 'completion-at-point-functions #'cape-tex) ;;(add-hook 'completion-at-point-functions #'cape-sgml) ;;(add-hook 'completion-at-point-functions #'cape-rfc1345) ;;(add-hook 'completion-at-point-functions #'cape-abbrev) ;;(add-hook 'completion-at-point-functions #'cape-dict) ;;(add-hook 'completion-at-point-functions #'cape-elisp-symbol) ;;(add-hook 'completion-at-point-functions #'cape-line) ) (use-package org-roam :ensure t :after org :after transient :custom (org-roam-directory (expand-file-name "~/data/notes/")) (org-roam-completion-everywhere t) ;; If you're using a vertical completion framework, you might want a more informative completion interface (org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag))) :bind ( ("C-c n" . noa/notes-menu) ) :config (org-roam-db-autosync-mode 1) (require 'org-roam-protocol) ;; Open the org-roam backlinks buffer in a sidebar on the right. I have it set to take up a quarter of the width, which looks nice on my tiny computer, but if i ever use a bigger display, i'll probably have to set it to some amount of characters instead. (add-to-list 'display-buffer-alist '( "^\\*org-roam\\*" display-buffer-in-side-window (side . right) (window-width . 0.25))) (transient-define-prefix noa/notes-menu () "Access org-roam functionality." [("l" "Toggle the backlinks sidebar" org-roam-buffer-toggle :transient nil)] ["Creating notes" ("f" "Open or create a note" org-roam-node-find :transient nil) ("i" "Insert a new or existing note at point" org-roam-node-insert :transient nil) ("c" "Org-roam capture" org-roam-capture :transient nil) ] [ "Interact with the daily journal" ("j" "Add to the daily journal" org-roam-dailies-capture-today :transient nil) ("d" "More daily entry commands" noa/dailies-menu :transient nil) ]) (transient-define-prefix noa/dailies-menu () "Interact with daily entries." [ ("." "Go to the dailies directory" org-roam-dailies-find-directory :transient nil) ("b" "Go to the previous daily note" org-roam-dailies-goto-previous-note :transient nil) ("c" "Go to the daily note for a selected day" org-roam-dailies-goto-date :transient nil) ("d" "Go to today's daily note" org-roam-dailies-goto-today :transient nil) ("f" "Go to the next daily note" org-roam-dailies-goto-next-note :transient nil) ("n" "Add to today's daily note" org-roam-dailies-capture-today :transient nil) ("t" "Go to tomorrow's daily note" org-roam-dailies-goto-tomorrow :transient nil) ("v" "Add to the daily note for a selected day" org-roam-dailies-capture-date :transient nil) ("y" "Go to yesterday's daily note" org-roam-dailies-goto-yesterday :transient nil) ] )) ;; (use-package org-contacts ;; :ensure t ;; :pin gnu ;; :custom ;; (org-contacts-files nil) ;; :config ;; (add-to-list org-roam-capture-templates ;; "c" "Contact" plain "%?" :target (file+head "~/data/notes/contacts/${slug}.org" "#+title: %(org-contacts-template-name)\n") ;; "* %(org-contacts-template-name) ;; :PROPERTIES: ;; :ADDRESS: %^{289 Cleveland St. Brooklyn, 11206 NY, USA} ;; :BIRTHDAY: %^{yyyy-mm-dd} ;; :EMAIL: %(org-contacts-template-email) ;; :NOTE: %^{NOTE} ;; :END:" ;; :empty-lines 1 ;; ) ;; ) (use-package transient :ensure t :commands transient-define-prefix) (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) )) ;; this doesn't seem to work nicely with proportional fonts (use-package adaptive-wrap :disabled :ensure t :hook (visual-line-mode . adaptive-wrap-prefix-mode)) (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-pretty-entities t) (org-use-sub-superscripts "{}") (org-hide-emphasis-markers t) (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) ) ) (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)))) ) (use-package org-appear :disabled t :ensure t :after org :hook (org-mode . org-appear-mode)) (use-package org-modern :ensure t :after org :hook (org-mode . org-modern-mode) (org-agenda-finalize . org-modern-agenda) :custom (org-modern-list '( (?+ . " • ") (?- . " • ") (?* . " • ") )) (org-modern-star nil) (org-modern-keyword nil) (org-modern-checkbox nil) (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")) (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) ) ) (use-package eww :custom (browse-url-browser-function 'eww-browse-url)) (use-package abbrev :hook (text-mode . abbrev-mode)) (use-package eldoc :delight :custom (global-eldoc-mode t)) (use-package rainbow-mode :ensure t :delight :hook (prog-mode . rainbow-mode)) ;; 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) (use-package dired :custom (dired-recursive-deletes 'always) (dired-recursive-copies 'always) (dired-clean-up-buffers-too nil) (delete-by-moving-to-trash t) (dired-dwim-target t) (dired-listing-switches "-alvFh --group-directories-first")) ;;; indentation: tabs and whitespace settings ;; my rules for inserting tabs are that the tab key should insert tabs. i personally prefer tabs. spaces don't work for me because then people go trigger happy with alignment, which doesn't really work with proportional fonts. ;; todo: elastic tabstops ;; https://nickgravgaard.com/elastic-tabstops/ ;; https://github.com/tenbillionwords/spaceship-mode ;; always insert \t for tab. (setopt electric-indent-mode nil backward-delete-char-untabify-method nil tab-width 8) ;; do a naive duplicate whitespace on return, vi/nano style. (define-key global-map (kbd "TAB") 'self-insert-command) (defun naive-return-and-indent () "insert a newline and copy the indentation of the previous line" (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))) (define-key global-map (kbd "RET") 'naive-return-and-indent) ;;; interface ;; we 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. (setopt ;; menu-bar-mode nil ;; tool-bar-mode nil tooltip-mode nil ;; show tooltips in the echo area echo-keystrokes 0.1) ;; show chord progress (almost) immediately (use-package paren :custom (show-paren-mode 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 make-backup-files nil backup-by-copying t create-lockfiles nil auto-save-mode 1 auto-save-interval 20 ;; every twenty keystrokes auto-save-timeout 5 ;; every 5 seconds auto-save-default t auto-save-no-message 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) ) ;;; scrolling (setopt mouse-wheel-scroll-amount '(1 ((shift) . 1)) mouse-wheel-progressive-speed nil mouse-wheel-follow-mouse 't scroll-step 0 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") require-final-newline t 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) (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) ) (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 apropos-do-all t mouse-yank-at-point t require-final-newline t visible-bell t load-prefer-newer t frame-inhibit-implied-resize 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) ;;; search ;; search without regard for unicode characters (setopt search-default-mode 'char-fold-to-regexp replace-char-fold t search-highlight t isearch-lazy-highlight t) ;; when exiting isearch after a match, put the cursor at the beginning of the search rather than the end (setq isearch-mode-end-hook '(goto-beginning-of-search)) (defun goto-beginning-of-search () "positions the point at the beginning of the isearch match" (when (and (not isearch-mode-end-hook-quit) isearch-forward) (goto-char isearch-other-end))) ;; wrap searches (setopt isearch-wrap-pause 'no-ding) ;; jump straight to previous match when change search direction (setopt isearch-repeat-on-direction-change 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 '("♢ " . "♢ ") ) ) (use-package message :custom (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 :ensure t :custom (which-key-popup-type 'side-window) (which-key-side-window-location 'bottom) (which-key-side-window-max-height 3) (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 )) ) (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 ;; symbol 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))) (setopt calendar-week-start-day 1) ;;; 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)