summaryrefslogtreecommitdiff
path: root/.emacs
diff options
context:
space:
mode:
authorDavid Moc <personal@cdatgoose.org>2026-05-24 11:48:07 +0200
committerDavid Moc <personal@cdatgoose.org>2026-05-24 11:48:07 +0200
commit5fb19f10389b70ee2389755106092a9ef6c64598 (patch)
tree969eb0466de581cb14aa09f751a95138620fc720 /.emacs
Go
Diffstat (limited to '.emacs')
-rwxr-xr-x.emacs647
1 files changed, 647 insertions, 0 deletions
diff --git a/.emacs b/.emacs
new file mode 100755
index 0000000..12a58c5
--- /dev/null
+++ b/.emacs
@@ -0,0 +1,647 @@
+;;; 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)
+ ("<escape>" . 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-<return>")
+ . (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-<tab>") . exwm-workspace-switch-next)
+ (,(kbd "s-S-<tab>") . 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 "<XF86AudioRaiseVolume>")
+ (lambda ()
+ (interactive)
+ (start-process-shell-command
+ "vol-up" nil "amixer set Master 5%+")))
+
+(global-set-key (kbd "<XF86AudioLowerVolume>")
+ (lambda ()
+ (interactive)
+ (start-process-shell-command
+ "vol-down" nil "amixer set Master 5%-")))
+
+(global-set-key (kbd "<XF86AudioMute>")
+ (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