;;; Preamble ;; Suppress native compilation warnings (setq native-comp-async-report-warnings-errors nil) ;; Set a location for the custom file so it doesn't pollute this file. (setq custom-file (locate-user-emacs-file "custom-vars.el")) ;;; Elpaca package manager (defvar elpaca-installer-version 0.11) (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory)) (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory)) (defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory)) (defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git" :ref nil :depth 1 :files (:defaults "elpaca-test.el" (:exclude "extensions")) :build (:not elpaca--activate-package))) (let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory)) (build (expand-file-name "elpaca/" elpaca-builds-directory)) (order (cdr elpaca-order)) (default-directory repo)) (add-to-list 'load-path (if (file-exists-p build) build repo)) (unless (file-exists-p repo) (make-directory repo t) (when (< emacs-major-version 28) (require 'subr-x)) (condition-case-unless-debug err (if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*")) ((zerop (apply #'call-process `("git" nil ,buffer t "clone" ,@(when-let ((depth (plist-get order :depth))) (list (format "--depth=%d" depth) "--no-single-branch")) ,(plist-get order :repo) ,repo)))) ((zerop (call-process "git" nil buffer t "checkout" (or (plist-get order :ref) "--")))) (emacs (concat invocation-directory invocation-name)) ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch" "--eval" "(byte-recompile-directory \".\" 0 'force)"))) ((require 'elpaca)) ((elpaca-generate-autoloads "elpaca" repo))) (progn (message "%s" (buffer-string)) (kill-buffer buffer)) (error "%s" (with-current-buffer buffer (buffer-string)))) ((error) (warn "%s" err) (delete-directory repo 'recursive)))) (unless (require 'elpaca-autoloads nil t) (require 'elpaca) (elpaca-generate-autoloads "elpaca" repo) (load "./elpaca-autoloads"))) (add-hook 'after-init-hook #'elpaca-process-queues) (elpaca `(,@elpaca-order)) (elpaca elpaca-use-package (elpaca-use-package-mode)) (elpaca-wait) ;; Magit depends on newer versions of seq/transient than Emacs bundles, but Elpaca ;; can't or won't update them on its own. (defun +elpaca-unload-seq (e) (and (featurep 'seq) (unload-feature 'seq t)) (elpaca--continue-build e)) (defun +elpaca-seq-build-steps () (append (butlast (if (file-exists-p (expand-file-name "seq" elpaca-builds-directory)) elpaca--pre-built-steps elpaca-build-steps)) (list '+elpaca-unload-seq 'elpaca--activate-package))) (use-package seq :ensure `(seq :build ,(+elpaca-seq-build-steps))) (use-package transient :ensure t) ;; Required for magit, but Elpaca seems unable to find it by default (use-package cond-let :ensure (:host github :repo "tarsius/cond-let")) ;;; OS-specific tweaks (defconst ON-LINUX (eq system-type 'gnu/linux)) (defconst ON-MAC (eq system-type 'darwin)) (defconst ON-BSD (or ON-MAC (eq system-type 'berkeley-unix))) (defconst ON-WINDOWS (memq system-type '(cygwin windows-nt ms-dos))) ;; Emacs on macOS doesn't naturally find shell variables like PATH, ;; so to help it out we run a shell and load some variables from the env command. (use-package exec-path-from-shell :ensure t :if (and ON-MAC (memq window-system '(mac ns))) :config (dolist (var '("NIX_SSL_CERT_FILE" "NIX_PATH" "NIX_PROFILES")) (add-to-list 'exec-path-from-shell-variables var)) (exec-path-from-shell-initialize)) (elpaca-wait) ;;; Keybindings ;;;; General (use-package emacs :bind (("C-c o b" . reveal-in-file-browser) ("C-h C-i" . info-apropos) ("C-x )" . kmacro-end-or-call-macro) ;; Eval related mappings ("C-x e" . nil) ("C-x e b" . eval-buffer) ("C-x e d" . eval-defun) ("C-x e r" . eval-region) ("C-x e e" . eval-expression) ;; Window-management ("C-o" . nil) ("C-o C-o" . other-window) ("C-o o" . other-window) ("C-o b" . consult-buffer-other-window-no-focus) ("C-o B" . consult-buffer-other-window) ("C-o /" . split-window-right) ("C-o -" . split-window-below) ("C-o C-k" . delete-window) ("C-o 1" . delete-other-windows) ("C-o t" . tab-bar-new-tab) ("C-o ]" . tab-bar-switch-to-next-tab) ("M-)" . tab-bar-switch-to-next-tab) ("M-(" . tab-bar-switch-to-prev-tab) ("C-o [" . tab-bar-switch-to-prev-tab) ("C-o T" . tab-bar-undo-close-tab) ("C-o w" . tab-bar-close-tab) ;; TODO C-/ for undo window state change ("C-o z" . my/zoom-window) ;; TODO other-window scrolling ("C-o n" . next-buffer) ("C-o p" . previous-buffer) :repeat-map kmacro-repeat-map (")" . kmacro-end-or-call-macro) :repeat-map tab-bar-repeat-map ("]" . tab-bar-switch-to-next-tab) ("[" . tab-bar-switch-to-prev-tab))) ;;;; which-key (use-package which-key :ensure t :init (which-key-mode) (which-key-setup-side-window-bottom)) ;;;; Mac modifier keys ;; Use right-option as regular option on Mac (setq ns-alternate-modifier 'meta) (setq ns-right-alternate-modifier 'none) (setq ns-command-modifier 'meta) (setq ns-right-command-modifier 'super) ;;;;; Training wheels (defun cmd-key-reminder (keybind emacs-alternative) "Display a beep and reminder message for macOS muscle memory keys. KEYBIND is the key combination pressed (e.g., 'Cmd-S'). EMACS-ALTERNATIVE is the Emacs equivalent (e.g., 'C-s')." (beep) (message "Muscle memory! Use %s instead of %s" emacs-alternative keybind)) ;; Define reminders for common macOS shortcuts (global-set-key (kbd "s-s") (lambda () (interactive) (cmd-key-reminder "Cmd-S" "C-x C-s (save-buffer)"))) (global-set-key (kbd "s-z") (lambda () (interactive) (cmd-key-reminder "Cmd-Z" "C-/ (undo)"))) (global-set-key (kbd "s-x") (lambda () (interactive) (cmd-key-reminder "Cmd-X" "C-w (kill-region)"))) (global-set-key (kbd "s-c") (lambda () (interactive) (cmd-key-reminder "Cmd-C" "M-w (copy-region-as-kill)"))) (global-set-key (kbd "s-v") (lambda () (interactive) (cmd-key-reminder "Cmd-V" "C-y (yank)"))) (global-set-key (kbd "s-a") (lambda () (interactive) (cmd-key-reminder "Cmd-A" "C-x h (mark-whole-buffer)"))) (global-set-key (kbd "s-0") (lambda () (interactive) (cmd-key-reminder "Cmd-0" "C-x 0 (text-scale-adjust)"))) (global-set-key (kbd "s-t") (lambda () (interactive) (cmd-key-reminder "Cmd-T" "C-o t (tab-bar-new-tab)"))) ;;; Editing ;;;; MWIM (use-package mwim :ensure t :bind (([remap move-beginning-of-line] . mwim-beginning) ([remap move-end-of-line] . mwim-end) :map visual-line-mode-map ("C-a" . mwim-beginning) ("C-e" . mwim-end)) :init (defun mwim-visual-line-end () (mwim-point-at (end-of-visual-line))) (defun mwim-visual-line-beginning () (mwim-point-at (beginning-of-visual-line))) :custom (mwim-beginning-position-functions '(mwim-visual-line-beginning mwim-line-beginning mwim-code-beginning mwim-comment-beginning)) (mwim-end-position-functions '(mwim-visual-line-end mwim-block-end mwim-code-end mwim-line-end))) ;;;; Isearch ;; Allow movement during isearch ;; C/M-v move to next/previous match not currently visible ;; M- move to first/last match in buffer (setq isearch-allow-motion t) (setq isearch-wrap-pause nil) ;;;; Avy (use-package avy :ensure t :bind (("C-j" . avy-goto-char-timer) ("M-l" . avy-goto-line) :map isearch-mode-map ("C-j" . avy-isearch) :map lisp-interaction-mode-map ("C-j" . nil)) :demand t :config (defun avy-action-embark (pt) (unwind-protect (save-excursion (goto-char pt) (embark-act)) (select-window (cdr (ring-ref avy-ring 0)))) t) (defun avy-action-embark-dwim (pt) (unwind-protect (save-excursion (goto-char pt) (embark-dwim)) (select-window (cdr (ring-ref avy-ring 0)))) t) (defun avy-action-isearch (pt) (unwind-protect (goto-char pt) (isearch-forward-thing-at-point)) t) (setf (alist-get ?\; avy-dispatch-alist) 'avy-action-embark) (setf (alist-get ?\: avy-dispatch-alist) 'avy-action-embark-dwim) (setf (alist-get ?i avy-dispatch-alist) 'avy-action-isearch) (setq avy-timeout-seconds 0.25) ;; Always show candidates even when there's just one, to give an ;; opportunity to select an avy action (setq avy-single-candidate-jump nil)) (use-package avy-zap :ensure t :bind (("C-z" . avy-zap-up-to-char-dwim) ("M-z" . avy-zap-to-char-dwim))) ;;;; Expand-region (use-package expand-region :ensure t :bind (("C-=" . er/expand-region))) ;;;; Dot-mode (use-package dot-mode :ensure t :config (global-dot-mode t)) ;;;; Vundo (use-package vundo :ensure t :bind ("M-/" . vundo)) ;;;; Outli (use-package outli :ensure (:host github :repo "jdtsmith/outli") :bind (:map outli-mode-map ("C-c C-p" . (lambda () (interactive) (outline-back-to-heading))) ;; Stop outline navigation from capturing C-p and C-n, which makes it ;; difficult to navigate into a section :map outline-navigation-repeat-map ("C-p" . nil) ("C-n" . nil)) :hook ((prog-mode . outli-mode) (text-mode . outli-mode))) ;;;; Jinx (use-package jinx :hook (emacs-startup . global-jinx-mode) :bind ([remap transpose-chars] . jinx-correct)) ;;; Completion ;;;; Vertico (use-package vertico :ensure t :init (vertico-mode) (vertico-multiform-mode 1) (add-to-list 'vertico-multiform-categories '(jinx grid (vertico-grid-annotate . 20)))) (use-package vertico-directory :after vertico :bind (:map vertico-map ("C-h" . vertico-directory-delete-word) ("C-l" . vertico-directory-enter))) ;;;; Marginalia (use-package marginalia :ensure t ;; Either bind `marginalia-cycle' globally or only in the minibuffer :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) :init (marginalia-mode)) ;;;; Orderless (use-package orderless :ensure t :init (setq completion-ignore-case t) (setq completion-styles '(orderless basic) completion-category-defaults nil completion-category-overrides '((file (styles partial-completion))))) ;;;; Consult (use-package consult :ensure t ;; Replace bindings. Lazily loaded due by `use-package'. :bind (("C-x b" . consult-buffer) ("C-x C-b" . consult-buffer) ("C-x p g" . consult-ripgrep) ("C-x p b" . consult-project-buffer) ("C-x p F" . consult-fd) ("M-i" . consult-imenu) ("M-y" . consult-yank-from-kill-ring)) ;; Enable automatic preview at point in the *Completions* buffer. This is ;; relevant when you use the default completion UI. ;; :hook (completion-list-mode . consult-preview-at-point-mode) :init ;; Optionally configure the register formatting. This improves the register ;; preview for `consult-register', `consult-register-load', ;; `consult-register-store' and the Emacs built-ins. (setq register-preview-delay 0.5 register-preview-function #'consult-register-format) ;; Optionally tweak the register preview window. ;; This adds thin lines, sorting and hides the mode line of the window. (advice-add #'register-preview :override #'consult-register-window) ;; Use Consult to select xref locations with preview (setq xref-show-xrefs-function #'consult-xref xref-show-definitions-function #'consult-xref) (defun consult-buffer-other-window-no-focus () (interactive) (with-selected-window (next-window) (consult-buffer))) :config ;; For some commands and buffer sources it is useful to configure the ;; :preview-key on a per-command basis using the `consult-customize' macro. (consult-customize consult-theme consult-xref consult-bookmark consult-ripgrep consult-git-grep consult-grep :preview-key '(:debounce 0.4 any) consult-recent-file consult-buffer consult--source-bookmark consult--source-recent-file consult--source-project-recent-file :preview-key "C-.")) (use-package consult-dir :ensure t :bind (("C-x C-d" . consult-dir) :map vertico-map ("C-x C-d" . consult-dir) ("C-x C-j" . consult-dir-jump-file))) ;;;; Embark (use-package embark :ensure t :bind (("C-;" . embark-act) ;; pick some comfortable binding ("C-:" . embark-dwim) ;; good alternative: M-. ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings' :init ;; Optionally replace the key help with a completing-read interface (setq prefix-help-command #'embark-prefix-help-command) :config ;; Hide the mode line of the Embark live/completions buffers (add-to-list 'display-buffer-alist '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*" nil (window-parameters (mode-line-format . none))))) ;; Consult users will also want the embark-consult package. (use-package embark-consult :ensure t :after (embark consult) :demand t ; only necessary if you have the hook below ;; if you want to have consult previews as you move around an ;; auto-updating embark collect buffer :hook (embark-collect-mode . consult-preview-at-point-mode)) ;;;; completion-at-point (use-package corfu :ensure t :bind (("C-'" . completion-at-point)) :init (global-corfu-mode)) (use-package cape :ensure t ;; Bind prefix keymap providing all Cape commands under a mnemonic key. :bind ("C-c p" . cape-prefix-map) ;; Alternative keys: M-p, M-+, ... :custom (cape-dabbrev-check-other-buffers nil) (cape-file-directory-must-exist nil) :init (add-hook 'completion-at-point-functions #'cape-dabbrev) (add-hook 'completion-at-point-functions #'cape-file) (add-hook 'completion-at-point-functions #'cape-elisp-block)) ;;;; yasnippet (use-package yasnippet :ensure t :init (require 'em-cmpl) (yas-global-mode 1)) ;;; Appearance ;;;; Theme (use-package doom-themes :ensure t :config ;; (setq doom-themes-enable-bold t ;; doom-themes-enable-italic t) ;; (load-theme 'doom-monokai-pro t) ;; Lighten comments slightly for better contrast ;; (custom-theme-set-faces ;; 'doom-monokai-pro ;; '(font-lock-comment-face ;; ((t (:foreground "#9A989A"))))) ;; (enable-theme 'doom-monokai-pro) ;; Enable flashing mode-line on errors (doom-themes-visual-bell-config) ;; Corrects (and improves) org-mode's native fontification. (doom-themes-org-config)) (use-package ef-themes :ensure t :config (load-theme 'ef-owl t) (enable-theme 'ef-owl)) ;;;; Modeline (use-package doom-modeline :ensure t :init (doom-modeline-mode 1) (setq doom-modeline-total-line-number nil)) (use-package nerd-icons :ensure t) ;;;; Window size (modify-all-frames-parameters '((alpha 99 99) (top . 50) (left . 100) (width . 190) (height . 60))) ;;;; Fonts (when window-system (setq my/font-candidates '("FiraCode Nerd Font Mono" "Fira Code" "Menlo" "Deja Vu Sans")) (defvar my/font (seq-find #'x-list-fonts my/font-candidates) "The default font to use.") (defvar my/font-size (if ON-MAC 14 12) "The default font size to use.") (modify-all-frames-parameters `((font . ,(concat my/font "-" (number-to-string my/font-size)))))) ;;;; Hide clutter (setq inhibit-startup-message t use-dialog-box nil) (tool-bar-mode -1) (scroll-bar-mode -1) ;; On a Mac, the menu bar doesn't take up screen real-estate, so leave it on (unless ON-MAC (menu-bar-mode -1)) ;;; Window management (setq switch-to-buffer-obey-display-actions t) (use-package emacs :bind (("s-t" . tab-bar-new-tab) ("s-w" . tab-bar-close-tab) ("s-}" . tab-bar-switch-to-next-tab) ("s-{" . tab-bar-switch-to-prev-tab)) :init (tab-bar-mode 1)) ;;;; Split window and focus (defun split-window-and-focus (split-function) "Split the window using SPLIT-FUNCTION and move the cursor to the new window." (interactive "aSplit function: ") (let ((new-window (funcall split-function))) (select-window new-window))) (defun split-window-below-and-focus () "Split the window horizontally and move the cursor below." (interactive) (split-window-and-focus 'split-window-below)) (defun split-window-right-and-focus () "Split the window vertically and move the cursor to the right." (interactive) (split-window-and-focus 'split-window-right)) (use-package emacs :bind (([remap split-window-below] . split-window-below-and-focus) ([remap split-window-right] . split-window-right-and-focus))) ;;;; Popper (use-package popper :ensure t :bind (("M-o" . popper-toggle) ("C-o ." . popper-cycle)) :init (setq popper-reference-buffers '("\\*Messages\\*" "\\*Warnings\\*" "Output\\*$" "\\*Async Shell Command\\*" "\\*Backtrace\\*" "\\*Compile-Log\\*" "\\*Flycheck errors\\*" help-mode compilation-mode "eshell-popup\\*$")) (defun my/popper-window-height (window) "Make eshell popups take half the frame height, otherwise defer to popper--fit-window-height" (let ((buffer (window-buffer window))) (if (with-current-buffer buffer (derived-mode-p 'eshell-mode)) (/ (frame-height (window-frame window)) 2) (popper--fit-window-height window)))) (setq popper-window-height #'my/popper-window-height) ;; (setq popper-group-function #'popper-group-by-project) (popper-mode +1) (popper-echo-mode +1)) ;;;; Zooming (defvar my/zoom-saved-windows nil "Variable to store the current window configuration for my/zoom.") (defun my/zoom-window () "Delete other windows, or restore the saved window configuration if available." (interactive) ;; Clean up if the saved config isn't valid anymore. (if (and my/zoom-saved-windows (not (window-configuration-p my/zoom-saved-windows))) (setq my/zoom-saved-windows nil)) (if my/zoom-saved-windows (progn (set-window-configuration my/zoom-saved-windows) (setq my/zoom-saved-windows nil)) (setq my/zoom-saved-windows (current-window-configuration)) (if (window-parameter (selected-window) 'window-side) (shrink-window -100) (delete-other-windows)))) ;;; Dired (use-package dired :bind (:map dired-mode-map ("C-o" . nil) ("h" . dired-up-directory))) (use-package dirvish :ensure t :init (dirvish-override-dired-mode)) ;;; Misc tweaks ;; When scrolling by page and hitting top/bottom, move cursor to top/bottom of buffer (setq-default scroll-error-top-bottom t) ;; Revert buffers when the underlying file has changed (global-auto-revert-mode 1) ;; Remember recent files (recentf-mode 1) (setq recentf-max-menu-items 20) (setq recentf-max-saved-items 1000) ;; Save what you enter into minibuffer prompts (setq history-length 25) (savehist-mode 1) ;; Visually mark the line the cursor is on ;; (global-hl-line-mode 1) (setq vc-follow-symlinks t) ;; Enable repeat mode. Keymaps are defined through use-package's :repeat-map directive. (repeat-mode 1) ;; Answer questions with y/n instead of yes/no (defalias 'yes-or-no-p 'y-or-n-p) ;; when opening a help window, switch focus to it unless a help window was already open (setq-default help-window-select t) ;; tabs are for monsters (setq-default indent-tabs-mode nil) (setq-default tab-width 4) (setq-default sentence-end-double-space nil) ;; Don't clobber the OS clipboard completely (setq save-interprogram-paste-before-kill t) (use-package ws-butler :ensure t :hook (prog-mode . ws-butler-mode)) ;; clean up backup file spam (setq backup-directory-alist `(("." . "~/.saves") ("/etc/\\.*" . nil)) delete-old-versions t kept-new-versions 4 kept-old-versions 2 version-control t) ;;;; Project (use-package project :config ;; Make project.el recognize directories with a .project file as project roots (add-to-list 'project-vc-extra-root-markers ".project")) ;;;; hl-todo (use-package hl-todo :ensure t :init (global-hl-todo-mode)) ;;;; View-mode ;; Read-only buffers activate view mode, toggle read only with C-x C-q (use-package view :init (setq view-read-only t) :bind (:map view-mode-map ("v" . View-scroll-half-page-forward) ("V" . View-scroll-half-page-backward))) ;;;; Direnv (use-package direnv :ensure t :config (setq direnv-always-show-summary nil) (direnv-mode)) ;;;; Explain-pause (use-package explain-pause-mode :ensure (:host github :repo "lastquestion/explain-pause-mode")) ;;;; Reveal-in-finder (defun first-executable (candidates) (seq-find #'executable-find candidates)) ;; Reveal in finder/nautilus/whatever (defun reveal-in-file-browser () (interactive) (call-process (first-executable '("xdg-open" "open")) nil nil nil ".")) ;;;; Hyperbole (use-package hyperbole :if nil :ensure (:repo "https://git.savannah.gnu.org/git/hyperbole.git" :tag "hyperbole-9.0.1") :bind (("C-o h" . hyperbole) ("C-o w" . hycontrol-windows) ("C-o f" . hycontrol-frames) :map hyperbole-mode-map ("M-" . nil) ("C-o " . hkey-either)) :custom (hkey-init nil) :init (hyperbole-mode 1)) ;;;; Emacs Server (server-start) ;;;; Just (use-package just-mode :ensure t) (use-package justl :ensure t :bind (("C-c j" . justl-exec-default-recipe) ("C-c J" . justl))) ;;;; Manually Enabled Commands ;; Enable narrow-to-region (C-x n n) (put 'narrow-to-region 'disabled nil) ;;; Org-mode (use-package org :bind (("C-c o ," . my/org-clear-all) ("C-c o s" . org-screenshot) ("C-c SPC" . org-table-blank-field) ("C-o a" . org-agenda) ("C-o d" . my/org-agenda-today) ;; Unbind to make room for avy and mwim :map org-mode-map ("C-j" . nil) ("C-a" . mwim-beginning) ("C-e" . mwim-end) :map org-agenda-mode-map ("C-x C-s" . org-save-all-org-buffers) :map org-src-mode-map ("C-c C-c" . org-edit-src-exit)) :hook ((org-mode . visual-line-mode)) :custom (org-agenda-files '("~/Seafile/Notes/TODO.org")) (org-special-ctrl-a/e 'reversed) (org-hide-emphasis-markers t) (org-use-speed-commands t) (add-to-list 'org-speed-commands '("h" . org-fold-hide-sublevels)) (org-use-fast-todo-selection 'expert) (org-todo-keywords '((sequence "TODO(t)" "BLOCKED(b)" "|" "DONE(d)"))) (org-use-property-inheritance '("STYLE")) (org-image-actual-width '(800)) (org-agenda-custom-commands '(("d" "Today's agenda and unscheduled TODOs" ((agenda "" ((org-agenda-span 1))) (todo "TODO" ((org-agenda-skip-function '(org-agenda-skip-entry-if 'scheduled 'deadline)))))))) :config (defun my/org-agenda-today () "Show org agenda for today only." (interactive) (org-agenda nil "d")) (add-hook 'org-after-todo-state-change-hook #'org-save-all-org-buffers)) (use-package org-bullets :ensure t :config (add-hook 'org-mode-hook (lambda () (org-bullets-mode 1))) ;; Eagerly load org-mode (with-temp-buffer (org-mode))) (defun my/org-clear-all () (interactive) (goto-char 0) (org-map-entries (lambda () (org-todo ""))) ;;(flush-lines "CLOSED") (message "Entries cleared.")) ;;;; Screenshots for games ;; Steps to set up: ;; 1. Install ShareX, add ShareX folder to PATH ;; 2. Create hotkey named GameNotes with settings: ;; Capture preconfigured window ;; Capture -> Pre configured window title: ;; Override after capture tasks to "Save image to file" ;; Override screenshots folder to org-screenshot-import-path ;; Override Upload settings -> File naming -> Name pattern ;; "GameNotes_%t_%y-%mo-%d-%h-%mi-%s.%ms" ;; 3. Put the following at the top of the org file: ;; #+STARTUP: inlineimages ;; #+ATTR_HTML: :width 500px (defvar org-screenshot-import-path "/mnt/c/Users/my/Seafile/Games/Screenshots/") (defvar org-screenshot-export-path org-screenshot-import-path) (defvar org-screenshot-exec-path "ShareX.exe") (defvar org-screenshot-exec-args "-s -workflow GameNotes") (defun org-screenshot () (interactive) (let* ((heading-name (org-get-heading t t t t)) ;; Strip out the font information from the heading name (_ (set-text-properties 0 (length heading-name) nil heading-name)) (_ (shell-command (concat org-screenshot-exec-path " " org-screenshot-exec-args))) ;; shell-command returns immediately, so wait until the screenshot is likely to have been taken (_ (sit-for 0.35)) (in-file (car (nreverse (directory-files org-screenshot-import-path 'full "GameNotes")))) (out-dir-name (file-name-sans-extension (file-name-nondirectory buffer-file-name))) (out-dir (file-name-as-directory (concat (file-name-as-directory org-screenshot-export-path) out-dir-name))) (out-file (concat out-dir heading-name "-" (org-id-uuid) ".png"))) (if (stringp in-file) (save-excursion (unless (file-exists-p out-dir) (make-directory out-dir)) (rename-file in-file out-file) (message (format "Saved %S to %S" in-file out-file)) (org-next-visible-heading 1) (org-open-line 1) (org-insert-link nil out-file)) (progn (message "Failed to find saved screenshot."))))) ;;; Magit (use-package magit :after seq :ensure t :custom (magit-save-repository-buffers 'dontask) ;; On Mac, use system git because Nix-installed git is very slow under magit (magit-git-executable (if ON-MAC "/usr/bin/git" "git")) :bind (("C-x g" . magit-status) :map magit-mode-map (":" . execute-extended-command) ("x" . magit-discard) :map magit-diff-section-map ("C-j" . nil) :map magit-file-section-map ;; Unbind to stop overriding avy ("C-j" . nil))) ;;; Programming ;;;; Treesitter (use-package treesit-auto :ensure t :custom (treesit-auto-install 'prompt) :config (treesit-auto-add-to-auto-mode-alist 'all) (global-treesit-auto-mode)) ;;;; Markdown (use-package markdown-mode :ensure t :mode ("README\\.md\\'" . gfm-mode) :hook ((markdown-mode . visual-line-mode))) ;; TODO - replace with https://github.com/sshaw/copy-as-format if necessary (defun copy-source-for-reddit () (interactive) (let ((contents (buffer-substring (point) (mark)))) (with-temp-buffer (insert contents) (mark-whole-buffer) (indent-rigidly (point) (mark) 4 t) (mark-whole-buffer) (kill-ring-save 0 0 t)))) (keymap-global-set "C-c o r" #'copy-source-for-reddit) ;;;; YAML (use-package yaml-mode :ensure t) ;;;; (use-package csv-mode :ensure t) ;;;; Rust (use-package rust-mode :ensure t) ;;;; Python (setq major-mode-remap-alist '((python-mode . python-ts-mode))) (use-package lsp-pyright :ensure t :custom (lsp-pyright-langserver-command "basedpyright") :hook (python-ts-mode . (lambda () (require 'lsp-pyright) (lsp)))) (setq lsp-pylsp-plugins-flake8-ignore '("D100" "D101" "D102")) ;;;; Docker (use-package dockerfile-mode :ensure t) ;;;; LSP (use-package lsp-mode :ensure t :commands lsp :custom (lsp-completion-provider :none) :init (setq lsp-keymap-prefix "C-c l") ;; from https://github.com/minad/corfu/wiki (defun my/lsp-mode-setup-completion () (setf (alist-get 'styles (alist-get 'lsp-capf completion-category-defaults)) '(orderless))) (defun lsp-booster--advice-json-parse (old-fn &rest args) "Try to parse bytecode instead of json." (or (when (equal (following-char) ?#) (let ((bytecode (read (current-buffer)))) (when (byte-code-function-p bytecode) (funcall bytecode)))) (apply old-fn args))) (advice-add (if (progn (require 'json) (fboundp 'json-parse-buffer)) 'json-parse-buffer 'json-read) :around #'lsp-booster--advice-json-parse) (defun lsp-booster--advice-final-command (old-fn cmd &optional test?) "Prepend emacs-lsp-booster command to lsp CMD." (let ((orig-result (funcall old-fn cmd test?))) (if (and (not test?) ;; for check lsp-server-present? (not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper lsp-use-plists (not (functionp 'json-rpc-connection)) ;; native json-rpc (executable-find "emacs-lsp-booster")) (progn (when-let ((command-from-exec-path (executable-find (car orig-result)))) ;; resolve command from exec-path (in case not found in $PATH) (setcar orig-result command-from-exec-path)) (message "Using emacs-lsp-booster for %s!" orig-result) (cons "emacs-lsp-booster" orig-result)) orig-result))) (advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command) :hook ((rust-ts-mode . lsp-deferred) (python-ts-mode . lsp-deferred) (lsp-mode . lsp-enable-which-key-integration) (lsp-completion-mode . my/lsp-mode-setup-completion))) (use-package lsp-ui :ensure t :commands lsp-ui-mode :custom (lsp-ui-sideline-show-diagnostics nil)) ;;;; Flycheck (use-package flycheck :ensure t :init (setq flycheck-keymap-prefix (kbd "C-c e")) (global-flycheck-mode) ;; Elisp lint errors are annoying, so disable flycheck in elisp modes :hook (emacs-lisp-mode . (lambda () (flycheck-mode -1))) :bind (:repeat-map flycheck-repeat-map ("n" . flycheck-next-error) ("p" . flycheck-previous-error))) ;;;; Nix (use-package nix-mode :ensure t :mode "\\.nix\\'") ;;;; Lisp (if (string-match "x86_64-apple" system-configuration) ;; The latest version of parinfer-rust-mode is incompatible with x86 Macs, so fall back on an earlier commit (use-package parinfer-rust-mode :ensure (:host github :repo "justinbarclay/parinfer-rust-mode" :ref "8df117a3b54d9e01266a3905b132a1d082944702") :hook emacs-lisp-mode) (use-package parinfer-rust-mode :ensure t :hook emacs-lisp-mode)) ;;;; Web ;;;;; PHP (use-package php-mode :ensure t) ;;;;; Svelte (use-package svelte-mode :ensure t) (use-package typescript-mode :ensure t) ;;; Eshell ;;;; Eshell (require 'cl-lib) (defun find-window-matching (regexp) "Find a visible window displaying a buffer whose name matches REGEXP." (cl-loop for window in (window-list) when (string-match regexp (buffer-name (window-buffer window))) thereis window)) (defun my/split-largest-eshell-and-focus () "Find the largest eshell window in the current frame, split it along its longest dimension, and create a new eshell in the split. The new eshell uses the `default-directory` of the buffer that was current when this command was invoked. Finally, the new eshell window is selected." (interactive) (let* ((starting-dir default-directory) (largest-eshell-window (cl-loop with max-area = 0 with largest-win = nil for win in (window-list) when (with-current-buffer (window-buffer win) (eq major-mode 'eshell-mode)) do (let ((area (* (window-total-width win) (window-total-height win)))) (when (> area max-area) (setq max-area area) (setq largest-win win))) finally return largest-win)) (new-window (with-selected-window largest-eshell-window (if (> (window-total-width) (* 2 (window-total-height))) (split-window-right) (split-window-below))))) (select-window new-window) (let ((default-directory starting-dir)) (eshell t)))) (defun my/eshell-delete-char-or-exit () "In Eshell, exit if at an empty prompt, otherwise delete a character." (interactive) ;; Check if the cursor is at the end of the buffer AND right after the prompt marker. ;; This is the condition for being on a clean, empty prompt. (if (and (eobp) (save-excursion (let ((orig (point))) (beginning-of-line) (= (point) orig)))) (progn (kill-this-buffer) (when (not (one-window-p)) (delete-window))) (delete-char 1))) (use-package eshell :bind (("M-`" . my/eshell-toggle) :map eshell-mode-map ("C-S-" . my/split-largest-eshell-and-focus) ("C-d" . my/eshell-delete-char-or-exit)) :config (setq eshell-destroy-buffer-when-process-dies t eshell-scroll-to-bottom-on-input t eshell-history-size 10000 eshell-save-history-on-exit t eshell-visual-commands '() eshell-banner-message "") (defun my/eshell-toggle () "Toggle eshell based on context. If in eshell, call `popper-toggle`. If not in eshell but in a project, call `project-eshell`. Otherwise, call `eshell`." (interactive) (if (find-window-matching "\\*.*eshell-popup\\*$") (popper-toggle) (if (project-current) (my/project-eshell-popup) (my/eshell-popup default-directory "*eshell-popup*")))) (defun my/eshell-popup (directory name) (defvar eshell-buffer-name) (let* ((default-directory directory) (eshell-buffer-name name) (eshell-buffer (get-buffer eshell-buffer-name))) (if eshell-buffer (pop-to-buffer eshell-buffer (bound-and-true-p display-comint-buffer-action)) (eshell t)))) (defun my/project-eshell-popup () (my/eshell-popup (project-root (project-current t)) (project-prefixed-buffer-name "eshell-popup"))) (defun eshell/ec (&rest args) "Substitute for emacsclient alias from inside eshell" (apply #'find-file args)) ;; From https://karthinks.com/software/jumping-directories-in-eshell/ (defun eshell/j (&optional regexp) "Navigate to a previously visited directory in eshell, or to any directory proffered by `consult-dir'." (let ((eshell-dirs (delete-dups (mapcar 'abbreviate-file-name (ring-elements eshell-last-dir-ring))))) (cond ((and (not regexp) (featurep 'consult-dir)) (let* ((consult-dir--source-eshell `(:name "Eshell" :narrow ?e :category file :face consult-file :items ,eshell-dirs)) (consult-dir-sources (cons consult-dir--source-eshell consult-dir-sources))) (eshell/cd (substring-no-properties (consult-dir--pick "Switch directory: "))))) (t (eshell/cd (if regexp (eshell-find-previous-directory regexp) (completing-read "cd: " eshell-dirs)))))))) (use-package eshell :bind (:map eshell-hist-mode-map ("M-r" . consult-history)) :after em-hist) (use-package pcre2el :ensure t :config (defmacro prx (&rest rx-sexp) "Convert rx-compatible regular expressions to PCRE." `(rxt-elisp-to-pcre (rx ,@rx-sexp)))) (use-package eshell-p10k :ensure (:host github :repo "elken/eshell-p10k") :config (eshell-p10k-def-segment time "" (format-time-string "%H:%M" (current-time)) 'eshell-p10k-distro-face) (defun num-exitcode-string () (if (= eshell-last-command-status 0) (number-to-string eshell-p10k--prompt-num-index) (format "%d (%d)" eshell-p10k--prompt-num-index eshell-last-command-status))) (defun num-exitcode-face () (if (= eshell-last-command-status 0) 'eshell-p10k-git-clean-face 'eshell-p10k-git-dirty-face)) (eshell-p10k-def-segment num-exitcode "" (num-exitcode-string) (num-exitcode-face)) (defun eshell-p10k-prompt-function () "Prompt defining function." (eshell-p10k-def-prompt '(num-exitcode time dir))) (setq eshell-prompt-function #'eshell-p10k-prompt-function eshell-prompt-regexp eshell-p10k-prompt-string)) (use-package eshell-syntax-highlighting :after eshell-mode :ensure t :config (eshell-syntax-highlighting-global-mode +1)) ;;;; Eat (use-package eat :ensure t :custom (eat-term-name "xterm-256color") :init (eval-after-load 'eshell #'eat-eshell-mode) (eval-after-load 'eshell #'eat-eshell-visual-command-mode) :config (defun advise-eat-keymap (map) (define-key map [?\C-o] nil) (define-key map (vector meta-prefix-char ?`) nil) (define-key map [S-v] #'eat-yank)) (define-advice eat-eshell-semi-char-mode (:after (&rest r)) (advise-eat-keymap eat-eshell-semi-char-mode-map)) (define-advice eat-semi-char-mode (:after (&rest r)) (advise-eat-keymap eat-semi-char-mode-map)) :hook (eat-mode . (lambda () (setq display-line-numbers nil) (hl-line-mode -1)))) ;;; PVS (setq ilisp-*use-fsf-compliant-keybindings* t)