feat(emacs): init
b2bff6b9
13 file(s) · +1481 −0
| 1 | + | ;;; early-init.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | (setq gc-cons-threshold most-positive-fixnum |
|
| 4 | + | gc-cons-percentage 1.0) |
|
| 5 | + | (add-hook 'emacs-startup-hook |
|
| 6 | + | (lambda () (setq gc-cons-threshold (* 16 1024 1024) |
|
| 7 | + | gc-cons-percentage 0.1))) |
|
| 8 | + | ||
| 9 | + | ;; Every `load'/`require' checks file names against these regexps; skip that |
|
| 10 | + | ;; during init and restore afterwards (adapted from minimal-emacs.d) |
|
| 11 | + | (let ((handlers file-name-handler-alist)) |
|
| 12 | + | ;; keep jka-compr so compressed .el.gz libraries still load |
|
| 13 | + | (setq file-name-handler-alist (list (rassq 'jka-compr-handler handlers))) |
|
| 14 | + | (add-hook 'emacs-startup-hook |
|
| 15 | + | (lambda () |
|
| 16 | + | (setq file-name-handler-alist |
|
| 17 | + | (delete-dups (append file-name-handler-alist handlers)))) |
|
| 18 | + | 101)) |
|
| 19 | + | ||
| 20 | + | (setq package-enable-at-startup t |
|
| 21 | + | inhibit-startup-screen t |
|
| 22 | + | inhibit-startup-echo-area-message user-login-name |
|
| 23 | + | frame-inhibit-implied-resize t |
|
| 24 | + | frame-resize-pixelwise t |
|
| 25 | + | auto-mode-case-fold nil) |
|
| 26 | + | ||
| 27 | + | ;; Skip UI chrome before first frame draws |
|
| 28 | + | (push '(menu-bar-lines . 0) default-frame-alist) |
|
| 29 | + | (push '(tool-bar-lines . 0) default-frame-alist) |
|
| 30 | + | (push '(vertical-scroll-bars) default-frame-alist) |
|
| 31 | + | (push '(background-color . "#121113") default-frame-alist) |
|
| 32 | + | (push '(foreground-color . "#c1c1c1") default-frame-alist) |
|
| 33 | + | ||
| 34 | + | ;; macOS: blend the title bar into the frame background |
|
| 35 | + | (when (eq system-type 'darwin) |
|
| 36 | + | (push '(ns-transparent-titlebar . t) default-frame-alist) |
|
| 37 | + | (push '(ns-appearance . dark) default-frame-alist) |
|
| 38 | + | (setq ns-use-proxy-icon nil |
|
| 39 | + | frame-title-format nil)) |
|
| 40 | + | ||
| 41 | + | ;; Keep native-comp noise quiet |
|
| 42 | + | (setq native-comp-async-report-warnings-errors 'silent) |
| 1 | + | ;;; init.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | (defvar my/cache-dir (expand-file-name "emacs/" "~/.cache/")) |
|
| 4 | + | (make-directory my/cache-dir t) |
|
| 5 | + | ||
| 6 | + | (add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory)) |
|
| 7 | + | (add-to-list 'custom-theme-load-path (expand-file-name "themes" user-emacs-directory)) |
|
| 8 | + | (setq custom-file (expand-file-name "custom.el" my/cache-dir)) |
|
| 9 | + | (load custom-file 'noerror 'nomessage) |
|
| 10 | + | ||
| 11 | + | (require 'package) |
|
| 12 | + | (setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/") |
|
| 13 | + | ("nongnu" . "https://elpa.nongnu.org/nongnu/") |
|
| 14 | + | ("melpa" . "https://melpa.org/packages/"))) |
|
| 15 | + | ||
| 16 | + | (setq use-package-always-ensure t) |
|
| 17 | + | ||
| 18 | + | (require 'init-defaults) |
|
| 19 | + | (require 'init-evil) |
|
| 20 | + | (require 'init-ui) |
|
| 21 | + | (require 'init-erc) |
|
| 22 | + | (require 'init-jabber) |
|
| 23 | + | (require 'init-lang) |
|
| 24 | + | (require 'init-tools) |
|
| 25 | + | (require 'init-term) |
| 1 | + | ;;; init-defaults.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | (setq user-full-name "Steve Simkins") |
|
| 4 | + | ||
| 5 | + | ;; Backups, autosaves, lockfiles: keep out of project dirs |
|
| 6 | + | (let ((dir (expand-file-name "backups/" my/cache-dir))) |
|
| 7 | + | (make-directory dir t) |
|
| 8 | + | (setq backup-directory-alist `(("." . ,dir)) |
|
| 9 | + | auto-save-file-name-transforms `((".*" ,dir t)) |
|
| 10 | + | create-lockfiles nil |
|
| 11 | + | backup-by-copying t |
|
| 12 | + | delete-old-versions t |
|
| 13 | + | version-control t)) |
|
| 14 | + | ||
| 15 | + | (setq save-place-file (expand-file-name "places" my/cache-dir) |
|
| 16 | + | savehist-file (expand-file-name "history" my/cache-dir) |
|
| 17 | + | recentf-save-file (expand-file-name "recentf" my/cache-dir) |
|
| 18 | + | recentf-max-saved-items 200) |
|
| 19 | + | ||
| 20 | + | ;; Sane editing |
|
| 21 | + | (setq-default indent-tabs-mode nil |
|
| 22 | + | tab-width 4 |
|
| 23 | + | fill-column 80) |
|
| 24 | + | (setq sentence-end-double-space nil |
|
| 25 | + | require-final-newline t |
|
| 26 | + | ring-bell-function #'ignore |
|
| 27 | + | use-short-answers t |
|
| 28 | + | confirm-kill-emacs #'y-or-n-p |
|
| 29 | + | scroll-conservatively 101 |
|
| 30 | + | scroll-margin 3 |
|
| 31 | + | mouse-wheel-progressive-speed nil |
|
| 32 | + | load-prefer-newer t |
|
| 33 | + | read-process-output-max (* 1024 1024)) |
|
| 34 | + | ||
| 35 | + | ;; Responsiveness / latency |
|
| 36 | + | (setq fast-but-imprecise-scrolling t |
|
| 37 | + | redisplay-skip-fontification-on-input t |
|
| 38 | + | inhibit-compacting-font-caches t |
|
| 39 | + | treesit-font-lock-level 2 |
|
| 40 | + | icomplete-compute-delay 0.01 |
|
| 41 | + | show-paren-delay 0.05 |
|
| 42 | + | which-key-idle-delay 0.5 |
|
| 43 | + | tooltip-delay 0.3 |
|
| 44 | + | tooltip-short-delay 0.08) |
|
| 45 | + | ||
| 46 | + | ;; Built-in minor modes |
|
| 47 | + | (delete-selection-mode 1) |
|
| 48 | + | (electric-pair-mode 1) |
|
| 49 | + | (show-paren-mode 1) |
|
| 50 | + | (save-place-mode 1) |
|
| 51 | + | (savehist-mode 1) |
|
| 52 | + | (recentf-mode 1) |
|
| 53 | + | (global-auto-revert-mode 1) |
|
| 54 | + | (setq global-auto-revert-non-file-buffers t) |
|
| 55 | + | (global-so-long-mode 1) |
|
| 56 | + | (repeat-mode 1) |
|
| 57 | + | (column-number-mode 1) |
|
| 58 | + | (pixel-scroll-precision-mode 1) |
|
| 59 | + | ||
| 60 | + | ;; Completion (built-in, no packages) |
|
| 61 | + | (setq completion-styles '(basic partial-completion flex) |
|
| 62 | + | completions-detailed t |
|
| 63 | + | completion-auto-help 'visible |
|
| 64 | + | completion-auto-select 'second-tab |
|
| 65 | + | tab-always-indent 'complete |
|
| 66 | + | read-file-name-completion-ignore-case t |
|
| 67 | + | read-buffer-completion-ignore-case t) |
|
| 68 | + | (fido-vertical-mode 1) |
|
| 69 | + | ||
| 70 | + | ;; Line numbers in code/text |
|
| 71 | + | (setq display-line-numbers-type 'relative) |
|
| 72 | + | (add-hook 'prog-mode-hook #'display-line-numbers-mode) |
|
| 73 | + | (add-hook 'text-mode-hook #'display-line-numbers-mode) |
|
| 74 | + | ||
| 75 | + | ;; Abbrevs: expand in text/code buffers |
|
| 76 | + | (setq abbrev-file-name (expand-file-name "abbrev_defs" my/cache-dir) |
|
| 77 | + | save-abbrevs 'silently) |
|
| 78 | + | (add-hook 'text-mode-hook #'abbrev-mode) |
|
| 79 | + | (add-hook 'prog-mode-hook #'abbrev-mode) |
|
| 80 | + | (define-abbrev-table 'global-abbrev-table |
|
| 81 | + | '(("legaladvice" "In case of an investigation by any federal entity or similar, I do not have any involvement with this group or with the people in it, I do not know how I am here, probably added by a thrid party, I do not support any actions by the member of this group."))) |
|
| 82 | + | ||
| 83 | + | ;; Trim trailing whitespace on save |
|
| 84 | + | (add-hook 'before-save-hook #'delete-trailing-whitespace) |
|
| 85 | + | ||
| 86 | + | ;; Show warnings/backtraces in a full window instead of a small bottom split |
|
| 87 | + | (add-to-list 'display-buffer-alist |
|
| 88 | + | '("\\`\\*\\(Warnings\\|Backtrace\\|Compile-Log\\)\\*\\'" |
|
| 89 | + | (display-buffer-reuse-window display-buffer-same-window))) |
|
| 90 | + | ||
| 91 | + | ;; macOS |
|
| 92 | + | (when (eq system-type 'darwin) |
|
| 93 | + | (setq mac-command-modifier 'super |
|
| 94 | + | mac-option-modifier 'meta |
|
| 95 | + | ns-use-proxy-icon nil)) |
|
| 96 | + | ||
| 97 | + | ;; GUI emacs: pick up shell PATH (brew, pass, gpg) |
|
| 98 | + | (use-package exec-path-from-shell |
|
| 99 | + | :if (memq window-system '(mac ns)) |
|
| 100 | + | :config (exec-path-from-shell-initialize)) |
|
| 101 | + | ||
| 102 | + | (use-package which-key |
|
| 103 | + | :ensure nil |
|
| 104 | + | :init (which-key-mode 1)) |
|
| 105 | + | ||
| 106 | + | (provide 'init-defaults) |
| 1 | + | ;;; init-erc.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | ;; soju password: read from `pass' (gpg-encrypted), cached in memory per session |
|
| 4 | + | (defvar my/soju-pass-entry "irc/soju") |
|
| 5 | + | (defvar my/soju--password nil) |
|
| 6 | + | ||
| 7 | + | (defun my/soju-password (&rest _) |
|
| 8 | + | (or my/soju--password |
|
| 9 | + | (setq my/soju--password |
|
| 10 | + | (or (auth-source-pass-get 'secret my/soju-pass-entry) |
|
| 11 | + | (user-error "No soju password in pass entry %s" my/soju-pass-entry))))) |
|
| 12 | + | ||
| 13 | + | (defun my/soju-user (client &optional network) |
|
| 14 | + | "Build soju username for CLIENT on NETWORK (default libera)." |
|
| 15 | + | (format "stevedylandev_/%s@%s" (or network "libera") client)) |
|
| 16 | + | ||
| 17 | + | (with-eval-after-load 'erc |
|
| 18 | + | (setq erc-server "138.197.115.89" |
|
| 19 | + | erc-port 6697 |
|
| 20 | + | erc-nick "stevedylandev_" |
|
| 21 | + | erc-user-full-name "Steve Simkins" |
|
| 22 | + | erc-prompt-for-password nil |
|
| 23 | + | erc-track-shorten-start 8 |
|
| 24 | + | erc-kill-buffer-on-part t |
|
| 25 | + | erc-kill-queries-on-quit t |
|
| 26 | + | erc-hide-list '("JOIN" "PART" "QUIT") |
|
| 27 | + | erc-nicks-skip-nicks '("ChanServ" "NickServ")) |
|
| 28 | + | (add-to-list 'erc-modules 'nicks) |
|
| 29 | + | (erc-update-modules)) |
|
| 30 | + | ||
| 31 | + | (defun my/erc-soju () |
|
| 32 | + | "Connect ERC to soju over TLS." |
|
| 33 | + | (interactive) |
|
| 34 | + | (erc-tls :server "138.197.115.89" |
|
| 35 | + | :port 6697 |
|
| 36 | + | :nick "stevedylandev_" |
|
| 37 | + | :user (my/soju-user "erc-benson") |
|
| 38 | + | :password (my/soju-password))) |
|
| 39 | + | ||
| 40 | + | (provide 'init-erc) |
| 1 | + | ;;; init-evil.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | (use-package evil |
|
| 4 | + | :init |
|
| 5 | + | (setq evil-want-integration t |
|
| 6 | + | evil-want-keybinding nil ; evil-collection handles other modes |
|
| 7 | + | evil-want-C-u-scroll t |
|
| 8 | + | evil-want-Y-yank-to-eol t |
|
| 9 | + | evil-undo-system 'undo-redo |
|
| 10 | + | evil-split-window-below t |
|
| 11 | + | evil-vsplit-window-right t |
|
| 12 | + | evil-ex-hl-update-delay 0.01) |
|
| 13 | + | :config |
|
| 14 | + | (evil-mode 1)) |
|
| 15 | + | ||
| 16 | + | (use-package evil-collection |
|
| 17 | + | :after evil |
|
| 18 | + | :config |
|
| 19 | + | (evil-collection-init)) |
|
| 20 | + | ||
| 21 | + | (use-package general |
|
| 22 | + | :after evil |
|
| 23 | + | :config |
|
| 24 | + | (general-create-definer my/leader |
|
| 25 | + | :states '(normal visual motion insert emacs) |
|
| 26 | + | :keymaps 'override |
|
| 27 | + | :prefix "SPC" |
|
| 28 | + | :global-prefix "M-SPC") |
|
| 29 | + | ||
| 30 | + | (my/leader |
|
| 31 | + | "SPC" '(project-find-file :wk "project file") |
|
| 32 | + | ":" '(execute-extended-command :wk "M-x") |
|
| 33 | + | "." '(find-file :wk "find file") |
|
| 34 | + | "," '(switch-to-buffer :wk "switch buffer") |
|
| 35 | + | "/" '(project-find-regexp :wk "project grep") |
|
| 36 | + | ||
| 37 | + | "X" '(org-capture :wk "org capture") |
|
| 38 | + | ||
| 39 | + | "b" '(:ignore t :wk "buffer") |
|
| 40 | + | "bb" '(switch-to-buffer :wk "switch") |
|
| 41 | + | "bi" '(ibuffer :wk "ibuffer") |
|
| 42 | + | "bd" '(kill-current-buffer :wk "kill") |
|
| 43 | + | "bn" '(next-buffer :wk "next") |
|
| 44 | + | "bp" '(previous-buffer :wk "previous") |
|
| 45 | + | "bs" '(save-buffer :wk "save") |
|
| 46 | + | ||
| 47 | + | "f" '(:ignore t :wk "file") |
|
| 48 | + | "ff" '(find-file :wk "find") |
|
| 49 | + | "fr" '(recentf-open :wk "recent") |
|
| 50 | + | "fs" '(save-buffer :wk "save") |
|
| 51 | + | "fd" '(dired-jump :wk "dired") |
|
| 52 | + | ||
| 53 | + | "g" '(:ignore t :wk "git") |
|
| 54 | + | "gg" '(magit-status :wk "status") |
|
| 55 | + | "gb" '(magit-blame :wk "blame") |
|
| 56 | + | "gl" '(magit-log-current :wk "log") |
|
| 57 | + | ||
| 58 | + | "o" '(:ignore t :wk "open") |
|
| 59 | + | "ow" '(eww :wk "eww") |
|
| 60 | + | "oe" '(elfeed :wk "elfeed") |
|
| 61 | + | "oi" '(my/erc-soju :wk "irc") |
|
| 62 | + | "oj" '(jabber-roster :wk "jabber") |
|
| 63 | + | "om" '(mu4e :wk "mail") |
|
| 64 | + | "ot" '(ghostel :wk "terminal") |
|
| 65 | + | "oT" '(ghostel-project :wk "project terminal") |
|
| 66 | + | "os" '(eshell :wk "eshell") |
|
| 67 | + | ||
| 68 | + | "ll" '(gptel :wk "gptel") |
|
| 69 | + | "la" '(gptel-add :wk "add context") |
|
| 70 | + | "lr" '(gptel-context-remove-all :wk "remove context") |
|
| 71 | + | "lm" '(gptel-menu :wk "menu") |
|
| 72 | + | "ls" '(gptel-send :wk "send") |
|
| 73 | + | ||
| 74 | + | "u" '(:ignore t :wk "upload") |
|
| 75 | + | "uu" '(uguu-upload-file :wk "upload file") |
|
| 76 | + | "uc" '(uguu-upload-clipboard :wk "upload clipboard") |
|
| 77 | + | ||
| 78 | + | "P" '(posts-compose :wk "new post") |
|
| 79 | + | ||
| 80 | + | "p" '(:ignore t :wk "project") |
|
| 81 | + | "pp" '(project-switch-project :wk "switch") |
|
| 82 | + | "pf" '(project-find-file :wk "find file") |
|
| 83 | + | "pg" '(project-find-regexp :wk "grep") |
|
| 84 | + | "pb" '(project-switch-to-buffer :wk "buffer") |
|
| 85 | + | ||
| 86 | + | "s" '(:ignore t :wk "search") |
|
| 87 | + | "ss" '(isearch-forward :wk "isearch") |
|
| 88 | + | "so" '(occur :wk "occur") |
|
| 89 | + | ||
| 90 | + | "w" '(:ignore t :wk "window") |
|
| 91 | + | "ww" '(other-window :wk "other") |
|
| 92 | + | "wv" '(evil-window-vsplit :wk "vsplit") |
|
| 93 | + | "ws" '(evil-window-split :wk "split") |
|
| 94 | + | "wd" '(delete-window :wk "close") |
|
| 95 | + | "wo" '(delete-other-windows :wk "only") |
|
| 96 | + | "wh" '(evil-window-left :wk "left") |
|
| 97 | + | "wj" '(evil-window-down :wk "down") |
|
| 98 | + | "wk" '(evil-window-up :wk "up") |
|
| 99 | + | "wl" '(evil-window-right :wk "right") |
|
| 100 | + | "w=" '(balance-windows :wk "balance") |
|
| 101 | + | ||
| 102 | + | "h" '(:ignore t :wk "help") |
|
| 103 | + | "hf" '(describe-function :wk "function") |
|
| 104 | + | "hv" '(describe-variable :wk "variable") |
|
| 105 | + | "hk" '(describe-key :wk "key") |
|
| 106 | + | "hm" '(describe-mode :wk "mode") |
|
| 107 | + | ||
| 108 | + | "q" '(:ignore t :wk "quit") |
|
| 109 | + | "qq" '(save-buffers-kill-terminal :wk "quit emacs"))) |
|
| 110 | + | ||
| 111 | + | (provide 'init-evil) |
| 1 | + | ;;; init-jabber.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | ;; Built from git for OMEMO. Native module needs mbedtls 3.x headers, gcc, |
|
| 4 | + | ;; make and pkg-config on PATH (4.x dropped mbedtls/aes.h). |
|
| 5 | + | (use-package jabber |
|
| 6 | + | :vc (:url "https://git.thanosapollo.org/emacs-jabber" |
|
| 7 | + | :lisp-dir "lisp" |
|
| 8 | + | :shell-command "MBED_STATIC=1 make module") |
|
| 9 | + | :defer t |
|
| 10 | + | :init |
|
| 11 | + | (with-eval-after-load 'jabber-roster-menu (require 'jabber)) |
|
| 12 | + | :config |
|
| 13 | + | (setq jabber-account-list |
|
| 14 | + | `(("stevedylandev@xmpp.is" |
|
| 15 | + | (:password . ,(auth-source-pass-get 'secret "xmpp/stevedylandev@xmpp.is"))))) |
|
| 16 | + | (jabber-modeline-mode 1)) |
|
| 17 | + | ||
| 18 | + | (with-eval-after-load 'jabber-omemo-trust |
|
| 19 | + | (evil-define-key 'normal jabber-omemo-trust-mode-map |
|
| 20 | + | "t" #'jabber-omemo-trust-set-verified |
|
| 21 | + | "u" #'jabber-omemo-trust-set-untrusted |
|
| 22 | + | "w" #'jabber-omemo-trust-copy-fingerprint |
|
| 23 | + | "r" #'jabber-omemo-reset-session |
|
| 24 | + | "d" #'jabber-omemo-trust-delete |
|
| 25 | + | "G" #'jabber-omemo-trust-refresh |
|
| 26 | + | "?" #'jabber-omemo-trust-menu)) |
|
| 27 | + | ||
| 28 | + | (setq jabber-muc-decorate-presence-patterns |
|
| 29 | + | '(("."))) ; "Hide all" |
|
| 30 | + | ||
| 31 | + | (provide 'init-jabber) |
| 1 | + | ;;; init-lang.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | ;;; Syntax highlighting: tree-sitter |
|
| 4 | + | (setopt treesit-enabled-modes t |
|
| 5 | + | treesit-auto-install-grammar 'always |
|
| 6 | + | treesit-font-lock-level 4) ; 4 = everything (calls, properties, operators) |
|
| 7 | + | ||
| 8 | + | ;;; LSP: eglot |
|
| 9 | + | (defvar my/lsp-servers |
|
| 10 | + | '(((typescript-ts-mode tsx-ts-mode js-ts-mode) "vtsls" "--stdio") |
|
| 11 | + | (rust-ts-mode "rust-analyzer") |
|
| 12 | + | ((go-ts-mode go-mod-ts-mode) "gopls") |
|
| 13 | + | ((c-ts-mode c++-ts-mode) "clangd") |
|
| 14 | + | (lua-ts-mode "lua-language-server") |
|
| 15 | + | ;;(css-ts-mode "vscode-css-language-server" "--stdio") |
|
| 16 | + | ((mhtml-ts-mode html-ts-mode) "vscode-html-language-server" "--stdio") |
|
| 17 | + | (json-ts-mode "vscode-json-language-server" "--stdio") |
|
| 18 | + | (python-ts-mode "pylsp") |
|
| 19 | + | (python-ts-mode "basedpyright-langserver" "--stdio") ; wins if both installed |
|
| 20 | + | (bash-ts-mode "bash-language-server" "start") |
|
| 21 | + | (scheme-mode "guile-lsp-server")) |
|
| 22 | + | "Alist of (MODES SERVER ARGS...) used to auto-start eglot.") |
|
| 23 | + | ||
| 24 | + | (use-package eglot |
|
| 25 | + | :ensure nil |
|
| 26 | + | :defer t |
|
| 27 | + | :custom |
|
| 28 | + | (eglot-autoshutdown t) ; kill server when last buffer closes |
|
| 29 | + | (eglot-events-buffer-config '(:size 0 :format full)) ; no event logging |
|
| 30 | + | (eglot-extend-to-xref t) ; stay in eglot when jumping into deps |
|
| 31 | + | :config |
|
| 32 | + | (dolist (entry my/lsp-servers) |
|
| 33 | + | (when (executable-find (cadr entry)) |
|
| 34 | + | (add-to-list 'eglot-server-programs |
|
| 35 | + | (cons (ensure-list (car entry)) (cdr entry)))))) |
|
| 36 | + | ||
| 37 | + | ;; Hooks go in now so eglot itself only loads when a matching buffer opens |
|
| 38 | + | (dolist (entry my/lsp-servers) |
|
| 39 | + | (when (executable-find (cadr entry)) |
|
| 40 | + | (dolist (mode (ensure-list (car entry))) |
|
| 41 | + | (add-hook (intern (format "%s-hook" mode)) #'eglot-ensure)))) |
|
| 42 | + | ||
| 43 | + | ;;; Indentation per language |
|
| 44 | + | (defvar my/indent-settings |
|
| 45 | + | '(((typescript-ts-mode tsx-ts-mode) 2 nil typescript-ts-mode-indent-offset) |
|
| 46 | + | (js-ts-mode 2 nil js-indent-level) |
|
| 47 | + | (rust-ts-mode 4 nil rust-ts-mode-indent-offset) |
|
| 48 | + | (go-ts-mode 4 t go-ts-mode-indent-offset) |
|
| 49 | + | ((c-ts-mode c++-ts-mode) 4 nil c-ts-mode-indent-offset) |
|
| 50 | + | (lua-ts-mode 2 nil lua-ts-indent-offset) |
|
| 51 | + | (css-ts-mode 2 nil css-indent-offset) |
|
| 52 | + | ((mhtml-ts-mode html-ts-mode) 2 nil html-ts-mode-indent-offset) |
|
| 53 | + | (json-ts-mode 2 nil json-ts-mode-indent-offset) |
|
| 54 | + | (python-ts-mode 4 nil python-indent-offset) |
|
| 55 | + | (bash-ts-mode 2 nil sh-basic-offset) |
|
| 56 | + | ((emacs-lisp-mode scheme-mode) 2 nil nil)) |
|
| 57 | + | "List of (MODES WIDTH TABS OFFSET-VAR) applied in each mode's hook.") |
|
| 58 | + | ||
| 59 | + | (dolist (entry my/indent-settings) |
|
| 60 | + | (pcase-let ((`(,modes ,width ,tabs ,var) entry)) |
|
| 61 | + | (dolist (mode (ensure-list modes)) |
|
| 62 | + | (add-hook (intern (format "%s-hook" mode)) |
|
| 63 | + | (lambda () |
|
| 64 | + | (setq-local tab-width width |
|
| 65 | + | indent-tabs-mode tabs) |
|
| 66 | + | (when var (set (make-local-variable var) width))))))) |
|
| 67 | + | ||
| 68 | + | ;;; Format on save |
|
| 69 | + | ;; Uses eglot when the server can format, else re-indents lisp buffers. Off for a |
|
| 70 | + | ;; buffer with SPC c F, or per project via .dir-locals.el: |
|
| 71 | + | (defvar my/format-on-save t |
|
| 72 | + | "Non-nil formats eglot-managed buffers before saving.") |
|
| 73 | + | (put 'my/format-on-save 'safe-local-variable #'booleanp) |
|
| 74 | + | ||
| 75 | + | (defvar my/reindent-modes '(emacs-lisp-mode scheme-mode) |
|
| 76 | + | "Modes without an LSP formatter that format by re-indenting the buffer.") |
|
| 77 | + | ||
| 78 | + | (defun my/format-buffer () |
|
| 79 | + | "Format with eglot if the server can, else re-indent in `my/reindent-modes'." |
|
| 80 | + | (interactive) |
|
| 81 | + | (cond ((and (fboundp 'eglot-managed-p) (eglot-managed-p) |
|
| 82 | + | (eglot-server-capable :documentFormattingProvider)) |
|
| 83 | + | (eglot-format-buffer)) |
|
| 84 | + | ((apply #'derived-mode-p my/reindent-modes) |
|
| 85 | + | (indent-region (point-min) (point-max))) |
|
| 86 | + | ((called-interactively-p 'any) |
|
| 87 | + | (message "No formatter for %s" major-mode)))) |
|
| 88 | + | ||
| 89 | + | (defun my/format-on-save () |
|
| 90 | + | (when my/format-on-save (my/format-buffer))) |
|
| 91 | + | ||
| 92 | + | (add-hook 'before-save-hook #'my/format-on-save) |
|
| 93 | + | ||
| 94 | + | (defun my/toggle-format-on-save () |
|
| 95 | + | "Toggle format on save for the current buffer." |
|
| 96 | + | (interactive) |
|
| 97 | + | (setq-local my/format-on-save (not my/format-on-save)) |
|
| 98 | + | (message "Format on save %s" (if my/format-on-save "on" "off"))) |
|
| 99 | + | ||
| 100 | + | ;; Diagnostics |
|
| 101 | + | (setq flymake-show-diagnostics-at-end-of-line 'short) |
|
| 102 | + | ||
| 103 | + | (with-eval-after-load 'general |
|
| 104 | + | (my/leader |
|
| 105 | + | "c" '(:ignore t :wk "code") |
|
| 106 | + | "ca" '(eglot-code-actions :wk "actions") |
|
| 107 | + | "cr" '(eglot-rename :wk "rename") |
|
| 108 | + | "cf" '(my/format-buffer :wk "format") |
|
| 109 | + | "cF" '(my/toggle-format-on-save :wk "toggle format on save") |
|
| 110 | + | "cd" '(xref-find-definitions :wk "definition") |
|
| 111 | + | "cR" '(xref-find-references :wk "references") |
|
| 112 | + | "ci" '(eglot-find-implementation :wk "implementation") |
|
| 113 | + | "ct" '(eglot-find-typeDefinition :wk "type def") |
|
| 114 | + | "ce" '(flymake-show-buffer-diagnostics :wk "errors") |
|
| 115 | + | "cE" '(flymake-show-project-diagnostics :wk "project errors") |
|
| 116 | + | "cn" '(flymake-goto-next-error :wk "next error") |
|
| 117 | + | "cp" '(flymake-goto-prev-error :wk "prev error") |
|
| 118 | + | "cl" '(eglot :wk "start lsp") |
|
| 119 | + | "cL" '(eglot-shutdown :wk "stop lsp"))) |
|
| 120 | + | ||
| 121 | + | (provide 'init-lang) |
| 1 | + | ;;; init-term.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | ;; Ghostel: libghostty-vt backed terminal. Native module downloads on first use. |
|
| 4 | + | (use-package ghostel |
|
| 5 | + | :commands (ghostel ghostel-project) |
|
| 6 | + | :init |
|
| 7 | + | (setq ghostel-timer-delay 0.01 |
|
| 8 | + | ghostel-max-scrollback (* 1024 1024))) |
|
| 9 | + | ||
| 10 | + | (use-package evil-ghostel |
|
| 11 | + | :after (ghostel evil) |
|
| 12 | + | :hook (ghostel-mode . evil-ghostel-mode)) |
|
| 13 | + | ||
| 14 | + | ;; Terminal perf tweaks, adapted from James Cherti (MIT): |
|
| 15 | + | (defun my/speed-up-terminal-buffer () |
|
| 16 | + | "Reduce unnecessary Emacs features in terminal buffers." |
|
| 17 | + | (let ((ghostel-buffer (derived-mode-p 'ghostel-mode))) |
|
| 18 | + | (setq-local font-lock-defaults '(nil t)) |
|
| 19 | + | ||
| 20 | + | (setq-local scroll-conservatively most-positive-fixnum) |
|
| 21 | + | (setq-local hscroll-margin 0) |
|
| 22 | + | (setq-local scroll-margin 0) |
|
| 23 | + | (setq-local auto-hscroll-mode nil) |
|
| 24 | + | ||
| 25 | + | (setq-local truncate-lines t) |
|
| 26 | + | (setq-local nobreak-char-display nil) |
|
| 27 | + | (setq-local bidi-paragraph-direction 'left-to-right) |
|
| 28 | + | (setq-local bidi-inhibit-bpa t) |
|
| 29 | + | ||
| 30 | + | ;; Ghostel computes row height from ghostel-line-spacing |
|
| 31 | + | (unless ghostel-buffer |
|
| 32 | + | (setq-local line-spacing 0) |
|
| 33 | + | (setq-local mode-line-format nil)) |
|
| 34 | + | ||
| 35 | + | (setq-local process-adaptive-read-buffering nil) |
|
| 36 | + | (let ((output-max (* 1024 1024))) |
|
| 37 | + | (when (< read-process-output-max output-max) |
|
| 38 | + | (setq-local read-process-output-max output-max))) |
|
| 39 | + | ||
| 40 | + | (buffer-disable-undo) |
|
| 41 | + | ||
| 42 | + | ;; Evil jump list hooks (C-o / C-i won't work in terminals) |
|
| 43 | + | (remove-hook 'pre-command-hook 'evil--jump-hook t) |
|
| 44 | + | (remove-hook 'post-command-hook 'evil--jump-handle-buffer-crossing t) |
|
| 45 | + | ||
| 46 | + | (let ((inhibit-redisplay t) |
|
| 47 | + | (inhibit-message t) |
|
| 48 | + | (modes (list 'electric-pair-local-mode |
|
| 49 | + | 'electric-indent-local-mode |
|
| 50 | + | 'display-line-numbers-mode |
|
| 51 | + | 'display-fill-column-indicator-mode |
|
| 52 | + | 'hl-line-mode |
|
| 53 | + | 'show-paren-local-mode |
|
| 54 | + | 'flymake-mode |
|
| 55 | + | 'flycheck-mode |
|
| 56 | + | 'evil-surround-mode |
|
| 57 | + | 'yas-minor-mode |
|
| 58 | + | 'company-mode |
|
| 59 | + | 'corfu-mode))) |
|
| 60 | + | ;; Ghostel relies on eldoc (link targets) and auto-composition |
|
| 61 | + | (unless ghostel-buffer |
|
| 62 | + | (push 'eldoc-mode modes) |
|
| 63 | + | (push 'auto-composition-mode modes)) |
|
| 64 | + | (dolist (mode modes) |
|
| 65 | + | (when (fboundp mode) |
|
| 66 | + | (ignore-errors (funcall mode -1))))))) |
|
| 67 | + | ||
| 68 | + | (add-hook 'term-mode-hook #'my/speed-up-terminal-buffer t) |
|
| 69 | + | (add-hook 'ghostel-mode-hook #'my/speed-up-terminal-buffer t) |
|
| 70 | + | ||
| 71 | + | (provide 'init-term) |
| 1 | + | ;;; init-tools.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | (use-package magit |
|
| 4 | + | :defer t) |
|
| 5 | + | ||
| 6 | + | (use-package diff-hl |
|
| 7 | + | :hook ((after-init . global-diff-hl-mode) |
|
| 8 | + | (dired-mode . diff-hl-dired-mode) |
|
| 9 | + | (magit-pre-refresh . diff-hl-magit-pre-refresh) |
|
| 10 | + | (magit-post-refresh . diff-hl-magit-post-refresh)) |
|
| 11 | + | :custom |
|
| 12 | + | (diff-hl-margin-symbols-alist |
|
| 13 | + | '((insert . "+") (delete . "-") (change . "~") |
|
| 14 | + | (unknown . "?") (ignored . "i") (reference . " "))) |
|
| 15 | + | (diff-hl-draw-borders nil) |
|
| 16 | + | :config |
|
| 17 | + | (diff-hl-flydiff-mode 1) |
|
| 18 | + | (diff-hl-margin-mode 1) |
|
| 19 | + | ;; base16-theme already defines these faces, so the theme file can't |
|
| 20 | + | ;; override them; set them here instead. |
|
| 21 | + | (custom-set-faces |
|
| 22 | + | '(diff-hl-insert ((t (:foreground "#fbcb97" :background "#fbcb97")))) |
|
| 23 | + | '(diff-hl-change ((t (:foreground "#e78a53" :background "#e78a53")))) |
|
| 24 | + | '(diff-hl-delete ((t (:foreground "#aa6c6c" :background "#aa6c6c")))) |
|
| 25 | + | ;; margin text: color the symbol only, no filled background |
|
| 26 | + | '(diff-hl-margin-insert ((t (:foreground "#fbcb97" :background unspecified :weight bold)))) |
|
| 27 | + | '(diff-hl-margin-change ((t (:foreground "#e78a53" :background unspecified :weight bold)))) |
|
| 28 | + | '(diff-hl-margin-delete ((t (:foreground "#aa6c6c" :background unspecified :weight bold)))))) |
|
| 29 | + | ||
| 30 | + | (use-package elfeed |
|
| 31 | + | :defer t |
|
| 32 | + | :config |
|
| 33 | + | (setq-default elfeed-search-filter "@6-months-ago") |
|
| 34 | + | ||
| 35 | + | (defvar my/elfeed-feeds-url "https://feeds.stevedylan.dev/feeds?format=json" |
|
| 36 | + | "Endpoint returning my subscriptions as JSON.") |
|
| 37 | + | ||
| 38 | + | (defun my/elfeed-refresh-feeds () |
|
| 39 | + | "Replace `elfeed-feeds' with the list from `my/elfeed-feeds-url'. |
|
| 40 | + | Keeps the current list if the fetch fails." |
|
| 41 | + | (interactive) |
|
| 42 | + | (condition-case err |
|
| 43 | + | (let ((buf (url-retrieve-synchronously my/elfeed-feeds-url t t 10))) |
|
| 44 | + | (unless buf (error "no response")) |
|
| 45 | + | (with-current-buffer buf |
|
| 46 | + | (goto-char (point-min)) |
|
| 47 | + | (re-search-forward "\n\n") |
|
| 48 | + | (let* ((data (json-parse-buffer :object-type 'alist :array-type 'list)) |
|
| 49 | + | (urls (mapcar (lambda (s) (alist-get 'url s)) |
|
| 50 | + | (alist-get 'subscriptions data)))) |
|
| 51 | + | (kill-buffer buf) |
|
| 52 | + | (setq elfeed-feeds urls) |
|
| 53 | + | (message "elfeed: loaded %d feeds" (length urls))))) |
|
| 54 | + | (error (message "elfeed: feed refresh failed (%s), keeping existing list" |
|
| 55 | + | (error-message-string err))))) |
|
| 56 | + | ||
| 57 | + | (advice-add 'elfeed-update :before #'my/elfeed-refresh-feeds) |
|
| 58 | + | ||
| 59 | + | (defun my/elfeed-show-font () |
|
| 60 | + | (face-remap-add-relative 'variable-pitch :family "BerkeleyMono Nerd Font Mono" :height 140) |
|
| 61 | + | (face-remap-add-relative 'default :family "BerkeleyMono Nerd Font Mono" :height 140)) |
|
| 62 | + | (add-hook 'elfeed-show-mode-hook #'my/elfeed-show-font)) |
|
| 63 | + | ||
| 64 | + | (use-package ob-go |
|
| 65 | + | :defer t) |
|
| 66 | + | ||
| 67 | + | (use-package org |
|
| 68 | + | :ensure nil |
|
| 69 | + | :defer t |
|
| 70 | + | :config |
|
| 71 | + | (require 'ob-go) |
|
| 72 | + | (org-babel-do-load-languages |
|
| 73 | + | 'org-babel-load-languages |
|
| 74 | + | '((js . t) |
|
| 75 | + | (shell . t) |
|
| 76 | + | (go . t))) |
|
| 77 | + | ||
| 78 | + | (setq org-directory "~/org/" |
|
| 79 | + | org-default-notes-file (expand-file-name "inbox.org" org-directory) |
|
| 80 | + | org-capture-templates |
|
| 81 | + | '(("t" "Todo" entry (file+headline "inbox.org" "Tasks") |
|
| 82 | + | "* TODO %?\n%i\n%a" :empty-lines 1) |
|
| 83 | + | ("n" "Note" entry (file+headline "inbox.org" "Notes") |
|
| 84 | + | "* %?\n%U\n%i" :empty-lines 1) |
|
| 85 | + | ("j" "Journal" entry (file+olp+datetree "journal.org") |
|
| 86 | + | "* %U %?\n%i" :empty-lines 1) |
|
| 87 | + | ("b" "Bookmark" entry (file "bookmarks.org") |
|
| 88 | + | "* [[%^{URL}][%^{Title}]] %^g\n:PROPERTIES:\n:ADDED: %U\n:END:\n%?" |
|
| 89 | + | :empty-lines 1)))) |
|
| 90 | + | ||
| 91 | + | (use-package dired |
|
| 92 | + | :ensure nil |
|
| 93 | + | :custom |
|
| 94 | + | (dired-kill-when-opening-new-dired-buffer t) ; reuse one buffer |
|
| 95 | + | (dired-dwim-target t) |
|
| 96 | + | :hook (dired-mode . dired-omit-mode) ; hide dotfiles by default |
|
| 97 | + | :config |
|
| 98 | + | (require 'dired-x) |
|
| 99 | + | (setq dired-omit-files "\\`[.][^.]") ; dotfiles, but keep ".." |
|
| 100 | + | (evil-collection-define-key 'normal 'dired-mode-map |
|
| 101 | + | "." 'dired-omit-mode ; toggle dotfiles |
|
| 102 | + | "h" 'dired-up-directory |
|
| 103 | + | "l" 'dired-find-file |
|
| 104 | + | "q" 'quit-window)) |
|
| 105 | + | ||
| 106 | + | (autoload 'posts-compose "posts" nil t) |
|
| 107 | + | (autoload 'uguu-upload-file "uguu" nil t) |
|
| 108 | + | (autoload 'uguu-upload-clipboard "uguu" nil t) |
|
| 109 | + | ||
| 110 | + | (defgroup openrouter nil |
|
| 111 | + | "OpenRouter backend for gptel." |
|
| 112 | + | :group 'gptel) |
|
| 113 | + | ||
| 114 | + | (defcustom openrouter-pass-entry "openrouter/openrouter-api-key" |
|
| 115 | + | "Name of the `pass' entry holding the API key." |
|
| 116 | + | :type 'string |
|
| 117 | + | :group 'openrouter) |
|
| 118 | + | ||
| 119 | + | (defun openrouter--api-key () |
|
| 120 | + | "Return the OpenRouter API key from password-store." |
|
| 121 | + | (require 'auth-source-pass) ; autoloaded in Emacs 28+, but cheap to be explicit |
|
| 122 | + | (let ((key (auth-source-pass-get 'secret openrouter-pass-entry))) |
|
| 123 | + | (or (and key (string-trim key)) |
|
| 124 | + | (user-error "No pass entry `%s'" openrouter-pass-entry)))) |
|
| 125 | + | ||
| 126 | + | (use-package gptel |
|
| 127 | + | :defer t |
|
| 128 | + | :config |
|
| 129 | + | (setq gptel-backend |
|
| 130 | + | (gptel-make-openai "OpenRouter" |
|
| 131 | + | :host "openrouter.ai" |
|
| 132 | + | :endpoint "/api/v1/chat/completions" |
|
| 133 | + | :stream t |
|
| 134 | + | :key #'openrouter--api-key |
|
| 135 | + | :request-params '(:tools [(:type "openrouter:web_search") |
|
| 136 | + | (:type "openrouter:web_fetch")]) |
|
| 137 | + | :models '(z-ai/glm-5.3 |
|
| 138 | + | z-ai/glm-5.3-flash |
|
| 139 | + | z-ai/glm-5.3-flashx)))) |
|
| 140 | + | ||
| 141 | + | (defun gptel-model (model) |
|
| 142 | + | "Set the gptel model in the current buffer." |
|
| 143 | + | (interactive |
|
| 144 | + | (list (intern (completing-read "Model: " |
|
| 145 | + | '("z-ai/glm-5.3" |
|
| 146 | + | "z-ai/glm-5.3-flash" |
|
| 147 | + | "z-ai/glm-5.3-flashx"))))) |
|
| 148 | + | (setq-local gptel-model model)) |
|
| 149 | + | ||
| 150 | + | (use-package markdown-mode |
|
| 151 | + | :defer t) |
|
| 152 | + | ||
| 153 | + | ||
| 154 | + | (provide 'init-tools) |
| 1 | + | ;;; init-ui.el -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | (when (display-graphic-p) |
|
| 4 | + | (set-face-attribute 'default nil :family "BerkeleyMono Nerd Font" :height 110)) |
|
| 5 | + | ||
| 6 | + | (use-package base16-theme) |
|
| 7 | + | (use-package nord-theme) |
|
| 8 | + | ||
| 9 | + | (load-theme 'nord t) |
|
| 10 | + | ||
| 11 | + | ;;; Mode line |
|
| 12 | + | ;; Evil's own indicator is dropped; the state is drawn by hand as a colored |
|
| 13 | + | ;; square with the state letter. Icons are Nerd Font glyphs. |
|
| 14 | + | (setq evil-mode-line-format nil |
|
| 15 | + | evil-echo-state nil) ; no "-- INSERT --" in the echo area |
|
| 16 | + | (column-number-mode 1) |
|
| 17 | + | ||
| 18 | + | ;; (defvar my/ml-states |
|
| 19 | + | ;; `((normal "N" ,(alist-get 'base08 darkmatter-palette)) |
|
| 20 | + | ;; (insert "I" ,(alist-get 'base0A darkmatter-palette)) |
|
| 21 | + | ;; (visual "V" ,(alist-get 'base0B darkmatter-palette)) |
|
| 22 | + | ;; (replace "R" ,(alist-get 'red darkmatter-palette)) |
|
| 23 | + | ;; (operator "O" ,(alist-get 'base0D darkmatter-palette)) |
|
| 24 | + | ;; (motion "M" ,(alist-get 'base0D darkmatter-palette)) |
|
| 25 | + | ;; (emacs "E" ,(alist-get 'base09 darkmatter-palette))) |
|
| 26 | + | ;; "Evil state -> (letter color) for the mode line.") |
|
| 27 | + | ||
| 28 | + | ;; (defun my/ml-evil-state () |
|
| 29 | + | ;; "Colored square with the current evil state letter." |
|
| 30 | + | ;; (when (bound-and-true-p evil-local-mode) |
|
| 31 | + | ;; (pcase-let ((`(,letter ,color) (or (alist-get evil-state my/ml-states) |
|
| 32 | + | ;; '("?" "#666666")))) |
|
| 33 | + | ;; (propertize (format " %s " letter) |
|
| 34 | + | ;; 'face `(:background ,color |
|
| 35 | + | ;; :foreground ,(alist-get 'base00 darkmatter-palette) |
|
| 36 | + | ;; :weight bold))))) |
|
| 37 | + | ||
| 38 | + | (setq-default |
|
| 39 | + | mode-line-format |
|
| 40 | + | '("%e" |
|
| 41 | + | (:eval (my/ml-evil-state)) |
|
| 42 | + | " " |
|
| 43 | + | (:eval (when (and buffer-file-name (buffer-modified-p)) |
|
| 44 | + | (propertize "\uf111 " 'face '(:foreground "#e78a53")))) ; nf-fa-circle |
|
| 45 | + | (:propertize "%b" face mode-line-buffer-id) |
|
| 46 | + | (:eval (when buffer-read-only " \uf023")) ; nf-fa-lock |
|
| 47 | + | " " |
|
| 48 | + | mode-name |
|
| 49 | + | (:eval (when vc-mode |
|
| 50 | + | (concat " \ue0a0 " ; branch |
|
| 51 | + | (string-trim (replace-regexp-in-string "^ *Git[-:]" "" vc-mode))))) |
|
| 52 | + | mode-line-format-right-align |
|
| 53 | + | ;; ERC / jabber activity ([#chan,...]) lives here |
|
| 54 | + | (:eval (when (string-match-p "[^ ]" (format-mode-line global-mode-string)) |
|
| 55 | + | global-mode-string)) |
|
| 56 | + | " %l:%c %p ")) |
|
| 57 | + | ||
| 58 | + | (setq initial-scratch-message |
|
| 59 | + | " . + . . . . . . |
|
| 60 | + | . . . * |
|
| 61 | + | . * . . . . . . + . |
|
| 62 | + | \"You Are Here\" . . + . . . |
|
| 63 | + | . | . . . . . . |
|
| 64 | + | | . . . +. + . |
|
| 65 | + | \\|/ . . . . |
|
| 66 | + | . . V . * . . . . + . |
|
| 67 | + | + . . . + |
|
| 68 | + | . . + .+. . |
|
| 69 | + | . . . + . . . . . |
|
| 70 | + | . . . . . . . . ! / |
|
| 71 | + | * . . . + . . - O - |
|
| 72 | + | . . . + . . * . . / | |
|
| 73 | + | . + . . . .. + . |
|
| 74 | + | . . . . * . * . +.. . * |
|
| 75 | + | . . . . . . . . + . . + |
|
| 76 | + | ||
| 77 | + | ") |
|
| 78 | + | ||
| 79 | + | (provide 'init-ui) |
| 1 | + | ;;; posts.el --- Emacs extension for Andromeda/Posts -*- lexical-binding: t; -*- |
|
| 2 | + | ;; |
|
| 3 | + | ;; Copyright (C) 2026 Steve Simkins |
|
| 4 | + | ;; |
|
| 5 | + | ;; Author: Steve Simkins <contact@stevedylan.dev> |
|
| 6 | + | ;; Version: 0.0.1 |
|
| 7 | + | ;; Keywords: tools blogging extension |
|
| 8 | + | ;; Homepage: https://github.com/stevedylandev/andromeda |
|
| 9 | + | ;; Package-Requires: ((emacs "27.1")) |
|
| 10 | + | ;; |
|
| 11 | + | ;; This file is not part of GNU Emacs. |
|
| 12 | + | ;; |
|
| 13 | + | ;;; Commentary: |
|
| 14 | + | ;; Sends micro posts to an Andromeda/posts instance. |
|
| 15 | + | ;; Run M-x posts-compose, fill in the `name: value' fields at the top |
|
| 16 | + | ;; (see `posts-fields'), write the content below, then press C-c C-c. |
|
| 17 | + | ;; |
|
| 18 | + | ;;; Code: |
|
| 19 | + | ||
| 20 | + | (require 'url) |
|
| 21 | + | (require 'json) |
|
| 22 | + | (require 'mailcap) |
|
| 23 | + | (require 'subr-x) |
|
| 24 | + | (require 'auth-source-pass) |
|
| 25 | + | ||
| 26 | + | (defgroup posts nil |
|
| 27 | + | "Andromeda/posts." |
|
| 28 | + | :group 'tools) |
|
| 29 | + | ||
| 30 | + | (defcustom posts-pass-entry "andromeda/posts-api-key" |
|
| 31 | + | "Name of the `pass' entry holding the API key." |
|
| 32 | + | :type 'string |
|
| 33 | + | :group 'posts) |
|
| 34 | + | ||
| 35 | + | (defun posts--api-key () |
|
| 36 | + | "Return the API key from password-store." |
|
| 37 | + | ;; Encode so the header is unibyte; otherwise url-http refuses to mix |
|
| 38 | + | ;; it with a binary request body. |
|
| 39 | + | (encode-coding-string |
|
| 40 | + | (or (auth-source-pass-get 'secret posts-pass-entry) |
|
| 41 | + | (user-error "No pass entry `%s'" posts-pass-entry)) |
|
| 42 | + | 'utf-8)) |
|
| 43 | + | ||
| 44 | + | (defcustom posts-endpoint "https://posts.stevedylan.dev/api/posts" |
|
| 45 | + | "URL that receives the JSON post." |
|
| 46 | + | :type 'string |
|
| 47 | + | :group 'posts) |
|
| 48 | + | ||
| 49 | + | (defcustom posts-files-endpoint "https://posts.stevedylan.dev/api/files" |
|
| 50 | + | "URL that receives multipart file uploads." |
|
| 51 | + | :type 'string |
|
| 52 | + | :group 'posts) |
|
| 53 | + | ||
| 54 | + | (defconst posts--max-file-size (* 10 1024 1024) |
|
| 55 | + | "Largest file the server accepts, in bytes.") |
|
| 56 | + | ||
| 57 | + | ;;;; Fields |
|
| 58 | + | ||
| 59 | + | (defcustom posts-fields |
|
| 60 | + | '(("title") |
|
| 61 | + | ("status" :default "draft" :choices ("draft" "published"))) |
|
| 62 | + | "Header fields read from the top of the compose buffer as `name: value'. |
|
| 63 | + | Each entry is (NAME . PLIST). PLIST keys: `:required', `:default', |
|
| 64 | + | `:choices'. Add an entry to support a new field; it is sent as a JSON |
|
| 65 | + | key of the same name." |
|
| 66 | + | :type '(alist :key-type string :value-type plist) |
|
| 67 | + | :group 'posts) |
|
| 68 | + | ||
| 69 | + | (defun posts--parse-buffer () |
|
| 70 | + | "Return (FIELDS . CONTENT) parsed from the current buffer. |
|
| 71 | + | FIELDS is an alist of raw (NAME . VALUE) strings from the leading |
|
| 72 | + | `name: value' lines. CONTENT is everything after them." |
|
| 73 | + | (save-excursion |
|
| 74 | + | (goto-char (point-min)) |
|
| 75 | + | (let (fields) |
|
| 76 | + | (while (looking-at "^\\([[:alnum:]_-]+\\):[ \t]*\\(.*?\\)[ \t]*$") |
|
| 77 | + | (push (cons (downcase (match-string-no-properties 1)) |
|
| 78 | + | (match-string-no-properties 2)) |
|
| 79 | + | fields) |
|
| 80 | + | (forward-line 1)) |
|
| 81 | + | (cons (nreverse fields) |
|
| 82 | + | (string-trim |
|
| 83 | + | (buffer-substring-no-properties (point) (point-max))))))) |
|
| 84 | + | ||
| 85 | + | (defun posts--resolve-fields (parsed) |
|
| 86 | + | "Validate PARSED against `posts-fields'; return alist with defaults applied." |
|
| 87 | + | (dolist (p parsed) |
|
| 88 | + | (unless (assoc (car p) posts-fields) |
|
| 89 | + | (user-error "Unknown field `%s'" (car p)))) |
|
| 90 | + | (delq nil |
|
| 91 | + | (mapcar |
|
| 92 | + | (lambda (spec) |
|
| 93 | + | (let* ((name (car spec)) |
|
| 94 | + | (opts (cdr spec)) |
|
| 95 | + | (raw (cdr (assoc name parsed))) |
|
| 96 | + | (val (if (and raw (not (string-empty-p raw))) |
|
| 97 | + | raw |
|
| 98 | + | (plist-get opts :default))) |
|
| 99 | + | (choices (plist-get opts :choices))) |
|
| 100 | + | (when (and (plist-get opts :required) (null val)) |
|
| 101 | + | (user-error "Field `%s' is required" name)) |
|
| 102 | + | (when (and val choices (not (member val choices))) |
|
| 103 | + | (user-error "Bad %s `%s' (one of: %s)" |
|
| 104 | + | name val (string-join choices ", "))) |
|
| 105 | + | (and val (cons name val)))) |
|
| 106 | + | posts-fields))) |
|
| 107 | + | ||
| 108 | + | (defun posts--payload (fields content) |
|
| 109 | + | "Build the JSON plist from FIELDS alist and CONTENT." |
|
| 110 | + | (append (mapcan (lambda (f) (list (intern (concat ":" (car f))) (cdr f))) |
|
| 111 | + | fields) |
|
| 112 | + | (list :content content))) |
|
| 113 | + | ||
| 114 | + | ;;;; Sending |
|
| 115 | + | ||
| 116 | + | (defun posts (fields content) |
|
| 117 | + | "Send a post to `posts-endpoint' as JSON. |
|
| 118 | + | FIELDS is an alist of (NAME . VALUE) strings, CONTENT the body." |
|
| 119 | + | (interactive |
|
| 120 | + | (let ((title (read-string "Title (optional): "))) |
|
| 121 | + | (list (delq nil (list (and (not (string-empty-p title)) |
|
| 122 | + | (cons "title" title)) |
|
| 123 | + | (cons "status" "draft"))) |
|
| 124 | + | (read-string "Content: ")))) |
|
| 125 | + | (let ((url-request-method "POST") |
|
| 126 | + | (url-request-extra-headers |
|
| 127 | + | `(("Content-Type" . "application/json; charset=utf-8") |
|
| 128 | + | ("X-API-Key" . ,(posts--api-key)))) |
|
| 129 | + | (url-request-data |
|
| 130 | + | (encode-coding-string |
|
| 131 | + | (json-serialize (posts--payload fields content)) |
|
| 132 | + | 'utf-8))) |
|
| 133 | + | (url-retrieve |
|
| 134 | + | posts-endpoint |
|
| 135 | + | (lambda (status) |
|
| 136 | + | (unwind-protect |
|
| 137 | + | (if (plist-get status :error) |
|
| 138 | + | (message "Posts error: %S" (plist-get status :error)) |
|
| 139 | + | (goto-char (point-min)) |
|
| 140 | + | (re-search-forward "\r?\n\r?\n" nil t) |
|
| 141 | + | (condition-case nil |
|
| 142 | + | (let ((resp (json-parse-buffer :object-type 'alist))) |
|
| 143 | + | (message "Posted: https://stevedylan.dev/now/%S" (alist-get 'slug resp))) |
|
| 144 | + | (error (message "Posted (non-JSON response)")))) |
|
| 145 | + | (kill-buffer (current-buffer)))) |
|
| 146 | + | nil t))) |
|
| 147 | + | ||
| 148 | + | ;;;; Uploading files |
|
| 149 | + | ||
| 150 | + | (defun posts--multipart-body (boundary file name) |
|
| 151 | + | "Return a unibyte multipart/form-data body for FILE using BOUNDARY. |
|
| 152 | + | NAME, when non-nil, is sent as the `name' field." |
|
| 153 | + | (let ((type (or (mailcap-file-name-to-mime-type file) |
|
| 154 | + | "application/octet-stream")) |
|
| 155 | + | (filename (replace-regexp-in-string |
|
| 156 | + | "\"" "\\\\\"" |
|
| 157 | + | (encode-coding-string (file-name-nondirectory file) 'utf-8)))) |
|
| 158 | + | (with-temp-buffer |
|
| 159 | + | (set-buffer-multibyte nil) |
|
| 160 | + | (when name |
|
| 161 | + | (insert "--" boundary "\r\n" |
|
| 162 | + | "Content-Disposition: form-data; name=\"name\"\r\n\r\n" |
|
| 163 | + | (encode-coding-string name 'utf-8) "\r\n")) |
|
| 164 | + | (insert "--" boundary "\r\n" |
|
| 165 | + | "Content-Disposition: form-data; name=\"file\"; filename=\"" |
|
| 166 | + | filename "\"\r\n" |
|
| 167 | + | "Content-Type: " type "\r\n\r\n") |
|
| 168 | + | (insert-file-contents-literally file) |
|
| 169 | + | (goto-char (point-max)) |
|
| 170 | + | (insert "\r\n--" boundary "--\r\n") |
|
| 171 | + | (buffer-string)))) |
|
| 172 | + | ||
| 173 | + | (defun posts--response-json () |
|
| 174 | + | "Parse the JSON body of the current HTTP response buffer, or nil." |
|
| 175 | + | (goto-char (point-min)) |
|
| 176 | + | (when (re-search-forward "\r?\n\r?\n" nil t) |
|
| 177 | + | (ignore-errors (json-parse-buffer :object-type 'alist)))) |
|
| 178 | + | ||
| 179 | + | (defun posts-upload-file (file &optional name callback) |
|
| 180 | + | "Upload FILE to `posts-files-endpoint'. |
|
| 181 | + | NAME, if non-empty, sets the stored filename. On success the file's |
|
| 182 | + | URL is copied to the kill ring and CALLBACK, if non-nil, is called |
|
| 183 | + | with the parsed response alist." |
|
| 184 | + | (interactive |
|
| 185 | + | (list (read-file-name "Upload file: " nil nil t) |
|
| 186 | + | (read-string "Name (optional): "))) |
|
| 187 | + | (setq file (expand-file-name file)) |
|
| 188 | + | (unless (file-regular-p file) |
|
| 189 | + | (user-error "Not a regular file: %s" file)) |
|
| 190 | + | (when (> (file-attribute-size (file-attributes file)) posts--max-file-size) |
|
| 191 | + | (user-error "File exceeds 10 MB: %s" file)) |
|
| 192 | + | (when (and name (string-empty-p (string-trim name))) |
|
| 193 | + | (setq name nil)) |
|
| 194 | + | (let* ((boundary (format "posts-%s" (md5 (format "%s%s" (random) (float-time))))) |
|
| 195 | + | (url-request-method "POST") |
|
| 196 | + | (url-request-extra-headers |
|
| 197 | + | `(("Content-Type" . ,(encode-coding-string |
|
| 198 | + | (concat "multipart/form-data; boundary=" boundary) |
|
| 199 | + | 'utf-8)) |
|
| 200 | + | ("X-API-Key" . ,(posts--api-key)))) |
|
| 201 | + | (url-request-data (posts--multipart-body boundary file name))) |
|
| 202 | + | (message "Uploading %s..." (file-name-nondirectory file)) |
|
| 203 | + | (url-retrieve |
|
| 204 | + | posts-files-endpoint |
|
| 205 | + | (lambda (status) |
|
| 206 | + | (unwind-protect |
|
| 207 | + | (let ((err (plist-get status :error)) |
|
| 208 | + | (resp (posts--response-json))) |
|
| 209 | + | (cond |
|
| 210 | + | ((equal err '(error http 409)) |
|
| 211 | + | (message "Upload failed: name `%s' is already taken" name)) |
|
| 212 | + | (err |
|
| 213 | + | (message "Upload error: %S%s" err |
|
| 214 | + | (if resp (format " %S" resp) ""))) |
|
| 215 | + | ((null (alist-get 'url resp)) |
|
| 216 | + | (message "Uploaded (unexpected response)")) |
|
| 217 | + | (t |
|
| 218 | + | (kill-new (alist-get 'url resp)) |
|
| 219 | + | (message "Uploaded: %s (copied)" (alist-get 'url resp)) |
|
| 220 | + | (when callback (funcall callback resp))))) |
|
| 221 | + | (kill-buffer (current-buffer)))) |
|
| 222 | + | nil t))) |
|
| 223 | + | ||
| 224 | + | ;;;; Compose buffer |
|
| 225 | + | ||
| 226 | + | (defvar posts-compose-mode-map |
|
| 227 | + | (let ((m (make-sparse-keymap))) |
|
| 228 | + | (define-key m (kbd "C-c C-c") #'posts-compose-submit) |
|
| 229 | + | (define-key m (kbd "C-c C-k") #'posts-compose-cancel) |
|
| 230 | + | (define-key m (kbd "C-c C-a") #'posts-compose-attach) |
|
| 231 | + | m) |
|
| 232 | + | "Keymap for `posts-compose-mode'.") |
|
| 233 | + | ||
| 234 | + | (define-derived-mode posts-compose-mode text-mode "Posts" |
|
| 235 | + | "Compose a post. Leading `name: value' lines are fields, the rest is content. |
|
| 236 | + | \\{posts-compose-mode-map}" |
|
| 237 | + | (setq header-line-format |
|
| 238 | + | "Fields as `name: value' lines, then content. C-c C-c send, C-c C-a attach file, C-c C-k cancel")) |
|
| 239 | + | ||
| 240 | + | ;;;###autoload |
|
| 241 | + | (defun posts-compose () |
|
| 242 | + | "Open a buffer to compose a post." |
|
| 243 | + | (interactive) |
|
| 244 | + | (pop-to-buffer (get-buffer-create "*posts-compose*")) |
|
| 245 | + | (erase-buffer) |
|
| 246 | + | (posts-compose-mode) |
|
| 247 | + | (dolist (spec posts-fields) |
|
| 248 | + | (insert (car spec) ": " (or (plist-get (cdr spec) :default) "") "\n")) |
|
| 249 | + | (insert "\n") |
|
| 250 | + | (goto-char (point-min)) |
|
| 251 | + | (end-of-line)) |
|
| 252 | + | ||
| 253 | + | (defun posts-compose-submit () |
|
| 254 | + | "Parse the compose buffer and send it." |
|
| 255 | + | (interactive) |
|
| 256 | + | (let* ((parsed (posts--parse-buffer)) |
|
| 257 | + | (fields (posts--resolve-fields (car parsed))) |
|
| 258 | + | (content (cdr parsed))) |
|
| 259 | + | (when (string-empty-p content) |
|
| 260 | + | (user-error "Content is empty")) |
|
| 261 | + | (posts fields content) |
|
| 262 | + | (quit-window t))) |
|
| 263 | + | ||
| 264 | + | (defun posts-compose-attach (file &optional name) |
|
| 265 | + | "Upload FILE (stored as NAME) and insert a Markdown link at point. |
|
| 266 | + | Images are inserted as `'." |
|
| 267 | + | (interactive |
|
| 268 | + | (list (read-file-name "Attach file: " nil nil t) |
|
| 269 | + | (read-string "Name (optional): "))) |
|
| 270 | + | (let ((marker (point-marker))) |
|
| 271 | + | (posts-upload-file |
|
| 272 | + | file name |
|
| 273 | + | (lambda (resp) |
|
| 274 | + | (when (buffer-live-p (marker-buffer marker)) |
|
| 275 | + | (with-current-buffer (marker-buffer marker) |
|
| 276 | + | (save-excursion |
|
| 277 | + | (goto-char marker) |
|
| 278 | + | (insert (format "%s[%s](%s)" |
|
| 279 | + | (if (string-prefix-p "image/" (or (alist-get 'content_type resp) "")) |
|
| 280 | + | "!" "") |
|
| 281 | + | (or (alist-get 'original_name resp) |
|
| 282 | + | (alist-get 'filename resp) "") |
|
| 283 | + | (alist-get 'url resp)))))))))) |
|
| 284 | + | ||
| 285 | + | (defun posts-compose-cancel () |
|
| 286 | + | "Discard the compose buffer." |
|
| 287 | + | (interactive) |
|
| 288 | + | (quit-window t)) |
|
| 289 | + | ||
| 290 | + | (provide 'posts) |
|
| 291 | + | ;;; posts.el ends here |
| 1 | + | ;;; uguu.el --- Upload files to uguu.se -*- lexical-binding: t; -*- |
|
| 2 | + | ;; |
|
| 3 | + | ;;; Commentary: |
|
| 4 | + | ;; `uguu-upload-file' and `uguu-upload-clipboard' upload to uguu.se and put |
|
| 5 | + | ;; the resulting link in the kill ring. In an ERC buffer the link is also |
|
| 6 | + | ;; inserted at the prompt. A prefix argument flips that behavior. |
|
| 7 | + | ;; |
|
| 8 | + | ;;; Code: |
|
| 9 | + | ||
| 10 | + | (require 'subr-x) |
|
| 11 | + | ||
| 12 | + | (defgroup uguu nil |
|
| 13 | + | "Upload files to uguu.se." |
|
| 14 | + | :group 'tools) |
|
| 15 | + | ||
| 16 | + | (defcustom uguu-endpoint "https://uguu.se/upload?output=text" |
|
| 17 | + | "Upload URL. `output=text' makes the response just the file link." |
|
| 18 | + | :type 'string) |
|
| 19 | + | ||
| 20 | + | (defcustom uguu-insert-modes '(erc-mode) |
|
| 21 | + | "Major modes where the link is inserted at point by default." |
|
| 22 | + | :type '(repeat symbol)) |
|
| 23 | + | ||
| 24 | + | (defun uguu--finish (url insert) |
|
| 25 | + | "Copy URL to the kill ring, and insert it when INSERT is non-nil." |
|
| 26 | + | (kill-new url) |
|
| 27 | + | (when insert |
|
| 28 | + | (when (derived-mode-p 'erc-mode) |
|
| 29 | + | (goto-char (point-max))) |
|
| 30 | + | (insert url)) |
|
| 31 | + | (message "Uploaded: %s%s" url (if insert "" " (copied)"))) |
|
| 32 | + | ||
| 33 | + | (defun uguu--upload (file insert &optional cleanup) |
|
| 34 | + | "Upload FILE asynchronously, then call `uguu--finish' with INSERT. |
|
| 35 | + | When CLEANUP is non-nil, delete FILE afterwards." |
|
| 36 | + | (let ((buf (current-buffer)) |
|
| 37 | + | (out (generate-new-buffer " *uguu*"))) |
|
| 38 | + | (message "Uploading %s..." (file-name-nondirectory file)) |
|
| 39 | + | (make-process |
|
| 40 | + | :name "uguu" :buffer out :noquery t |
|
| 41 | + | :command (list "curl" "-sS" "--fail-with-body" |
|
| 42 | + | "-F" (concat "files[]=@" (expand-file-name file)) |
|
| 43 | + | uguu-endpoint) |
|
| 44 | + | :sentinel |
|
| 45 | + | (lambda (proc _event) |
|
| 46 | + | (when (memq (process-status proc) '(exit signal)) |
|
| 47 | + | (let ((resp (with-current-buffer out (string-trim (buffer-string))))) |
|
| 48 | + | (kill-buffer out) |
|
| 49 | + | (when cleanup (ignore-errors (delete-file file))) |
|
| 50 | + | (if (and (zerop (process-exit-status proc)) |
|
| 51 | + | (string-prefix-p "http" resp)) |
|
| 52 | + | (if (buffer-live-p buf) |
|
| 53 | + | (with-current-buffer buf (uguu--finish resp insert)) |
|
| 54 | + | (uguu--finish resp nil)) |
|
| 55 | + | (message "Uguu upload failed: %s" resp)))))))) |
|
| 56 | + | ||
| 57 | + | (defun uguu--insert-p (arg) |
|
| 58 | + | "Whether to insert the link, given prefix ARG." |
|
| 59 | + | (let ((default (apply #'derived-mode-p uguu-insert-modes))) |
|
| 60 | + | (if arg (not default) default))) |
|
| 61 | + | ||
| 62 | + | ;;;###autoload |
|
| 63 | + | (defun uguu-upload-file (file &optional arg) |
|
| 64 | + | "Upload FILE to uguu.se. Copy the link; insert it too in ERC. |
|
| 65 | + | With prefix ARG, flip whether the link is inserted." |
|
| 66 | + | (interactive (list (read-file-name "Upload file: " nil nil t) current-prefix-arg)) |
|
| 67 | + | (uguu--upload file (uguu--insert-p arg))) |
|
| 68 | + | ||
| 69 | + | (defun uguu--clipboard-file () |
|
| 70 | + | "Return the path of a file copied in Finder/CleanShot, or nil. |
|
| 71 | + | Checked before the image data, which for a copied file is just its icon." |
|
| 72 | + | (with-temp-buffer |
|
| 73 | + | (when (zerop (call-process |
|
| 74 | + | "osascript" nil '(t nil) nil |
|
| 75 | + | "-e" "POSIX path of (the clipboard as «class furl»)")) |
|
| 76 | + | (let ((path (string-trim (buffer-string)))) |
|
| 77 | + | (and (file-regular-p path) path))))) |
|
| 78 | + | ||
| 79 | + | (defun uguu--clipboard-image () |
|
| 80 | + | "Write the clipboard image to a temp PNG and return its path, or nil." |
|
| 81 | + | (let ((tmp (make-temp-file "uguu-" nil ".png"))) |
|
| 82 | + | (if (zerop (call-process |
|
| 83 | + | "osascript" nil nil nil |
|
| 84 | + | "-e" (format "set f to open for access POSIX file %S with write permission" |
|
| 85 | + | tmp) |
|
| 86 | + | "-e" "set eof f to 0" |
|
| 87 | + | "-e" "write (the clipboard as «class PNGf») to f" |
|
| 88 | + | "-e" "close access f")) |
|
| 89 | + | tmp |
|
| 90 | + | (delete-file tmp) |
|
| 91 | + | nil))) |
|
| 92 | + | ||
| 93 | + | ;;;###autoload |
|
| 94 | + | (defun uguu-upload-clipboard (&optional arg) |
|
| 95 | + | "Upload the clipboard (an image, else text) to uguu.se. |
|
| 96 | + | With prefix ARG, flip whether the link is inserted." |
|
| 97 | + | (interactive "P") |
|
| 98 | + | (let* ((insert (uguu--insert-p arg)) |
|
| 99 | + | (file (uguu--clipboard-file)) |
|
| 100 | + | (img (and (not file) (uguu--clipboard-image)))) |
|
| 101 | + | (cond |
|
| 102 | + | (file (uguu--upload file insert)) |
|
| 103 | + | (img (uguu--upload img insert t)) |
|
| 104 | + | (t |
|
| 105 | + | (let ((text (or (ignore-errors (gui-get-selection 'CLIPBOARD 'STRING)) |
|
| 106 | + | (car kill-ring))) |
|
| 107 | + | (tmp (make-temp-file "uguu-" nil ".txt"))) |
|
| 108 | + | (unless (and text (not (string-empty-p text))) |
|
| 109 | + | (user-error "Clipboard is empty")) |
|
| 110 | + | (write-region text nil tmp nil 'silent) |
|
| 111 | + | (uguu--upload tmp insert t)))))) |
|
| 112 | + | ||
| 113 | + | (provide 'uguu) |
|
| 114 | + | ;;; uguu.el ends here |
| 1 | + | ;;; darkmatter-theme.el --- Darkmatter on base16 -*- lexical-binding: t; -*- |
|
| 2 | + | ||
| 3 | + | ;; https://darkmattertheme.com |
|
| 4 | + | ;; Palette + a few overrides on top of `base16-theme' (package base16-theme). |
|
| 5 | + | ;; The 16 slots are mapped to match the old doom-darkmatter roles. |
|
| 6 | + | ||
| 7 | + | (require 'cl-lib) |
|
| 8 | + | (require 'base16-theme) |
|
| 9 | + | ||
| 10 | + | ;;; Palette -- edit these and everything below follows |
|
| 11 | + | ||
| 12 | + | (defun darkmatter--blend (a b alpha) |
|
| 13 | + | "Mix hex colors A and B; ALPHA is the share of B (0..1)." |
|
| 14 | + | (cl-flet ((rgb (c) (mapcar (lambda (i) (string-to-number (substring c i (+ i 2)) 16)) |
|
| 15 | + | '(1 3 5)))) |
|
| 16 | + | (apply #'format "#%02x%02x%02x" |
|
| 17 | + | (cl-mapcar (lambda (x y) (round (+ (* x (- 1 alpha)) (* y alpha)))) |
|
| 18 | + | (rgb a) (rgb b))))) |
|
| 19 | + | ||
| 20 | + | (defvar darkmatter-palette |
|
| 21 | + | '((base00 . "#121113") ; background |
|
| 22 | + | (base01 . "#121212") ; lighter background |
|
| 23 | + | (base02 . "#222222") ; selection |
|
| 24 | + | (base03 . "#333333") ; comments |
|
| 25 | + | (base04 . "#999999") ; dark foreground, doc strings |
|
| 26 | + | (base05 . "#c1c1c1") ; foreground |
|
| 27 | + | (base06 . "#999999") ; light foreground |
|
| 28 | + | (base07 . "#c1c1c1") ; lightest |
|
| 29 | + | (base08 . "#5f8787") ; variables, errors |
|
| 30 | + | (base09 . "#aaaaaa") ; constants, numbers |
|
| 31 | + | (base0A . "#e78a53") ; types, accent, warnings |
|
| 32 | + | (base0B . "#fbcb97") ; strings, added |
|
| 33 | + | (base0C . "#aaaaaa") ; builtins, support |
|
| 34 | + | (base0D . "#888888") ; functions |
|
| 35 | + | (base0E . "#999999") ; keywords |
|
| 36 | + | (base0F . "#444444") ; deprecated |
|
| 37 | + | (red . "#aa6c6c")) ; extra: removed lines, ERC errors (not a base16 slot) |
|
| 38 | + | "Darkmatter colors. Base16 slots plus `red'. Everything else is derived.") |
|
| 39 | + | ||
| 40 | + | (deftheme darkmatter "Darkmatter, based on Black Metal Bathory.") |
|
| 41 | + | ||
| 42 | + | (let* ((base00 (alist-get 'base00 darkmatter-palette)) |
|
| 43 | + | (base01 (alist-get 'base01 darkmatter-palette)) |
|
| 44 | + | (base02 (alist-get 'base02 darkmatter-palette)) |
|
| 45 | + | (base03 (alist-get 'base03 darkmatter-palette)) |
|
| 46 | + | (base04 (alist-get 'base04 darkmatter-palette)) |
|
| 47 | + | (base05 (alist-get 'base05 darkmatter-palette)) |
|
| 48 | + | (base06 (alist-get 'base06 darkmatter-palette)) |
|
| 49 | + | (base07 (alist-get 'base07 darkmatter-palette)) |
|
| 50 | + | (base08 (alist-get 'base08 darkmatter-palette)) |
|
| 51 | + | (base09 (alist-get 'base09 darkmatter-palette)) |
|
| 52 | + | (base0A (alist-get 'base0A darkmatter-palette)) |
|
| 53 | + | (base0B (alist-get 'base0B darkmatter-palette)) |
|
| 54 | + | (base0C (alist-get 'base0C darkmatter-palette)) |
|
| 55 | + | (base0D (alist-get 'base0D darkmatter-palette)) |
|
| 56 | + | (base0E (alist-get 'base0E darkmatter-palette)) |
|
| 57 | + | (base0F (alist-get 'base0F darkmatter-palette)) |
|
| 58 | + | (red (alist-get 'red darkmatter-palette)) |
|
| 59 | + | ;; roles used by the face overrides below |
|
| 60 | + | (bg base00) |
|
| 61 | + | (bg-alt base01) |
|
| 62 | + | (sel base02) |
|
| 63 | + | (comment base03) |
|
| 64 | + | (dim base04) |
|
| 65 | + | (fg base05) |
|
| 66 | + | (fg-hi base06) |
|
| 67 | + | (fg-max base07) |
|
| 68 | + | (teal base08) |
|
| 69 | + | (const base09) |
|
| 70 | + | (orange base0A) |
|
| 71 | + | (string base0B) |
|
| 72 | + | (func base0D) |
|
| 73 | + | (kw base0E) |
|
| 74 | + | (dep base0F) |
|
| 75 | + | ;; derived |
|
| 76 | + | (base0 (darkmatter--blend bg "#000000" 0.4)) |
|
| 77 | + | (faint (darkmatter--blend bg-alt kw 0.2)) |
|
| 78 | + | (muted (darkmatter--blend bg dim 0.45)) |
|
| 79 | + | (bg-tint (darkmatter--blend bg const 0.15)) |
|
| 80 | + | (added-fg (darkmatter--blend string "#000000" 0.2)) |
|
| 81 | + | (added-bg (darkmatter--blend bg string 0.1)) |
|
| 82 | + | (added-bg-hi (darkmatter--blend bg string 0.2)) |
|
| 83 | + | (removed-bg (darkmatter--blend sel red 0.1)) |
|
| 84 | + | (removed-bg-hi (darkmatter--blend sel red 0.2)) |
|
| 85 | + | (changed-bg (darkmatter--blend bg orange 0.1)) |
|
| 86 | + | (changed-bg-hi (darkmatter--blend bg orange 0.2)) |
|
| 87 | + | (red-dark (darkmatter--blend red "#000000" 0.2)) |
|
| 88 | + | (orange-dark (darkmatter--blend orange "#000000" 0.2))) |
|
| 89 | + | ||
| 90 | + | ;; Fringe/line numbers share the main background, like doom-darkmatter |
|
| 91 | + | (setq base16-theme-distinct-fringe-background nil) |
|
| 92 | + | ;; In a tty base16 defaults to the terminal's ANSI palette by name |
|
| 93 | + | ;; (base02 -> brightyellow, i.e. orange in ghostty), so map our hex |
|
| 94 | + | ;; colors to the nearest 256-color instead. |
|
| 95 | + | (setq base16-theme-256-color-source 'colors) |
|
| 96 | + | ||
| 97 | + | (base16-theme-define |
|
| 98 | + | 'darkmatter |
|
| 99 | + | (list :base00 base00 :base01 base01 :base02 base02 :base03 base03 |
|
| 100 | + | :base04 base04 :base05 base05 :base06 base06 :base07 base07 |
|
| 101 | + | :base08 base08 :base09 base09 :base0A base0A :base0B base0B |
|
| 102 | + | :base0C base0C :base0D base0D :base0E base0E :base0F base0F)) |
|
| 103 | + | ||
| 104 | + | ;; Where doom-darkmatter differs from stock base16 role assignments, |
|
| 105 | + | ;; plus packages base16 has no faces for |
|
| 106 | + | (custom-theme-set-faces |
|
| 107 | + | 'darkmatter |
|
| 108 | + | `(cursor ((t (:background ,orange)))) |
|
| 109 | + | `(font-lock-variable-name-face ((t (:foreground ,teal)))) |
|
| 110 | + | `(font-lock-builtin-face ((t (:foreground ,kw)))) |
|
| 111 | + | `(font-lock-constant-face ((t (:foreground ,const)))) |
|
| 112 | + | `(font-lock-number-face ((t (:foreground ,const)))) |
|
| 113 | + | `(font-lock-warning-face ((t (:foreground ,orange)))) |
|
| 114 | + | `(mode-line ((t (:foreground ,fg :background ,bg :overline ,sel :box nil)))) |
|
| 115 | + | `(mode-line-inactive ((t (:foreground ,dim :background ,bg :overline ,sel :box nil)))) |
|
| 116 | + | `(mode-line-buffer-id ((t (:foreground ,fg :weight bold)))) |
|
| 117 | + | `(line-number ((t (:foreground ,faint :background ,bg)))) |
|
| 118 | + | `(line-number-current-line ((t (:foreground ,orange :background ,bg)))) |
|
| 119 | + | `(vertical-border ((t (:foreground ,sel)))) |
|
| 120 | + | `(erc-prompt-face ((t (:foreground ,orange :weight bold)))) |
|
| 121 | + | ;; base16 uses base01 for highlight, which is ~identical to bg here |
|
| 122 | + | `(highlight ((t (:background ,sel)))) |
|
| 123 | + | `(icomplete-selected-match ((t (:foreground ,orange :background ,sel :weight bold :extend t)))) |
|
| 124 | + | `(completions-highlight ((t (:inherit icomplete-selected-match)))) |
|
| 125 | + | ;; base16 makes this inherit gnus-group-news-6, whose default spec |
|
| 126 | + | ;; inherits this face back -> cycle error in enable-theme |
|
| 127 | + | `(gnus-group-news-6-empty ((t (:foreground ,base04 :inherit outline-2)))) |
|
| 128 | + | ;; base16 only themes the normal 8; bright ones (used by ghostel etc.) |
|
| 129 | + | ;; fall back to Emacs defaults. Match the darkmatter ghostty palette. |
|
| 130 | + | `(ansi-color-bright-black ((t (:foreground ,base03 :background ,base03)))) |
|
| 131 | + | `(ansi-color-bright-red ((t (:foreground ,base08 :background ,base08)))) |
|
| 132 | + | `(ansi-color-bright-green ((t (:foreground ,base0B :background ,base0B)))) |
|
| 133 | + | `(ansi-color-bright-yellow ((t (:foreground ,base0A :background ,base0A)))) |
|
| 134 | + | `(ansi-color-bright-blue ((t (:foreground ,base0D :background ,base0D)))) |
|
| 135 | + | `(ansi-color-bright-magenta ((t (:foreground ,base0E :background ,base0E)))) |
|
| 136 | + | `(ansi-color-bright-cyan ((t (:foreground ,base0C :background ,base0C)))) |
|
| 137 | + | `(ansi-color-bright-white ((t (:foreground ,base07 :background ,base07)))) |
|
| 138 | + | ||
| 139 | + | ;; Packages base16 has no faces for |
|
| 140 | + | `(elfeed-search-date-face ((t (:foreground ,const)))) |
|
| 141 | + | `(elfeed-search-feed-face ((t (:foreground ,func)))) |
|
| 142 | + | `(elfeed-search-tag-face ((t (:foreground ,comment)))) |
|
| 143 | + | `(elfeed-search-title-face ((t (:foreground ,comment)))) |
|
| 144 | + | `(elfeed-search-unread-title-face ((t (:foreground ,fg :weight bold)))) |
|
| 145 | + | `(elfeed-search-unread-count-face ((t (:foreground ,orange)))) |
|
| 146 | + | `(elfeed-search-filter-face ((t (:foreground ,const)))) |
|
| 147 | + | `(elfeed-log-debug-level-face ((t (:foreground ,comment)))) |
|
| 148 | + | `(elfeed-log-error-level-face ((t (:inherit error)))) |
|
| 149 | + | `(elfeed-log-info-level-face ((t (:inherit success)))) |
|
| 150 | + | `(elfeed-log-warn-level-face ((t (:inherit warning)))) |
|
| 151 | + | `(which-key-key-face ((t (:foreground ,orange)))) |
|
| 152 | + | `(which-key-group-description-face ((t (:foreground ,kw)))) |
|
| 153 | + | `(which-key-command-description-face ((t (:foreground ,fg))))) |
|
| 154 | + | ||
| 155 | + | ;; Magit/diff/org/vc faces, flattened from doom-darkmatter |
|
| 156 | + | (custom-theme-set-faces |
|
| 157 | + | 'darkmatter |
|
| 158 | + | `(diff-hl-change ((t (:foreground "#e78a53" :background "#e78a53")))) |
|
| 159 | + | `(diff-hl-delete ((t (:foreground "#aa6c6c" :background "#aa6c6c")))) |
|
| 160 | + | `(diff-hl-insert ((t (:foreground "#fbcb97" :background "#fbcb97")))) |
|
| 161 | + | `(diff-added ((t (:inherit hl-line :foreground ,string)))) |
|
| 162 | + | `(diff-changed ((t (:foreground ,const)))) |
|
| 163 | + | `(diff-context ((t (:foreground ,const)))) |
|
| 164 | + | `(diff-removed ((t (:foreground ,red :background ,sel)))) |
|
| 165 | + | `(diff-header ((t (:foreground ,const)))) |
|
| 166 | + | `(diff-file-header ((t (:foreground ,func)))) |
|
| 167 | + | `(diff-hunk-header ((t (:foreground ,const)))) |
|
| 168 | + | `(diff-indicator-added ((t (:foreground ,string)))) |
|
| 169 | + | `(diff-indicator-changed ((t (:foreground ,orange)))) |
|
| 170 | + | `(diff-indicator-removed ((t (:foreground ,red)))) |
|
| 171 | + | `(diff-refine-added ((t (:inherit diff-added :inverse-video t)))) |
|
| 172 | + | `(diff-refine-changed ((t (:inherit diff-changed :inverse-video t)))) |
|
| 173 | + | `(diff-refine-removed ((t (:inherit diff-removed :inverse-video t)))) |
|
| 174 | + | `(ediff-fine-diff-A ((t (:inherit diff-refine-removed)))) |
|
| 175 | + | `(ediff-fine-diff-B ((t (:inherit diff-refine-added)))) |
|
| 176 | + | `(ediff-fine-diff-C ((t (:inherit ediff-fine-diff-A)))) |
|
| 177 | + | `(ediff-current-diff-A ((t (:foreground ,red :background ,removed-bg-hi :extend t)))) |
|
| 178 | + | `(ediff-current-diff-B ((t (:foreground ,string :background ,added-bg-hi :extend t)))) |
|
| 179 | + | `(ediff-current-diff-C ((t (:inherit ediff-current-diff-A)))) |
|
| 180 | + | `(ediff-even-diff-A ((t (:inherit hl-line)))) |
|
| 181 | + | `(ediff-even-diff-B ((t (:inherit ediff-even-diff-A)))) |
|
| 182 | + | `(ediff-even-diff-C ((t (:inherit ediff-even-diff-A)))) |
|
| 183 | + | `(ediff-odd-diff-A ((t (:inherit ediff-even-diff-A)))) |
|
| 184 | + | `(ediff-odd-diff-B ((t (:inherit ediff-odd-diff-A)))) |
|
| 185 | + | `(ediff-odd-diff-C ((t (:inherit ediff-odd-diff-A)))) |
|
| 186 | + | `(git-commit-summary ((t (:foreground ,string)))) |
|
| 187 | + | `(git-commit-overlong-summary ((t (:inherit error :background ,base0 :slant italic :weight bold)))) |
|
| 188 | + | `(git-commit-nonempty-second-line ((t (:inherit git-commit-overlong-summary)))) |
|
| 189 | + | `(git-commit-keyword ((t (:foreground ,const :slant italic)))) |
|
| 190 | + | `(git-commit-pseudo-header ((t (:foreground ,dim :slant italic)))) |
|
| 191 | + | `(git-commit-known-pseudo-header ((t (:foreground ,dim :weight bold :slant italic)))) |
|
| 192 | + | `(git-commit-comment-branch-local ((t (:foreground ,kw)))) |
|
| 193 | + | `(git-commit-comment-branch-remote ((t (:foreground ,string)))) |
|
| 194 | + | `(git-commit-comment-detached ((t (:foreground ,orange)))) |
|
| 195 | + | `(git-commit-comment-heading ((t (:foreground ,kw)))) |
|
| 196 | + | `(git-commit-comment-file ((t (:foreground ,const)))) |
|
| 197 | + | `(git-commit-comment-action ((t nil))) |
|
| 198 | + | `(hl-line ((t (:background ,bg :extend t)))) |
|
| 199 | + | `(magit-bisect-bad ((t (:foreground ,red)))) |
|
| 200 | + | `(magit-bisect-good ((t (:foreground ,string)))) |
|
| 201 | + | `(magit-bisect-skip ((t (:foreground ,orange)))) |
|
| 202 | + | `(magit-blame-hash ((t (:foreground ,const)))) |
|
| 203 | + | `(magit-blame-date ((t (:foreground ,red)))) |
|
| 204 | + | `(magit-blame-heading ((t (:foreground ,orange :background ,sel :extend t)))) |
|
| 205 | + | `(magit-branch-current ((t (:foreground ,func)))) |
|
| 206 | + | `(magit-branch-local ((t (:foreground ,const)))) |
|
| 207 | + | `(magit-branch-remote ((t (:foreground ,string)))) |
|
| 208 | + | `(magit-cherry-equivalent ((t (:foreground ,const)))) |
|
| 209 | + | `(magit-cherry-unmatched ((t (:foreground ,const)))) |
|
| 210 | + | `(magit-diff-added ((t (:foreground ,added-fg :background ,added-bg :extend t)))) |
|
| 211 | + | `(magit-diff-added-highlight ((t (:foreground ,string :background ,added-bg-hi :weight bold :extend t)))) |
|
| 212 | + | `(magit-diff-base ((t (:foreground ,orange-dark :background ,changed-bg :extend t)))) |
|
| 213 | + | `(magit-diff-base-highlight ((t (:foreground ,orange :background ,changed-bg-hi :weight bold :extend t)))) |
|
| 214 | + | `(magit-diff-context ((t (:foreground ,dim :background ,bg :extend t)))) |
|
| 215 | + | `(magit-diff-context-highlight ((t (:foreground ,fg :background ,bg :extend t)))) |
|
| 216 | + | `(magit-diff-file-heading ((t (:foreground ,fg :weight bold :extend t)))) |
|
| 217 | + | `(magit-diff-file-heading-selection ((t (:foreground ,kw :background ,dim :weight bold :extend t)))) |
|
| 218 | + | `(magit-diff-hunk-heading ((t (:foreground ,bg :background ,muted :extend t)))) |
|
| 219 | + | `(magit-diff-hunk-heading-highlight ((t (:foreground ,bg :background ,const :weight bold :extend t)))) |
|
| 220 | + | `(magit-diff-lines-heading ((t (:foreground ,orange :background ,red :extend t :extend t)))) |
|
| 221 | + | `(magit-diff-removed ((t (:foreground ,red-dark :background ,removed-bg :extend t)))) |
|
| 222 | + | `(magit-diff-removed-highlight ((t (:foreground ,red :background ,removed-bg-hi :weight bold :extend t)))) |
|
| 223 | + | `(magit-diffstat-added ((t (:foreground ,string)))) |
|
| 224 | + | `(magit-diffstat-removed ((t (:foreground ,red)))) |
|
| 225 | + | `(magit-dimmed ((t (:foreground ,kw)))) |
|
| 226 | + | `(magit-hash ((t (:foreground ,comment)))) |
|
| 227 | + | `(magit-header-line ((t (:background ,dim :foreground ,fg :weight bold :box (:line-width 3 :color ,dim))))) |
|
| 228 | + | `(magit-filename ((t (:foreground ,const)))) |
|
| 229 | + | `(magit-log-author ((t (:foreground ,orange)))) |
|
| 230 | + | `(magit-log-date ((t (:foreground ,func)))) |
|
| 231 | + | `(magit-log-graph ((t (:foreground ,comment)))) |
|
| 232 | + | `(magit-process-ng ((t (:inherit error)))) |
|
| 233 | + | `(magit-process-ok ((t (:inherit success)))) |
|
| 234 | + | `(magit-reflog-amend ((t (:foreground ,kw)))) |
|
| 235 | + | `(magit-reflog-checkout ((t (:foreground ,func)))) |
|
| 236 | + | `(magit-reflog-cherry-pick ((t (:foreground ,string)))) |
|
| 237 | + | `(magit-reflog-commit ((t (:foreground ,string)))) |
|
| 238 | + | `(magit-reflog-merge ((t (:foreground ,string)))) |
|
| 239 | + | `(magit-reflog-other ((t (:foreground ,const)))) |
|
| 240 | + | `(magit-reflog-rebase ((t (:foreground ,kw)))) |
|
| 241 | + | `(magit-reflog-remote ((t (:foreground ,const)))) |
|
| 242 | + | `(magit-reflog-reset ((t (:inherit error)))) |
|
| 243 | + | `(magit-refname ((t (:foreground ,comment)))) |
|
| 244 | + | `(magit-section-heading ((t (:foreground ,func :weight bold :extend t)))) |
|
| 245 | + | `(magit-section-heading-selection ((t (:foreground ,orange :weight bold :extend t)))) |
|
| 246 | + | `(magit-section-highlight ((t (:inherit hl-line)))) |
|
| 247 | + | `(magit-section-secondary-heading ((t (:foreground ,const :weight bold :extend t)))) |
|
| 248 | + | `(magit-sequence-drop ((t (:foreground ,red)))) |
|
| 249 | + | `(magit-sequence-head ((t (:foreground ,func)))) |
|
| 250 | + | `(magit-sequence-part ((t (:foreground ,orange)))) |
|
| 251 | + | `(magit-sequence-stop ((t (:foreground ,string)))) |
|
| 252 | + | `(magit-signature-bad ((t (:inherit error)))) |
|
| 253 | + | `(magit-signature-error ((t (:inherit error)))) |
|
| 254 | + | `(magit-signature-expired ((t (:foreground ,orange)))) |
|
| 255 | + | `(magit-signature-good ((t (:inherit success)))) |
|
| 256 | + | `(magit-signature-revoked ((t (:foreground ,kw)))) |
|
| 257 | + | `(magit-signature-untrusted ((t (:foreground ,orange)))) |
|
| 258 | + | `(magit-tag ((t (:foreground ,orange)))) |
|
| 259 | + | `(org-block ((t (:background ,sel :extend t)))) |
|
| 260 | + | `(org-block-background ((t (:background ,sel :extend t)))) |
|
| 261 | + | `(org-block-begin-line ((t (:inherit org-block :foreground ,comment)))) |
|
| 262 | + | `(org-block-end-line ((t (:inherit org-block-begin-line)))) |
|
| 263 | + | `(org-checkbox ((t (:inherit org-todo)))) |
|
| 264 | + | `(org-checkbox-statistics-done ((t (:inherit org-done)))) |
|
| 265 | + | `(org-checkbox-statistics-todo ((t (:inherit org-todo)))) |
|
| 266 | + | `(org-code ((t (:inherit org-block :foreground ,orange)))) |
|
| 267 | + | `(org-date ((t (:foreground ,orange)))) |
|
| 268 | + | `(org-document-info ((t (:foreground ,kw)))) |
|
| 269 | + | `(org-document-title ((t (:foreground ,kw :weight bold)))) |
|
| 270 | + | `(org-done ((t (:inherit org-headline-done :strike-through unspecified :weight bold)))) |
|
| 271 | + | `(org-link ((t (:inherit link :foreground ,orange)))) |
|
| 272 | + | `(org-meta-line ((t (:foreground ,dim)))) |
|
| 273 | + | `(org-table ((t (:foreground ,const)))) |
|
| 274 | + | `(org-tag ((t (:foreground ,dim :weight normal)))) |
|
| 275 | + | `(org-todo ((t (:foreground ,string :bold inherit)))) |
|
| 276 | + | `(org-verbatim ((t (:foreground ,string)))) |
|
| 277 | + | `(outline-1 ((t (:foreground ,func :weight bold :extend t)))) |
|
| 278 | + | `(outline-2 ((t (:foreground ,kw :weight bold :extend t)))) |
|
| 279 | + | `(outline-3 ((t (:foreground ,const :weight bold :extend t)))) |
|
| 280 | + | `(outline-4 ((t (:foreground ,const :weight bold :extend t)))) |
|
| 281 | + | `(outline-5 ((t (:foreground ,const :weight bold :extend t)))) |
|
| 282 | + | `(outline-6 ((t (:foreground ,fg :weight bold :extend t)))) |
|
| 283 | + | `(outline-7 ((t (:foreground ,fg-hi :weight bold :extend t)))) |
|
| 284 | + | `(outline-8 ((t (:foreground ,fg-max :weight bold :extend t)))) |
|
| 285 | + | `(smerge-lower ((t (:background ,added-bg-hi)))) |
|
| 286 | + | `(smerge-upper ((t (:background ,removed-bg-hi)))) |
|
| 287 | + | `(smerge-base ((t (:background ,bg-tint)))) |
|
| 288 | + | `(smerge-markers ((t (:background ,comment :foreground ,bg :distant-foreground ,fg :weight bold)))) |
|
| 289 | + | `(smerge-refined-added ((t (:inherit diff-added :inverse-video t)))) |
|
| 290 | + | `(smerge-refined-removed ((t (:inherit diff-removed :inverse-video t)))) |
|
| 291 | + | `(smerge-mine ((t (:background ,removed-bg-hi)))) |
|
| 292 | + | `(smerge-other ((t (:background ,added-bg-hi)))))) |
|
| 293 | + | ||
| 294 | + | (provide-theme 'darkmatter) |
|
| 295 | + | ||
| 296 | + | ;;; darkmatter-theme.el ends here |