;;; init.el --- Personal Emacs config -*- lexical-binding: t; -*- ;; ------------------------- ;; Basic UI ;; ------------------------- (setq inhibit-startup-message t confirm-kill-processes nil ring-bell-function 'ignore) (tool-bar-mode -1) (menu-bar-mode -1) (scroll-bar-mode -1) (setq backup-directory-alist `(("." . ,(expand-file-name "/tmp/backups/" user-emacs-directory)))) (setq auto-save-hook nil) (push '(fullscreen . maximized) default-frame-alist) (setq-default indent-tabs-mode t tab-width 8 window-combination-resize t history-delete-duplicates t) ;; ------------------------- ;; Package setup ;; ------------------------- (require 'package) (setq package-archives '(("melpa" . "https://melpa.org/packages/") ("gnu" . "https://elpa.gnu.org/packages/") ("nongnu" . "https://elpa.nongnu.org/nongnu/"))) (package-initialize) (unless package-archive-contents (package-refresh-contents)) (unless (package-installed-p 'use-package) (package-install 'use-package)) (require 'use-package) (setq use-package-always-ensure t) ;; ------------------------- ;; Theme ;; ------------------------- (use-package gruvbox-theme) (load-theme 'gruvbox t) ;; ------------------------- ;; Completion stack ;; ------------------------- (use-package vertico :init (vertico-mode)) (use-package orderless :init (setq completion-styles '(orderless basic))) (use-package marginalia :init (marginalia-mode)) (use-package consult :bind (("C-x b" . consult-buffer) ("C-x p f" . project-find-file))) (use-package savehist :ensure nil :init (savehist-mode)) (use-package embark :bind (("C-." . embark-act) ("C-;" . embark-dwim))) (use-package embark-consult) (use-package corfu :init (global-corfu-mode) :custom (corfu-auto t) (corfu-cycle t) (corfu-auto-delay 0.1) (corfu-auto-prefix 1) :bind (:map corfu-map ("C-t" . corfu-next) ("C-n" . corfu-previous) ("" . corfu-quit))) (use-package cape :init (add-to-list 'completion-at-point-functions #'cape-file) (add-to-list 'completion-at-point-functions #'cape-dabbrev)) ;; ------------------------- ;; Icons + UI polish ;; ------------------------- (use-package nerd-icons) (use-package nerd-icons-completion :after marginalia :init (nerd-icons-completion-mode) (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup)) ;; ------------------------- ;; File browser: Dirvish ;; ------------------------- (use-package dirvish :init ;; Makes ordinary `dired' buffers use Dirvish automatically. (dirvish-override-dired-mode) :custom (dirvish-quick-access-entries '(("h" "~/" "Home") ("d" "~/Downloads/" "Downloads") ("c" "~/Code/" "Code") ("e" "~/.emacs.d/" "Emacs"))) (dirvish-mode-line-format '(:left (sort symlink) :right (omit yank index))) (dirvish-attributes '(nerd-icons file-time file-size collapse subtree-state vc-state git-msg)) (dirvish-side-attributes '(vc-state file-size nerd-icons collapse)) :config ;; Preview images, GIFs, PDFs, archives, audio, and video thumbnails. ;; ;; Recommended system packages: ;; Arch: sudo pacman -S ffmpegthumbnailer libvips poppler mpv imv ;; Debian/Ubuntu: sudo apt install ffmpegthumbnailer libvips-tools poppler-utils mpv imv ;; ;; Notes: ;; - Images preview automatically at point. ;; - Videos preview as thumbnails; open them in mpv for playback. (setq dirvish-preview-dispatchers '(image gif video audio epub archive font pdf fallback)) ;; Open media asynchronously so Emacs/EXWM does not block while mpv/imv is open. (defun my/open-file-externally (&optional file) "Open FILE asynchronously using a suitable external program." (interactive) (let* ((file (or file (dired-get-file-for-visit))) (lower (downcase file)) (program (cond ((string-match-p "\\.\\(mp4\\|mkv\\|webm\\|mov\\|avi\\)\\'" lower) "mpv") ((string-match-p "\\.\\(png\\|jpg\\|jpeg\\|gif\\|webp\\|svg\\)\\'" lower) "imv") (t "xdg-open")))) (start-process "open-file-externally" nil program file))) ;; Dired's `!' runs a foreground shell command unless you add `&'. ;; This keeps `o' as a non-blocking media opener. :bind (("C-c f d" . dirvish) ("C-c f j" . dirvish-side) :map dirvish-mode-map ("q" . quit-window) ("TAB" . dirvish-subtree-toggle) ("M-t" . dired-next-line) ("M-n" . dired-previous-line) ("RET" . dired-find-file) ("o" . my/open-file-externally) ("SPC" . dirvish-show-history) ("a" . dirvish-quick-access) ("f" . dirvish-file-info-menu) ("y" . dirvish-yank-menu) ("N" . dirvish-narrow) ("^" . dired-up-directory))) ;; ------------------------- ;; Which-key ;; ------------------------- (use-package which-key :config (which-key-mode) (setq which-key-idle-delay 0.5)) ;; ------------------------- ;; Git ;; ------------------------- (use-package magit :bind (("C-x g" . magit-status))) (use-package diff-hl :init (global-diff-hl-mode) (add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh) (add-hook 'prog-mode-hook #'diff-hl-mode)) ;; ------------------------- ;; Project ;; ------------------------- (use-package project :ensure nil) ;; ------------------------- ;; Browser / URL handling ;; ------------------------- (defvar my/browser "firefox") (defun my/open-browser () "Open the external browser." (interactive) (start-process-shell-command "browser" nil my/browser)) (defun my/browse-url (url &optional _new-window) "Open URL in the external browser." (start-process-shell-command "browser-url" nil (format "%s %s" my/browser (shell-quote-argument url)))) (setq browse-url-browser-function #'my/browse-url) ;; ------------------------- ;; Dvorak-friendly movement fallbacks ;; ;; C-s and C-r are left as normal Emacs search keys: ;; C-s = isearch-forward ;; C-r = isearch-backward ;; ;; Dvorak movement is available on M-h/M-t/M-n/M-s: ;; M-h = left ;; M-t = down ;; M-n = up ;; M-s = right ;; ------------------------- (global-set-key (kbd "C-s") #'isearch-forward) (global-set-key (kbd "C-r") #'isearch-backward) (global-set-key (kbd "M-h") #'backward-char) (global-set-key (kbd "M-t") #'next-line) (global-set-key (kbd "M-n") #'previous-line) (global-set-key (kbd "M-s") #'forward-char) (use-package avy :bind (("C-:" . avy-goto-char) ("C-'" . avy-goto-word-1))) (use-package repeat :ensure nil :init (repeat-mode)) ;; ------------------------- ;; Typst ;; ------------------------- ;; Requires system binaries: ;; typst - Typst compiler ;; tinymist - Typst language server ;; ;; Arch/Artix: ;; paru -S typst tinymist ;; ;; Then in Emacs: ;; M-x treesit-install-language-grammar RET typst RET (use-package treesit :ensure nil :init (add-to-list 'treesit-language-source-alist '(typst "https://github.com/uben0/tree-sitter-typst"))) (use-package typst-ts-mode :mode ("\\.typ\\'" . typst-ts-mode) :hook ((typst-ts-mode . eglot-ensure) (typst-ts-mode . visual-line-mode)) :custom (typst-ts-mode-watch-options "--open") :bind (:map typst-ts-mode-map ("C-c C-c" . typst-ts-compile) ("C-c C-w" . typst-ts-watch-mode) ("C-c C-p" . typst-ts-preview))) ;; ------------------------- ;; Leader-like keybindings using C-c ;; ------------------------- ;; GPT (global-set-key (kbd "C-c a a") #'gptel) (global-set-key (kbd "C-c a s") #'gptel-send) (global-set-key (kbd "C-c a r") #'gptel-rewrite) ;; Buffers (global-set-key (kbd "C-c b b") #'consult-buffer) (global-set-key (kbd "C-c b d") #'kill-current-buffer) (global-set-key (kbd "C-c b n") #'next-buffer) (global-set-key (kbd "C-c b p") #'previous-buffer) (global-set-key (kbd "C-c b s") #'save-buffer) ;; Files (global-set-key (kbd "C-c f f") #'find-file) (global-set-key (kbd "C-c f d") #'dirvish) (global-set-key (kbd "C-c f j") #'dirvish-side) (global-set-key (kbd "C-c f r") #'consult-recent-file) (global-set-key (kbd "C-c f s") #'save-buffer) ;; Open external browser (global-set-key (kbd "C-c o b") #'my/open-browser) ;; Windows — h/t/n/s mirrors your Dvorak movement (global-set-key (kbd "C-c w h") #'windmove-left) (global-set-key (kbd "C-c w t") #'windmove-down) (global-set-key (kbd "C-c w n") #'windmove-up) (global-set-key (kbd "C-c w s") #'windmove-right) (global-set-key (kbd "C-c w d") #'delete-window) (global-set-key (kbd "C-c w v") #'split-window-right) (global-set-key (kbd "C-c w -") #'split-window-below) (global-set-key (kbd "C-c w H") #'shrink-window-horizontally) (global-set-key (kbd "C-c w S") #'enlarge-window-horizontally) (global-set-key (kbd "C-c w N") #'enlarge-window) (global-set-key (kbd "C-c w T") #'shrink-window) ;; Project (global-set-key (kbd "C-c p p") #'project-switch-project) (global-set-key (kbd "C-c p f") #'project-find-file) (global-set-key (kbd "C-c p g") #'consult-ripgrep) (global-set-key (kbd "C-c p b") #'consult-project-buffer) ;; Git (global-set-key (kbd "C-c g g") #'magit-status) (global-set-key (kbd "C-c g b") #'magit-blame) (global-set-key (kbd "C-c g l") #'magit-log-current) (global-set-key (kbd "C-c g d") #'magit-diff-buffer-file) ;; LSP / Code (global-set-key (kbd "C-c c a") #'eglot-code-actions) (global-set-key (kbd "C-c c r") #'eglot-rename) (global-set-key (kbd "C-c c f") #'eglot-format-buffer) (global-set-key (kbd "C-c c d") #'xref-find-definitions) (global-set-key (kbd "C-c c D") #'xref-find-references) (global-set-key (kbd "C-c c i") #'consult-imenu) ;; Typst (global-set-key (kbd "C-c y c") #'typst-ts-compile) (global-set-key (kbd "C-c y w") #'typst-ts-watch-mode) (global-set-key (kbd "C-c y p") #'typst-ts-preview) ;; Errors (global-set-key (kbd "C-c e n") #'flymake-goto-next-error) (global-set-key (kbd "C-c e p") #'flymake-goto-prev-error) (global-set-key (kbd "C-c e l") #'flymake-show-buffer-diagnostics) ;; Search (global-set-key (kbd "C-c s s") #'consult-line) (global-set-key (kbd "C-c s g") #'consult-ripgrep) (global-set-key (kbd "C-c s i") #'consult-imenu) ;; Toggles (global-set-key (kbd "C-c t s") #'flyspell-mode) (global-set-key (kbd "C-c t w") #'whitespace-mode) ;; Spell language ;; C-c l is already used by xkbmap-switcher, so use C-c d for dictionaries. (global-set-key (kbd "C-c d e") #'my/ispell-en) (global-set-key (kbd "C-c d c") #'my/ispell-cs) ;; Quit (global-set-key (kbd "C-c q q") #'save-buffers-kill-emacs) ;; ------------------------- ;; Eglot (LSP) ;; ------------------------- (use-package eglot :ensure nil :hook ((c-mode . eglot-ensure) (c++-mode . eglot-ensure) (c-ts-mode . eglot-ensure) (c++-ts-mode . eglot-ensure) (nasm-mode . eglot-ensure) (python-mode . eglot-ensure) (python-ts-mode . eglot-ensure) (typst-ts-mode . eglot-ensure)) :custom (eglot-autoshutdown t) (eglot-confirm-server-initiated-edits nil) :config (defun my/eglot-format-on-save () (add-hook 'before-save-hook #'eglot-format-buffer nil t)) (add-hook 'c-mode-hook #'my/eglot-format-on-save) (add-hook 'c++-mode-hook #'my/eglot-format-on-save) (add-hook 'c-ts-mode-hook #'my/eglot-format-on-save) (add-hook 'c++-ts-mode-hook #'my/eglot-format-on-save) (setq c-ts-mode-indent-offset 8 c-ts-mode-indent-style 'bsd) (add-to-list 'eglot-server-programs '((c-mode c++-mode c-ts-mode c++-ts-mode) . ("clangd" "--background-index" "--clang-tidy" "--completion-style=detailed" "--header-insertion=never" "--fallback-style=none"))) (add-to-list 'eglot-server-programs '(python-mode . ("pylsp"))) (add-to-list 'eglot-server-programs '(typst-ts-mode . ("tinymist")))) ;; ------------------------- ;; Flymake (used by Eglot) ;; ------------------------- (use-package flymake :ensure nil :custom (flymake-no-changes-timeout 1.0) :bind (:map flymake-mode-map ("M-n" . flymake-goto-next-error) ("M-p" . flymake-goto-prev-error))) ;; ------------------------- ;; NASM mode ;; ------------------------- (use-package nasm-mode :hook (asm-mode . nasm-mode)) ;; ------------------------- ;; EXWM ;; ------------------------- (use-package exwm :config (setq exwm-workspace-number 5) (add-hook 'exwm-update-title-hook (lambda () (exwm-workspace-rename-buffer (format "%s: %s" exwm-class-name exwm-title)))) (setq mouse-autoselect-window t focus-follows-mouse t) (setq exwm-layout-show-all-buffers t exwm-workspace-show-all-buffers t) (defvar my/terminal "kitty") (setq exwm-input-global-keys `((,(kbd "s-r") . exwm-reset) (,(kbd "s-") . (lambda () (interactive) (start-process-shell-command "term" nil my/terminal))) (,(kbd "s-b") . my/open-browser) (,(kbd "s-d") . dirvish) (,(kbd "s-x") . (lambda () (interactive) (start-process-shell-command "lock" nil "betterscreenlock -l"))) (,(kbd "s-q") . kill-buffer) (,(kbd "s-f") . exwm-floating-toggle-floating) ;; Window resize — uppercase H/T/N/S mirrors movement (,(kbd "s-H") . shrink-window-horizontally) (,(kbd "s-S") . enlarge-window-horizontally) (,(kbd "s-N") . enlarge-window) (,(kbd "s-T") . shrink-window) ;; Window focus — lowercase mirrors movement (,(kbd "s-h") . windmove-left) (,(kbd "s-s") . windmove-right) (,(kbd "s-n") . windmove-up) (,(kbd "s-t") . windmove-down) ,@(mapcar (lambda (i) `(,(kbd (format "s-%d" i)) . (lambda () (interactive) (exwm-workspace-switch ,i)))) (number-sequence 0 4)) (,(kbd "s-") . exwm-workspace-switch-next) (,(kbd "s-S-") . exwm-workspace-switch-previous) (,(kbd "s-w") . exwm-workspace-move-window) (,(kbd "s-&") . (lambda (command) (interactive (list (read-shell-command "$ "))) (start-process-shell-command command nil command))))) (setq exwm-manage-configurations '(((string-match-p "dialog" exwm-class-name) floating t) ((string-match-p "confirm" exwm-title) floating t))) ;; NOTE: Your monitor names differ here from the xrandr command below. ;; Keep whichever names are correct on your machine. (setq exwm-randr-workspace-monitor-plist '(0 "DP-4" 1 "HDMI-0")) (add-hook 'exwm-randr-screen-change-hook (lambda () (start-process-shell-command "xrandr" nil "xrandr --output DP-3 --primary --auto \ --output HDMI-1 --auto --left-of DP-3"))) (exwm-randr-mode 1) (defun my/exwm-init () (start-process-shell-command "pipewire" nil "pipewire") (start-process-shell-command "pipewire-pulse" nil "pipewire-pulse") (start-process-shell-command "wireplumber" nil "wireplumber")) (add-hook 'exwm-init-hook #'my/exwm-init) (when (and (getenv "DISPLAY") (not (getenv "EXWM_SKIP"))) (exwm-enable))) ;; ------------------------- ;; Audio keys ;; ------------------------- (global-set-key (kbd "") (lambda () (interactive) (start-process-shell-command "vol-up" nil "amixer set Master 5%+"))) (global-set-key (kbd "") (lambda () (interactive) (start-process-shell-command "vol-down" nil "amixer set Master 5%-"))) (global-set-key (kbd "") (lambda () (interactive) (start-process-shell-command "mute" nil "amixer set Master toggle"))) ;; ------------------------- ;; Navidrome ;; ------------------------- (use-package naviel :load-path "~/Code/naviel/" :config (setq naviel-url "http://navi.cdatgoose.org") (setq naviel-eq-backend 'lavfi)) (use-package tasker :load-path "~/Code/tasker" :commands (tasker-list tasker-new tasker-open tasker-add-code-link) :bind (:map tasker-list-mode-map ("q" . quit-window))) ;; ------------------------- ;; EAF ;; ------------------------- ;; EAF is kept available, but it no longer overrides `browse-url'. ;; Use `eaf-open-browser' manually when you specifically want EAF. (use-package eaf :load-path "~/.emacs.d/site-lisp/emacs-application-framework" :custom (eaf-browser-continue-where-left-off t) (eaf-browser-enable-adblocker t) :config (defalias 'browse-web #'eaf-open-browser)) ;; ------------------------- ;; XKB switcher (Dvorak-friendly) ;; ------------------------- (use-package xkbmap-switcher :load-path "~/Code/keyswtch/" :config (global-set-key (kbd "C-c l") #'xkbmap-switcher-cycle) (global-set-key (kbd "C-c L") #'xkbmap-switcher-set)) ;; ------------------------- ;; Spell checking (Hunspell EN + CZ) ;; ------------------------- (use-package flyspell :ensure nil :hook ((text-mode . flyspell-mode) (prog-mode . flyspell-prog-mode)) :init (setq ispell-program-name "hunspell") (setq ispell-dictionary "en_US") (setq ispell-local-dictionary-alist '(("en_US" "[A-Za-z]" "[^A-Za-z]" "[']" nil ("-d" "en_US") nil utf-8) ("cs_CZ" "[A-Za-zÁ-ž]" "[^A-Za-zÁ-ž]" "[']" nil ("-d" "cs_CZ") nil utf-8)))) ;; ------------------------- ;; Quick language switching ;; ------------------------- (defun my/ispell-en () "Switch spell check to English." (interactive) (ispell-change-dictionary "en_US") (message "Switched to English (en_US)")) (defun my/ispell-cs () "Switch spell check to Czech." (interactive) (ispell-change-dictionary "cs_CZ") (message "Switched to Czech (cs_CZ)")) (global-set-key (kbd "C-c s e") #'my/ispell-en) (global-set-key (kbd "C-c s c") #'my/ispell-cs) ;; ------------------------- ;; Custom ;; ------------------------- (custom-set-variables ;; custom-set-variables was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. '(org-safe-remote-resources '("\\`/ssh:apps@5\\.189\\.176\\.18:/home/apps/portfolio/blog/org/thoughts\\.org\\'" "\\`/ssh:apps@cdatgoose\\.org:/home/apps/portfolio/blog/org/thoughts\\.org\\'" "\\`https://fniessen\\.github\\.io/org-html-themes/org/html-theme-readtheorg\\.setup\\'" "\\`https://fniessen\\.github\\.io/org-html-themes/org/html-theme-bigblow\\.setup\\'")) '(package-selected-packages '(avy cape corfu diff-hl dirvish embark-consult empv exwm gptel gruvbox-theme ibrowse magit marginalia nasm-mode naviel nerd-icons-completion orderless tidal typst-ts-mode vertico websocket xkbmap-switcher xkcd))) (custom-set-faces ;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. ) ;;; init.el ends here