diff --git a/config/emacs/early-init.el b/config/emacs/early-init.el
index f754e73..993208e 100644
--- a/config/emacs/early-init.el
+++ b/config/emacs/early-init.el
@@ -1,8 +1,6 @@
;;; early-init.el --- Pre-init: GC ceiling and frame chrome -*- lexical-binding: t; -*-
-
;;; Commentary:
-;; Runs before package.el initialises and before the first frame is drawn, so
-;; it is the only place two things can take effect:
+;; Runs before package.el initialises and first frame is drawn, the only place these take effect:
;; 1. Raising the GC ceiling, so the whole startup (package load + init.el)
;; runs without garbage-collection pauses.
;; 2. Removing the menu/tool/scroll bars via `default-frame-alist', so they
@@ -10,15 +8,6 @@
;; them and then removes them, a visible flicker on launch.
;;
;; Nothing here may depend on a package: none are loaded yet.
-;;
-;; IMPORTANT: the GC bump below is only half the pair. Restore a working value
-;; once startup is done by adding this to init.el:
-;;
-;; (add-hook 'emacs-startup-hook
-;; (lambda () (setq gc-cons-threshold (* 16 1024 1024))))
-;;
-;; Without it, GC never runs again and Emacs slowly eats memory.
-
;;; Code:
(startup-redirect-eln-cache (expand-file-name "var/eln-cache/" user-emacs-directory))
diff --git a/config/emacs/init.el b/config/emacs/init.el
index e3c4b7d..ae0475c 100644
--- a/config/emacs/init.el
+++ b/config/emacs/init.el
@@ -3,7 +3,10 @@
;; C-h works too, except in normal/motion state where it moves to the window on the left.
;;; Code:
-;;;; 1. Emac setup, files, packages and paths
+;;;; 1. Emac setup, gc, files, packages
+(add-hook 'emacs-startup-hook
+ (lambda () (setq gc-cons-threshold (* 16 1024 1024))))
+
(dolist (bin (list (expand-file-name "~/.local/bin")
(expand-file-name "~/.go/bin")))
(add-to-list 'exec-path bin)
@@ -134,7 +137,7 @@
(kbd "TAB") #'my/insert-tab
(kbd "C-SPC") #'completion-at-point)
- ;; C-p, C-f and C-b are assigned to Consult below.
+ ;; C-p and C-f are assigned to Consult below, C-m to ibuffer.
(evil-define-key '(normal motion) 'global
(kbd "C-e") #'dired-jump))
@@ -144,7 +147,7 @@
;; Don't add its global [b ]b [e ]e ... bindings.
(evil-collection-want-unimpaired-p nil)
:config
- (evil-collection-init '(info)))
+ (evil-collection-init '(info ibuffer)))
;; Minibuffer. Vertico
(with-eval-after-load 'vertico
@@ -183,22 +186,16 @@
;; Searches start at the project root when there is one, else the current directory.
(use-package consult
:config
- ;; Hide noise from the buffer picker.
- (dolist (re '("\\`\\*scratch\\*\\'"
- "\\`\\*Messages\\*\\'"
- "\\`\\*Warnings\\*\\'"
- "\\`\\*xref\\*\\'"))
- (add-to-list 'consult-buffer-filter re))
-
;; Don't re-run the preview on every keystroke while typing a search.
(consult-customize consult-ripgrep
:preview-key '(:debounce 0.4 any)))
;; Bind immediately, without waiting for consult to load.
+;; C-m opens the full ibuffer list (evil-collection provides its navigation).
(evil-define-key '(normal motion) 'global
(kbd "C-p") #'consult-fd
(kbd "C-f") #'consult-ripgrep
- (kbd "C-m") #'consult-buffer)
+ (kbd "C-m") #'ibuffer)
;;;; 7. Dired
(setq dired-dwim-target t
@@ -222,12 +219,10 @@
;;;; Core config ends here.
;;;; -----------------------------------------------------------------
;;;; 8. Modules - additional functionality outside of the core init.el
+
;; Setup for orgmode
(load (locate-user-emacs-file "modules/org.el") nil 'nomessage)
-;; Setup for rss feed
-(load (locate-user-emacs-file "modules/rss.el") nil 'nomessage)
-
;; Magit and the :git command live in modules/git.el.
(load (locate-user-emacs-file "modules/git.el") nil 'nomessage)
diff --git a/config/emacs/modules/build.el b/config/emacs/modules/build.el
index 617eb9f..36e3978 100644
--- a/config/emacs/modules/build.el
+++ b/config/emacs/modules/build.el
@@ -2,16 +2,17 @@
;;; Commentary:
;; Loaded from init.el after Evil.
;;
-;; :build Run ./build.sh from the project root.
+;; :build Run ./build.sh from the nearest ancestor dir containing it.
;; :build ARGS Run ./build.sh ARGS, e.g. :build release -v
;;
+;; If no build.sh is found (or it isn't executable), report it and do nothing.
+;;
;; The compilation buffer opens at the bottom (45% height, full width) and
;; takes focus when the build starts. ANSI colours are rendered.
;; Kill a running build with M-x kill-compilation.
;;; Code:
-(setq compile-command "./build.sh"
- compilation-read-command nil ; run without prompting
+(setq compilation-read-command nil ; run without prompting
compilation-ask-about-save nil
compilation-scroll-output t)
@@ -31,10 +32,15 @@
;; :build runs ./build.sh; :build ARGS appends ARGS.
(evil-define-command my/ex-build (args)
- "Run `./build.sh ARGS' from the project root."
+ "Run `./build.sh ARGS' from the nearest directory containing build.sh."
(interactive "")
- (let ((compile-command (concat "./build.sh " args))) ; nil args -> ""
- (project-compile)))
+ (let ((root (locate-dominating-file default-directory "build.sh")))
+ (unless root
+ (user-error "No build.sh found"))
+ (let ((default-directory root))
+ (unless (file-executable-p "build.sh")
+ (user-error "build.sh is not executable"))
+ (compile (string-trim (concat "./build.sh " args)))))) ; nil args -> ""
(evil-ex-define-cmd "build" #'my/ex-build)
diff --git a/config/emacs/modules/rss.el b/config/emacs/modules/rss.el
deleted file mode 100644
index 28f6ecc..0000000
--- a/config/emacs/modules/rss.el
+++ /dev/null
@@ -1,278 +0,0 @@
-;;; rss.el --- Elfeed for YouTube feeds, played in mpv -*- lexical-binding: t; -*-
-;;; Commentary:
-;; Elfeed with a little glue for Evil. Needs the `elfeed' package (MELPA).
-;;
-;; :rss / :rss get open elfeed / also fetch all feeds now
-;;
-;; Search buffer RET show the entry
-;; o play in mpv and mark read
-;; r/u mark read / unread
-;; A mark everything listed as read
-;; c pick a channel/feed by name and show only it
-;; s change the filter; TAB completes +tags
-;; (type "+seb" TAB -> "+Sebastian_Lague")
-;; gr fetch all feeds
-;; q quit
-;; Entry buffer o play in mpv
-;; q close
-;;
-;; Every feed is tagged with its category (+youtube, +news) and with its
-;; own channel tag: its name with spaces replaced by "_". So
-;; "gingerBill" is +gingerBill and "Sebastian Lague" is +Sebastian_Lague.
-;; Tags match exactly, but in the `s' prompt TAB completes them, ignoring
-;; case. Give a feed a short tag of your own with a third argument:
-;; (my/rss-yt "Jonathan Blow highlights" "UC..." 'blow).
-;;
-;; Everything else is your normal Evil keys. Nothing is fetched
-;; automatically; fetching is always manual (:rss get / gr). Fetch errors
-;; are logged in the *elfeed-log* buffer.
-;;
-;; Feeds live in `my/rss-categories' below, grouped by tag. Edit the list
-;; and restart Emacs.
-;;; Code:
-
-(use-package elfeed
- :ensure t
- :defer t
- :custom
- (elfeed-search-filter "@1-month-ago +unread")
- ;; Be gentle with YouTube: few simultaneous connections.
- (elfeed-curl-max-connections 4)
- :config
- ;; Not in :custom: `my/rss-feeds' is defined below, and :custom runs
- ;; immediately, while :config waits until elfeed loads.
- (setq elfeed-feeds (my/rss-feeds))
- (my/rss-apply-titles)
- (my/rss-retag))
-
-(defun my/rss-yt (name channel-id &optional tag)
- "Feed entry for a YouTube channel, uploads only (no Shorts).
-TAG, a symbol, overrides the channel tag made from NAME."
- (let ((url (concat "https://www.youtube.com/feeds/videos.xml?playlist_id=UULF"
- (substring channel-id 2))))
- (if tag (list name url tag) (list name url))))
-
-(defvar my/rss-categories
- (list
- (list 'youtube
- (my/rss-yt "Awesome" "UCXzw-OdotBUcNA9yhuYQBwA")
- (my/rss-yt "Coding with Sphere" "UCpwl7jNE9PJc-lBTShNs5TQ")
- (my/rss-yt "Computerphile" "UC9-y-6csu5WGm29I7JiwpnA")
- (my/rss-yt "gingerBill" "UCUSck1dOH7VKmG4lRW7tZXg")
- (my/rss-yt "Jonathan Blow highlights" "UCavhLwXoB9tkqd-KsXpovOQ")
- (my/rss-yt "Karl Zylinski" "UC5YnADU2rChK7598A9gKygQ")
- (my/rss-yt "Nic Barker" "UCz-yrxeZYIYdpEZgHGvIydA")
- (my/rss-yt "Nir Lichtman" "UCAMu6Dso0ENoNm3sKpQsy0g")
- (my/rss-yt "Ryan Fleury" "UCCsdwE2z_kRL3vfVsd5OGyA")
- (my/rss-yt "Tariq10x" "UCXyisNI9ezbv7fvGMoSaFKg")
- (my/rss-yt "The Coding Gopher" "UC-mTIBh__DzAqW495JHXy5A")
- (my/rss-yt "Tsoding Daily" "UCrqM0Ym_NbK1fqeQG2VIohg")
- (my/rss-yt "zyapguy" "UCc4IizbM0q_JySK3XUY0Drw")
- (my/rss-yt "Dylan Falconer" "UCZzs3Umh6sRiYS_IQIQDThw")
- (my/rss-yt "Gamefromscratch" "UCr-5TdGkKszdbboXXsFZJTQ")
- (my/rss-yt "Jonas Tyroller" "UC_p_9arduPuxM8DHTGIuSOg")
- (my/rss-yt "Miziziziz" "UCaoqVlqPTH78_xjTjTOMcmQ")
- (my/rss-yt "Penusbmic" "UCjU-rielI7RsX1hqsCQdfhw")
- (my/rss-yt "randy" "UCUmLRMERmJrmUtgnbFfknAg")
- (my/rss-yt "randy uncut" "UCgXdyySqz7qH-lDiVG-7KwA")
- (my/rss-yt "raylib" "UC8WIBkhYb5sBNqXO1mZ7WSQ")
- (my/rss-yt "Sebastian Lague" "UCmtyQOKKmrMVaKuRXz02jbQ")
- (my/rss-yt "Tantan" "UChl_NKOs1qqh_x7yJfaDpDw")
- (my/rss-yt "ThinMatrix" "UCUkRj4qoT1bsWpE_C8lZYoQ")
- (my/rss-yt "Brodie Robertson" "UCld68syR8Wi-GY_n4CaoJGA")
- (my/rss-yt "ETA PRIME" "UC_0CVCfC_3iuHqmyClu59Uw")
- (my/rss-yt "Benny Surge" "UCkih_7PNYICxAZi1omr2_Og")
- (my/rss-yt "bizlychannel" "UCMPGiQ8gwDXFYpwQhX6kK9A")
- (my/rss-yt "Daz Games" "UCDBCCMlFD-hArPbWv38FINA")
- (my/rss-yt "Deep Dive Dev" "UC1yGsLABIDAqLVtPnoHbpEA")
- (my/rss-yt "Flo Woelki" "UC18qytfIhR9cNEjUcgGLl3A")
- (my/rss-yt "Gohjoe" "UCac9YlOpTLy_wToRb5Bo-DQ")
- (my/rss-yt "Herbcode" "UC79nyXj9gujAj5CySiy2FAQ")
- (my/rss-yt "hoff._world" "UCaezB99bVg9xlMS6Jn_ka0A")
- (my/rss-yt "Inbound Shovel" "UCdYwjLVP-98bptdlQFO_5zQ")
- (my/rss-yt "Jimmy Oakes" "UChp08hcL-qAJD4LK51ZZZvw")
- (my/rss-yt "Juby Philosophy" "UCLrpO7U1lnmbRtVxji843-Q")
- (my/rss-yt "Lance Hedrick" "UCR2_8uHyWcTSV6iIw6BN4rg")
- (my/rss-yt "Mental Moshpit" "UC5a4hb7i2UAT6DxmGz72hdg")
- (my/rss-yt "NIGHTRIDE" "UCuYLDxZWv-aal88czKkFvKA")
- (my/rss-yt "Rockers Games" "UCeq6focWSVKJy5DR1JxDGyw")
- (my/rss-yt "Slax" "UCc0V8-3xYIT6aNOAreAjRcw")
- (my/rss-yt "StayAtHomeDev" "UCDshKIMtFjYreCzApleuV8w")
- (my/rss-yt "Turkey Tom" "UCDKJdFer1phQI95UinPZehw")
- (my/rss-yt "Zei" "UCywwEsnSmADUyhtSaq5dKNg")
- (my/rss-yt "Zip Tie Tech" "UCUpt_5O1vYpWAGg77iNMQXA"))
- (list 'news
- '("Hacker News" "https://hnrss.org/frontpage?points=100")))
- "Feeds by tag: each entry is (TAG FEED...), a feed is (NAME URL [CHANNEL-TAG]).
-The tag is what you filter on in elfeed, e.g. \"+youtube\". Each feed
-also gets its own channel tag, see `my/rss-channel-tag'.")
-
-(defun my/rss-slug (name)
- "Tag symbol made from NAME, with spaces replaced by \"_\"."
- (intern (replace-regexp-in-string " +" "_" (string-trim name) t t)))
-
-(defun my/rss-channel-tag (feed)
- "Channel tag for FEED, (NAME URL [TAG]): TAG if given, else NAME slugged."
- (or (nth 2 feed) (my/rss-slug (car feed))))
-
-(defun my/rss-feeds ()
- "Elfeed feed list, (URL CATEGORY CHANNEL-TAG), from `my/rss-categories'."
- (mapcan (lambda (cat)
- (mapcar (lambda (feed)
- (list (cadr feed) (car cat) (my/rss-channel-tag feed)))
- (cdr cat)))
- my/rss-categories))
-
-(defun my/rss-all-feeds ()
- "Every (NAME URL) in `my/rss-categories', across all tags."
- (mapcan (lambda (cat) (copy-sequence (cdr cat))) my/rss-categories))
-
-(defun my/rss-apply-titles ()
- "Give each feed the name from `my/rss-categories' as its display title.
-YouTube playlist feeds all call themselves \"Videos\"; this overrides
-that, so the list shows the channel and \"=Name\" filters work. The
-title is stored as feed metadata, which fetching never overwrites."
- (dolist (feed (my/rss-all-feeds))
- (setf (elfeed-meta (elfeed-db-get-feed (cadr feed)) :title)
- (car feed))))
-
-(defun my/rss-retag ()
- "Give already-fetched entries their feed's tags from `elfeed-feeds'.
-Elfeed tags entries only as they arrive, so this catches up entries
-fetched before a feed's tags changed. Safe to run any time."
- (interactive)
- (elfeed-db-ensure)
- (let ((tags-by-url (make-hash-table :test #'equal))
- (n 0))
- (dolist (f elfeed-feeds)
- (puthash (car f) (cdr f) tags-by-url))
- (maphash
- (lambda (_id entry)
- (let* ((tags (gethash (elfeed-entry-feed-id entry) tags-by-url))
- (missing (cl-set-difference tags (elfeed-entry-tags entry))))
- (when missing
- (apply #'elfeed-tag entry missing)
- (cl-incf n))))
- elfeed-db-entries)
- (when (or (> n 0) (called-interactively-p 'any))
- (message "rss: retagged %d entries" n))))
-
-;;;; Playing videos
-
-(defun my/rss-play (url &rest _)
- "Play URL in mpv. Its output goes to the *mpv* buffer."
- (message "mpv: starting %s (can take a few seconds)..." url)
- (set-process-sentinel
- (start-process "mpv" "*mpv*" "mpv" url)
- (lambda (proc _event)
- (when (and (eq (process-status proc) 'exit)
- (/= (process-exit-status proc) 0))
- (message "mpv failed (exit %d), see the *mpv* buffer"
- (process-exit-status proc))
- (display-buffer "*mpv*")))))
-
-;; YouTube links go to mpv, everywhere in Emacs.
-(with-eval-after-load 'browse-url
- (add-to-list 'browse-url-handlers
- '("\\`https?://\\(?:www\\.\\|m\\.\\)?\\(?:youtube\\.com\\|youtu\\.be\\)/"
- . my/rss-play)))
-
-;;;; Elfeed
-
-(defvar my/rss-stagger 4
- "Seconds between feed fetches. YouTube answers with 404/500 when hit fast.")
-
-(defun my/rss-fetch ()
- "Fetch every feed, one at a time, `my/rss-stagger' seconds apart.
-Run it again to fill in feeds that failed; entries are not duplicated."
- (interactive)
- (seq-do-indexed
- (lambda (feed i)
- (run-with-timer (* my/rss-stagger i) nil #'elfeed-update-feed (car feed)))
- elfeed-feeds)
- (message "rss: fetching %d feeds, about %ds"
- (length elfeed-feeds) (* my/rss-stagger (length elfeed-feeds))))
-
-(defun my/rss-all-tags ()
- "Every tag name: the ones in `elfeed-feeds' plus any in the database."
- (delete-dups
- (mapcar #'symbol-name
- (append (mapcan (lambda (f) (copy-sequence (cdr f))) elfeed-feeds)
- (elfeed-db-get-all-tags)))))
-
-(defun my/rss-filter-capf ()
- "Complete the +tag or -tag at point in the filter prompt."
- (when (looking-back "\\(?:^\\| \\)[+-]\\([^ ]*\\)" (line-beginning-position))
- (list (match-beginning 1) (match-end 1)
- (completion-table-case-fold (my/rss-all-tags))
- :exclusive 'no)))
-
-(defvar my/rss-filter-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "TAB") #'completion-at-point)
- map)
- "Extra keys for the filter prompt.")
-
-(defun my/rss-filter ()
- "Elfeed's live filter, with TAB completing +tags and -tags."
- (interactive)
- (minibuffer-with-setup-hook
- (lambda ()
- (add-hook 'completion-at-point-functions #'my/rss-filter-capf nil t)
- (use-local-map (make-composed-keymap my/rss-filter-map
- (current-local-map))))
- (elfeed-search-live-filter)))
-
-(defun my/rss-mark-all-read ()
- "Mark every entry the current filter lists as read.
-Works on elfeed's list directly, so it doesn't depend on the region or
-on Evil's visual state. Clear the filter first to mark everything."
- (interactive)
- (let ((n (length elfeed-search-entries)))
- (elfeed-untag elfeed-search-entries 'unread)
- (elfeed-search-update :force)
- (message "rss: marked %d entries read" n)))
-
-(defun my/rss-channel ()
- "Pick a feed by name and show only its entries (read and unread).
-Filters on the feed URL, so it works whatever the feed calls itself.
-Press `s' and clear the filter to go back."
- (interactive)
- (let* ((feeds (my/rss-all-feeds))
- (name (completing-read "Channel: " feeds nil t))
- (url (cadr (assoc name feeds))))
- (elfeed-search-set-filter
- (concat "@6-months-ago =" (regexp-quote url)))))
-
-;; Elfeed's buffers start in motion state: Evil movement, no editing keys.
-(evil-set-initial-state 'elfeed-search-mode 'motion)
-(evil-set-initial-state 'elfeed-show-mode 'motion)
-
-(with-eval-after-load 'elfeed-search
- (evil-define-key 'motion elfeed-search-mode-map
- (kbd "RET") #'elfeed-search-show-entry
- "o" #'elfeed-search-browse-url ; plays via `browse-url-handlers', marks read
- "r" #'elfeed-search-untag-all-unread
- "u" #'elfeed-search-tag-all-unread
- "A" #'my/rss-mark-all-read
- "c" #'my/rss-channel
- "s" #'my/rss-filter
- "gr" #'my/rss-fetch
- "q" #'elfeed-search-quit-window))
-
-(with-eval-after-load 'elfeed-show
- (evil-define-key 'motion elfeed-show-mode-map
- "o" #'elfeed-show-visit
- "q" #'elfeed-kill-buffer))
-
-;; :rss opens elfeed; :rss get also fetches every feed now.
-(evil-define-command my/rss (args)
- "Open elfeed, or fetch all feeds too with `:rss get'."
- (interactive "")
- (elfeed)
- (when (equal (string-trim (or args "")) "get")
- (my/rss-fetch)))
-
-(evil-ex-define-cmd "rss" #'my/rss)
-
-;;; rss.el ends here
diff --git a/config/emacs/themes/minimal-theme.el b/config/emacs/themes/minimal-theme.el
index 0a92067..e3eaaa9 100644
--- a/config/emacs/themes/minimal-theme.el
+++ b/config/emacs/themes/minimal-theme.el
@@ -1,5 +1,7 @@
;;; minimal-theme.el --- Minimal high-functionality Ujelly port -*- lexical-binding: t; -*-
-;; Built on the compline-theme face structure, updated with the color palette from Ujelly (color-theme-ujelly by marktran).
+;;
+;; Built on the compline-theme face structure, updated with the color palette
+;; from Ujelly (color-theme-ujelly by marktran).
;;
;;; Code:
@@ -17,7 +19,7 @@
'(escape-glyph ((t (:foreground "#8fbfdc"))))
'(homoglyph ((t (:foreground "#8fbfdc"))))
'(minibuffer-prompt ((t (:foreground "#8fbfdc"))))
- '(highlight ((t (:foreground "#151515" :background "#fad07a"))))
+ '(highlight ((t (:foreground "#e8e8d3" :background "#303030" :weight bold))))
'(hl-line ((t (:extend t :background "#1b1b1b"))))
'(region ((t (:extend t :background "#404040"))))
'(shadow ((t (:foreground "#7f7f7f"))))
@@ -81,10 +83,10 @@
'(tab-bar-tab ((t (:inherit tab-bar :background "#262626" :foreground "#e8e8d3" :weight bold :box nil))))
'(tab-bar-tab-inactive ((t (:inherit tab-bar :background "#151515" :foreground "#999999" :weight normal :box nil))))
- ;;;; Search
+ ;;;; Search & Completions
'(isearch ((t (:weight bold :inherit (lazy-highlight)))))
'(isearch-fail ((t (:weight bold :foreground "#151515" :background "#cf6a4c"))))
- '(lazy-highlight ((t (:weight bold :foreground "#151515" :background "#fad07a"))))
+ '(lazy-highlight ((t (:weight bold :foreground "#e8e8d3" :background "#303030"))))
'(match ((t (:weight bold :foreground "#99ad6a" :background "#151515"))))
'(next-error ((t (:inherit (region)))))
'(query-replace ((t (:inherit (isearch)))))