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)))))