feat(emacs): init b2bff6b9
Steve Simkins · 2026-10-18 17:13 13 file(s) · +1481 −0
emacs.d/early-init.el (added) +42 −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)
emacs.d/init.el (added) +25 −0
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)
emacs.d/lisp/init-defaults.el (added) +106 −0
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)
emacs.d/lisp/init-erc.el (added) +40 −0
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)
emacs.d/lisp/init-evil.el (added) +111 −0
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)
emacs.d/lisp/init-jabber.el (added) +31 −0
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)
emacs.d/lisp/init-lang.el (added) +121 −0
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)
emacs.d/lisp/init-term.el (added) +71 −0
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)
emacs.d/lisp/init-tools.el (added) +154 −0
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)
emacs.d/lisp/init-ui.el (added) +79 −0
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)
emacs.d/lisp/posts.el (added) +291 −0
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 `![](url)'."
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
emacs.d/lisp/uguu.el (added) +114 −0
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
emacs.d/themes/darkmatter-theme.el (added) +296 −0
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