#+title: Dots My literate system configuration * TODO Foreword * Emacs ** UI ~How Emacs looks!~ *** Theme Load the directory with additional, local themes This currently mostly loads the emacs port of the Evergarden theme (made by me!) at [[https://codeberg.org/roughlynamed/emacs-evergarden][roughlynamed/emacs-evergarden]], also the fortune-gold theme. The reason i use modus-vivendi instead of my evergarden port is because i dont like evergarden (dont tell the devs though!), seeing it not have an emacs port just made me sad. I also use tango from time-to-time, thats the comment. #+begin_src emacs-lisp (add-to-list 'custom-theme-load-path "~/.config/doom/extra-themes/") (setq doom-theme 'modus-vivendi) ;; (setq doom-theme 'tango) #+end_src By default, Emacs prompts 2 times when loading a theme, this is annoying so im getting rid of it. #+begin_src emacs-lisp (setq custom-safe-themes t) #+end_src *** Font I use the completely not pirated ~Berkeley Mono~ font. I really like it. Sometimes i will use the Commit mono font (sometimes!) #+begin_src emacs-lisp (setq doom-font (font-spec :family "Berkeley Mono" :size 12.0)) #+end_src Line numbers! And not relative! I dont like rln, i just dont #+begin_src emacs-lisp (setq display-line-numbers-type t) #+end_src *** Modeline mood-line is a nice & simple modeline for emacs! #+begin_src emacs-lisp (use-package! mood-line :config (mood-line-mode) :custom (mood-line-glyph-alist mood-line-glyphs-fira-code)) #+end_src *** Custom window management I didnt like how in emacs, split windows under ~SPC w s~ just stack, so i made this function to make it dynamic in a fibonnaci-like layout. #+begin_src emacs-lisp (defun my/split-window-dynamically () "Split the selected window, choosing orientation by its shape." (interactive) (select-window (if (> (window-total-width) (* 2 (window-total-height))) (split-window-right) (split-window-below)))) #+end_src Map the function to override ~SPC w s~ #+begin_src emacs-lisp (map! :leader "w s" #'my/split-window-dynamically) #+end_src *** UX ~ UI Scroll margin - when the cursor reaches 3 lines above/below the window bottom/top, the window goes down (this happens in helix automatically, but the margin is 5) #+begin_src emacs-lisp (setq scroll-margin 3) #+end_src *** Breadcrumb Breadcrumb is a package that shows you where you are in a file at the very top of the window. Editing this section of this file, the breadcrumb shows: ~stow/d/.config/doom/config.org : UI~, this is because we are in the UI section of the file. #+begin_src emacs-lisp (use-package! breadcrumb :defer t :hook (prog-mode . breadcrumb-mode) ;; make sure its in programming modes :config (breadcrumb-mode 1)) #+end_src *** Zen the ~zen~ option in Doom Emacs enables ~writeroom~ mode on toggle. ~writeroom~ centers the buffer, makes it bigger and lets you type away without distractions. By default, entering ~writeroom~ mode (which i will continue to call ~zen~ mode) changes the typeface in the buffer. I don't like that, especially when im editing source blocks (e.g here). Let's turn it off. #+begin_src emacs-lisp (setq +zen-mixed-pitch-modes '()) #+end_src ** Languages & LSP ~M-x vscode~ *** Haskell A custom function that runs the current Haskell file with runghc (interpreter instead of a compiler). #+begin_src emacs-lisp (defun my/hs-eval () (interactive) (let* ((src (buffer-file-name)) (out (file-name-sans-extension src))) (when (zerop (call-process-shell-command (format "runghc %s" src) nil "*runghc-output*")) (let ((buf (make-comint "hs-run" out))) (display-buffer buf))))) #+end_src Hook the function up up to f5 #+begin_src emacs-lisp (add-hook 'haskell-mode-hook (lambda () (local-set-key (kbd "") #'my/hs-eval))) #+end_src *** Eglot Eglot is, well, LSP for Emacs! Here i hook eglot-ensure into prog-mode. eglot-ensure makes sure that eglot is enabled and working when opening a prog-mode buffer now. #+begin_src emacs-lisp (use-package! eglot :hook (prog-mode . eglot-ensure)) #+end_src I copied this block from somewhere and have no idea what it does. #+begin_src emacs-lisp (after! eglot (cl-defmethod eglot-register-capability (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) nil) #+end_src Here we add a custom LSP for typescript and javascript mode. Oxlint is a rewrite of ESLint in Rust, Not sure why oxlsp isnt its own tool, but oh well. #+begin_src emacs-lisp (add-to-list 'eglot-server-programs '((typescript-ts-mode javascript-mode) . ("oxlint" "--lsp")))) #+end_src ~eglot-sync-connect~ makes it so that eglot has to catch up and connect before a buffer can be worked on, thats why i set it to nil. ~eglot-connect-timeout~ just stops trying to connect to eglot after a minute. #+begin_src emacs-lisp (setq eglot-sync-connect nil eglot-connect-timeout 60) #+end_src *** Flycheck Flycheck is a linter engine for Emacs. Here i define a new checker for flycheck, htmlhint (which is the linter i most enjoy using). Then i hook it into ~web-mode~ #+begin_src emacs-lisp (after! flycheck (flycheck-define-checker htmlhint "htmlhint for flycheck" :command ("htmlhint" "--format" "unix" source) :error-patterns ((error line-start (file-name) ":" line ":" column ": " (message) line-end)) :modes (web-mode)) #+end_src I then add the new checker to the flycheck checkers (say that 3 times fast) #+begin_src emacs-lisp (add-to-list 'flycheck-checkers 'htmlhint) #+end_src Here we add additional checkers to flycheck. We add ESLint and oxlint, and then another block for html-tidy and the new htmlhint. The reason this cant be all in one ~flycheck-add-next-checker~ block is because ~flyckech-add-next-checker~ only takes 2 variables. #+begin_src emacs-lisp (flycheck-add-next-checker 'javascript-eslint 'javascript-oxlint) (flycheck-add-next-checker 'html-tidy 'htmlhint) #+end_src We then make flycheck check the file after saving. #+begin_src emacs-lisp (setq flycheck-check-syntax-automatically '(save mode-enabled))) #+end_src *** Apheleia Apheleia is an easy way to format your files in Emacs. First we hook apheleia into prog-mode. #+begin_src emacs-lisp (use-package! apheleia :hook (prog-mode . apheleia-mode) #+end_src We can then add a new apheleia formatter, in this case oxfmt. (why is oxfmt its own tool but oxlsp isnt? why cant it just be ~oxlint --lsp~ and ~oxlint --format~, this is dumb) #+begin_src emacs-lisp :config (setf (alist-get 'oxfmt apheleia-formatters) '("oxfmt" "--config=.oxfmtrc.json" "--stdin-filepath" filepath)) #+end_src Here we hook the new oxfmt formatter into: + html-mode + mhtml-mode + web-mode + css-mode #+begin_src emacs-lisp (dolist (mode '(html-mode mhtml-mode web-mode css-mode)) (setf (alist-get mode apheleia-mode-alist) 'oxfmt))) #+end_src ** Email ~Emacs - prover of Zawinki's law~ *** Auth To send mail, we need to log into everything. Instead of typing out passwords every time, we use the ~~.authinfo.gpg~ file (can also be without the gpg encryption). To read the file: #+begin_src emacs-lisp (setq auth-sources '("~/.authinfo.gpg")) #+end_src Emacs automatically decrypts the file with ~epa~ and ~epg~. To make this not expire, we set #+begin_src emacs-lisp (setq auth-source-cache-expiry nil) #+end_src *** Gnus Gnus is Emacs's built in Email reader, sender, news viewer and RSS reader. But right now we're setting up the Email part. This makes it so that all subscribed groups are visible, no matter if theres no new messages. #+begin_src emacs-lisp (setq gnus-permanently-visible-groups ".") #+end_src When browsing gnus, make the mode in evil standard emacs. #+begin_src emacs-lisp (evil-set-initial-state 'gnus-browse-mode 'emacs) #+end_src Now we configure our user of gnus. #+begin_src emacs-lisp (setq user-mail-address "hi@roughlynamed.net" ; my email user-full-name "Magdalena Szweda") ; my name #+end_src While we're at it, lets point Emacs at my LibrePGP key. #+begin_src emacs-lisp (setq mml-secure-openpgp-signers '("3258B28C124AB0891D6C9E929B72909EEF00CBD0") mml-secure-openpgp-sign-with-sender t) #+end_src We can also set up the signature via the ~.signature~ file instead of encoding it raw into the config. #+begin_src emacs-lisp (setq message-signature-file "~/.signature") ; note no trailing slash #+end_src Now we can set up the actual mail part. I use migadu for my email hosting, provided to me by ~@bas.sh~ Here we set up IMAP for receiving mail. #+begin_src emacs-lisp (setq gnus-select-method '(nnimap "migadu" (nnimap-address "imap.migadu.com") (nnimap-server-port 993) (nnimap-stream ssl))) #+end_src Once that's done, we can configure the sending (smtp) part. #+begin_src emacs-lisp (setq smtpmail-smtp-server "smtp.migadu.com" smtpmail-smtp-service 465 ; port 465 smtpmail-stream-type 'ssl message-send-mail-function #'smtpmail-send-it ) #+end_src After that, a little bit more config for UX #+begin_src emacs-lisp (setq gnus-message-archive-method '(nnimap "migadu") ; use migadu for archiving email gnus-message-archive-group "Sent") ; where to put sent mail (setq gnus-large-newsgroup nil gnus-fetch-old-headers t nnmail-expiry-target "Trash") #+end_src By default, Gnus puts entries newest last, let's reverse it. #+begin_src emacs-lisp (setq gnus-thread-sort-functions '((not gnus-thread-sort-by-date))) (setq gnus-subthread-sort-functions '((not gnus-thread-sort-by-date))) (setq gnus-thread-sort-most-recent-first t) #+end_src What Gnus also does by default is ask to read the .newsrc file, this file is basically an auto-save, 90% of the time you want to read it. Let's just disable the prompt on every Gnus startup. #+begin_src emacs-lisp (setq gnus-always-read-dribble-file nil) #+end_src I like to sign my message with pgp as a way to confirm that its really me sending the email. We can do this with the following: #+begin_src emacs-lisp (add-hook 'message-send-hook #'mml-secure-message-sign-pgpmime) #+end_src We use pgpmime to put the signature in the MIME header instead of inline PGP blocks. I'm not a fan of html in my emails as i find it rendering bad and being hard to navigate, so i set it to discourage html and prefer plain text emails. #+begin_src emacs-lisp (with-eval-after-load "mm-decode" (add-to-list 'mm-discouraged-alternatives "text/html") (add-to-list 'mm-discouraged-alternatives "text/richtext")) #+end_src ** Jabber ~Jabber? I hardly know her!~ Excuse the bad pun, jabber.el is a way to use XMPP in Emacs. #+begin_src emacs-lisp (use-package! jabber :config #+end_src We point jabber at my XMPP account: #+begin_src emacs-lisp (setq jabber-account-list '(("roughlynamed@yax.im"))) #+end_src And let jabber use the modeline #+begin_src emacs-lisp (jabber-modeline-mode 1) #+end_src Now, we can hook jabber to kill itself when emacs dies. #+begin_src emacs-lisp :hook (kill-emacs . jabber-disconnect)) #+end_src As to not rely on ~M-x~ commands, define a binding as so #+begin_src emacs-lisp (map! :leader :prefix "j" :desc "Connect" "c" #'jabber-connect-all :desc "Disconnect" "q" #'jabber-disconnect :desc "Dash" "f" #'jabber-roster :desc "Reply" "r" #'jabber-chat-reply :desc "New chat" "n" #'jabber-chat-with :desc "Set status" "s" #'jabber-send-presence) #+end_src ** EMMS ~Music to my ears~ EMMS, the Emacs MultiMedia System (but mostly its just used for music). Now, emms has this issue where mpd and the emms cache fall out of sync. This is a documented issue so lets make them catch up by default. #+BEGIN_SRC emacs-lisp (defun emms-full-resync () "Update MPD's db, then pull it into EMMS's cache, then rebuild the browser." (interactive) (emms-player-mpd-update-all) (run-with-timer 2 nil (lambda () (emms-cache-set-from-mpd-all) (when (get-buffer "Browsing by: artist") (with-current-buffer "Browsing by: artist" (revert-buffer))))) (emms-browser)) #+END_SRC Declare the package block and set up the commands. ~:commands~ in elisp is used to eagerly load functions even when a package is lazy loaded. The following makes the commands ~emms~ and ~emms-browser~ available even before emms itself is loaded. #+begin_src emacs-lisp (use-package! emms :commands (emms emms-browser emms-player-mpd-update-all emms-cache-set-from-mpd-all) #+end_src Configure the mappings for EMMS ~:init~ in elisp runs the following code =before= a package is loaded. In this case, it makes the emms mappings available right when emacs starts up. #+begin_src emacs-lisp :init (map! :leader (:prefix ("e" . "emms") :desc "Browser" "b" #'emms-full-resync :desc "Playlist" "m" #'emms-playlist-mode-go :desc "Play/pause" "SPC" #'emms-pause :desc "Next" "n" #'emms-next :desc "Previous" "p" #'emms-previous :desc "Stop" "s" #'emms-stop :desc "Clear" "c" #'emms-playlist-clear)) #+end_src Here we tell EMMS to use the preset configuration loader, this makes it so that i dont have to wire up a bunch of emms submodules myself. #+begin_src emacs-lisp :config (require 'emms-setup) #+end_src We then use ~emms-all~ to load every emms submodule. Possible variations include: | Name | What it does | |---------------+----------------------------------------------------------------| | ~emms-all~ | Loads every EMMS submodule. (browser, cover art, lyrics, etc.) | |---------------+----------------------------------------------------------------| | ~emms-standard~ | Smaller and more curated subset of features | |---------------+----------------------------------------------------------------| | ~emms-minimal~ | Very barebones EMMS | #+begin_src emacs-lisp (emms-all) #+end_src Here we tell EMMS to use MPD instead of MPRIS. #+begin_src emacs-lisp (require 'emms-player-mpd) (setq emms-player-list '(emms-player-mpd) emms-info-functions '(emms-info-mpd) #+end_src Here we point EMMS/MPD at the default server and port for MPD #+begin_src emacs-lisp emms-player-mpd-server-name "localhost" emms-player-mpd-server-port "6600" #+end_src Tell EMMS where MPD's music dir lives, and where its own music dir lives. #+begin_src emacs-lisp emms-player-mpd-music-directory "~/Music" emms-source-file-default-directory (expand-file-name "~/Music/") #+end_src Make playlists loop instead of stopping at the last track. #+begin_src emacs-lisp emms-repeat-playlist t #+end_src Make EMMS use Doom Emacs' cache instead of its own. #+begin_src emacs-lisp emms-cache-file (concat doom-cache-dir "emms-cache")) #+end_src Clear the playlist both on EMMS and MPD #+begin_src emacs-lisp (add-hook 'emms-playlist-cleared-hook #'emms-player-mpd-clear) #+end_src Connect to MPD as soon as this config loads. #+begin_src emacs-lisp (emms-player-mpd-connect) #+end_src Turn on the little [ARTIST - ALBUM - TITLE] indicator in the modeline. #+begin_src emacs-lisp (emms-mode-line-mode 1) #+end_src Show lyrics in the buffer whenever its free #+begin_src emacs-lisp (emms-lyrics-toggle-display-on-minibuffer) #+end_src LISTENED/LENGTH indicator in the modeline #+begin_src emacs-lisp (emms-playing-time-mode 1) #+end_src This makes it so that presing RET on a song in normal mode adds the tracks to the playlist and plays it. This currently doesnt work. #+begin_src emacs-lisp (map! :map emms-browser-mode-map :n "RET" #'emms-browser-add-tracks-and-play)) #+end_src ** Git ~LEAVE Emacs? You gotta be kidding.~ *** Majutsu I use the JuJutsu version control system. I am an old user of ~magit~. Why not use magit for JJ? Define mappings for the already-installed ~majutsu~ Open the main majutsu buffer. #+begin_src emacs-lisp (map! :leader :prefix "g m" :desc "Status" "m" #'majutsu ; default majutsu buffer :desc "Describe" "d" #'majutsu-describe ; equivalent of 'jj describe' :desc "New" "n" #'majutsu-new ; new jj rev :desc "Push" "p" #'majutsu-git-push) ; push to git #+end_src ** Org ~Protesting for Org README support.~ Org is Emacs's version of markdown, and is what this file is written in. Tell org what directory to use #+begin_src emacs-lisp (setq org-directory "~/org/") #+end_src Now it's time to get to the guts; the main org config. But first, some good 'ol self-documenting code: #+begin_src emacs-lisp (after! org (setq org-startup-folded t ; fold headers on startup org-pretty-entities t org-hide-emphasis-markers t org-auto-align-tags nil org-tags-column 0 org-catch-invisible-edits 'show-and-error org-agenda-tags-column 0 org-capture-templates-contexts nil org-insert-heading-respect-content t org-special-ctrl-a/e t org-return-follows-link t #+end_src *** Org Babel Org babel is Org's version of code blocks. Every code block defined above and below is made via org-babel and the following config. #+begin_src emacs-lisp org-confirm-babel-evaluate nil) (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (python . t) (C . t) (lisp . t)))) #+end_src *** poly-org ~poly-org~ lets you use LSP and autocompletion in org code blocks without pressing ~C-c '~ By default, org lets you 'enter' code blocks with ~C-c '~ and have actual autocompletion/LSP features. But, typing that every block is annoying (especially with a literate config), that's why ~poly-org~ does the same thing* as ~C-c '~, but it happens when the cursor hovers over a block. #+begin_src emacs-lisp (use-package! poly-org :hook (org-mode . poly-org-mode)) #+end_src After that, we still need to add Eglot integration. #+begin_src emacs-lisp (add-hook 'org-src-mode-hook #'eglot-ensure) #+end_src *** Org UX This makes org capturing much better. When capturing a new org note, all windows are deleted, and the capture window is made a dedicated window. #+begin_src emacs-lisp (add-hook 'org-capture-mode-hook #'delete-other-windows) (add-hook 'org-capture-mode-hook (lambda () (set-window-dedicated-p (selected-window) nil))) #+end_src *** TODO Org roam As much as i'd love to put it under its own higher header, that would be nonsensical. Org roam is org's way of a Zettelkasten style knowledge place. Something like obsidian, logseq or ~zk~. Another plus is everything lives in a ~SQLite~ database. Point org-roam at the org directory (declared seperately). #+begin_src emacs-lisp (after! org-roam (setq org-roam-directory (file-truename "~/org/")) #+end_src Make roam sync it's database automatically. #+begin_src emacs-lisp (org-roam-db-autosync-mode) #+end_src This is a custom function, i made it to mimic how the ~zk~ cli works when making a new note, which is usually a random 4 character string. for example ~zqy2.md~ could be a note. #+begin_src emacs-lisp (defun my/org-roam-short-id () (let ((chars "abcdefghijklmnopqrstuvwxyz0123456789")) (apply #'string (cl-loop repeat 4 collect (aref chars (random (length chars))))))) #+end_src The template for new org notes. #+begin_src emacs-lisp (setq org-roam-capture-templates '(("d" "default" plain "* %?" :if-new #+end_src Here we use the function defined above to name the captured org file #+begin_src emacs-lisp (file+head (lambda () (concat (my/org-roam-short-id) ".org")) "#+TITLE: %^{title|Untitled}\n#+FILETAGS: :%^{tags}:\n\n") ; title&tags :unnarrowed t))) (cl-defmethod org-roam-node-file-stem ((node org-roam-node)) (file-name-base (org-roam-node-file node))) (setq org-roam-node-display-template (concat "${title:*} " (propertize "${file-stem}" 'face 'org-tag))) #+end_src Define sections in roam. #+begin_src emacs-lisp (setq org-roam-mode-sections '(org-roam-backlinks-section org-roam-reflinks-section org-roam-unlinked-references-section)) #+end_src **** Link checking Here we have custom functions, mimicking the ~zk~ cli. This functions check if an org file has an inserted link to itself. #+begin_src emacs-lisp (defun my/org-roam-check-self-link () (when-let* ((node (org-roam-node-at-point)) (id (org-roam-node-id node))) (save-excursion (goto-char (point-min)) (while (re-search-forward org-link-any-re nil t) (when (string-match-p (regexp-quote id) (match-string 0)) (message "Warning: self-link detected")))))) #+end_src Check links that dont point to an actual file #+begin_src emacs-lisp (defun my/org-roam-check-dead-links () (dolist (link (org-roam-db-query [:select [dest] :from links :where (= source $s1)] (org-roam-id-at-point))) (unless (org-roam-db-query [:select id :from nodes :where (= id $s1)] (car link)) (message "Error: dead link to %s" (car link))))) #+end_src **** Hooks Run the defined above functions before every org file save #+begin_src emacs-lisp (add-hook 'before-save-hook (lambda () (when (org-roam-file-p) (my/org-roam-check-self-link) (my/org-roam-check-dead-links))) nil t) #+end_src **** Maps Define mappings #+begin_src emacs-lisp (map! :leader :prefix ("n r" . "roam") :desc "Find node" "f" #'org-roam-node-find ; 'zk edit -i' equivalent :desc "Insert node" "i" #'org-roam-node-insert ; link a note :desc "Backlinks" "b" #'org-roam-buffer-toggle :desc "Capture" "c" #'org-roam-capture ; make a new note #+end_src The org graph requires Graphviz and the ~dot~ executable #+begin_src emacs-lisp :desc "Graph" "g" #'org-roam-graph ; graph of every note #+end_src #+begin_src emacs-lisp :desc "Random node" "r" #'org-roam-node-random ; random node :desc "Open link" "o" #'org-open-at-point ; open linked note under cursor :desc "Jump back" "O" #'org-mark-ring-goto)) ; jump back to previous note after SPC n r o #+end_src *** ox-html ~ox-html~ is a way to convert org files into html. #+begin_src emacs-lisp (with-eval-after-load 'ox-html (setq org-html-xml-declaration nil org-html-doctype "html5" org-html-html5-fancy t #+end_src Dont use ox's default html style #+begin_src emacs-lisp org-html-head-include-default-style nil #+end_src Dont postamble in generated html #+begin_src emacs-lisp org-html-postamble nil)) #+end_src *** org-roam-ui ~org-roam-ui~ makes a Obsidian-like graph available in the browser. It makes a pretty graph of all your notes and puts it at http://localhost:35901. Required websocket #+begin_src emacs-lisp (use-package! websocket :after org-roam) #+end_src org-roam config #+begin_src emacs-lisp (use-package! org-roam-ui :after org-roam :config (setq org-roam-ui-sync-theme t ; use emacs theme org-roam-ui-follow t org-roam-ui-update-on-save t ; update when a org roam file is saved )) #+end_src *** org-appear Org appear makes it so that inline code blocks (=~~=), words marked verbatim (====) and (hyper)link syntax hide when viewing the file and appear when hovered on for easy editing. for example =[[id:xqyz][emacs]]= will become =emacs= when viewing the file, but turn into the original when the cursor is over the text. #+begin_src emacs-lisp (use-package! org-appear :hook (org-mode . org-appear-mode) :config (setq org-appear-autoemphasis t org-appear-autolinks t org-appear-autosubmarkers t org-appear-autoentities t)) #+end_src *** org-modern-indent Because doom's org ~+pretty~ flag turns on org modern, and org-modern is incompatible with ~org-startup-indented~, we use the ~org-indent-modern~ package #+begin_src emacs-lisp (use-package! org-modern-indent :defer t :hook (org-mode . org-modern-indent-mode)) #+end_src ** Eww #+begin_quote Eww... What browser is that girl using? #+end_quote ~eww~ (Emacs Web WOWser) is, well, a web browser in Emacs. It only renders HTML and tries its best to render CSS, so javascript is out of the question (hooray!) ~eww~ is fine on its own, but i have a config to make the experience better. Here we define what to search with, for me this is Ecosia. #+begin_src emacs-lisp (setq eww-search-prefix "https://ecosia.org/search?q=") #+end_src Where eww downloads files to. #+begin_src emacs-lisp (setq eww-download-directory (expand-file-name "~/Downloads/")) #+end_src *** Functions Open a link in mpv #+begin_src emacs-lisp (defun my-browse-url-mpv (url &rest _args) "Open URL in mpv." (start-process "mpv" nil "mpv" url)) #+end_src Open a url in pdf-tools #+begin_src emacs-lisp (defun my-browse-url-pdf (url &rest _args) "Fetch remote PDF and open in pdf-tools within Emacs." (let ((tmp (make-temp-file "emacs-pdf-" nil ".pdf"))) (url-copy-file url tmp t) (find-file-other-window tmp) (pdf-view-mode))) #+end_src Download an image and put it into ~eww-download-directory~ #+begin_src emacs-lisp (defun my/eww-download-image-at-point () (interactive) (let ((url (or (get-text-property (point) 'image-url) (get-text-property (point) 'shr-url)))) (if (not url) (message "No image at point") (let* ((filename (file-name-nondirectory (url-filename (url-generic-parse-url url)))) (dest (expand-file-name filename eww-download-directory))) (url-copy-file url dest t) (message "Saved: %s" dest))))) #+end_src *** Config :: Misc Use the functions defined above when opening specific links #+begin_src emacs-lisp (setq browse-url-handlers '(("\\(youtube\\.com\\|youtu\\.be\\|vimeo\\.com\\|twitch\\.tv\\)" . my-browse-url-mpv) ("\\.mp4$" . my-browse-url-mpv) ("\\.pdf$" . my-browse-url-pdf) ("^gemini://" . elpher-browse-url-elpher) ("^gopher://" . elpher-browse-url-elpher) ("." . eww-browse-url))) #+end_src Open a url in a secondary program, in my case ~Vivaldi~ #+begin_src emacs-lisp (setq browse-url-secondary-browser-function 'browse-url-generic browse-url-generic-program "vivaldi") #+end_src *** UI/UX settings #+begin_src emacs-lisp (setq shr-width 100) (setq shr-max-width 120) (setq shr-indentation 4) (setq shr-use-fonts nil) (setq shr-max-image-size '(800 . 600)) (setq shr-image-animate t) #+end_src Let's make ~eww~ go into readable mode on page enter. #+begin_src emacs-lisp (add-hook 'eww-after-render-hook #'eww-readable) #+end_src *** Keybindings #+begin_src emacs-lisp (with-eval-after-load 'eww (define-key eww-mode-map (kbd "=") #'text-scale-increase) (define-key eww-mode-map (kbd "-") #'text-scale-decrease) (define-key eww-mode-map (kbd "0") #'text-scale-adjust) (define-key eww-mode-map (kbd "b") #'eww-back-url) (define-key eww-mode-map (kbd "a") #'eww-add-bookmark) (define-key eww-mode-map (kbd "U") #'shr-copy-url) (define-key eww-mode-map (kbd "D") #'my/eww-download-image-at-point) (evil-define-key 'normal eww-mode-map "b" #'eww-back-url "a" #'eww-add-bookmark "U" #'shr-copy-url "D" #'my/eww-download-image-at-point)) (provide 'browser) #+end_src ** Elpher #+begin_quote Elpher? I hardly know her! #+end_quote Elpher is a way to use the Gemini and Gopher protocols in Emacs. #+begin_src emacs-lisp (use-package! elpher) #+end_src There's not much config needed except 1 (one) binding #+begin_src emacs-lisp (map! :leader :prefix "o g" :desc "Elpher" "e" #'elpher :desc "Browse Elpher" "b" #'elpher-browse-url-elpher) #+end_src ** Packages #+BEGIN_QUOTE The ones that dont belong #+END_QUOTE These packages are (mostly) defined in ~packages.el~ and configured here. *** TODO mosey Mosey lets you ~Mosey down~ (get it?) to different points in a code block. #+begin_src emacs-lisp (use-package! mosey :commands (mosey-forward mosey-backward mosey-forward-bounce mosey-backward-bounce mosey-forward-cycle mosey-backward-cycle)) #+end_src TODO: make this work properly and not just go to the start/end of line **** Keybinds Keybinds for mosey, under K because m was taken by localleader bindings #+begin_src emacs-lisp (map! :leader (:prefix ("k" . "mosey") :desc "Mosey backward" "a" #'mosey-backward-bounce :desc "Mosey forward" "e" #'mosey-forward-bounce)) #+end_src *** Woman ~woman~ is a way to view man pages straight in Emacs, without shelling out to the actual ~man~ binary, it stands for ~without man~ Let's map this to a mnemonic binding, in this case ~Open Woman~ #+begin_src emacs-lisp (map! :leader :prefix "o" :desc "Woman" "w" #'woman) #+end_src *** Dired ~Dired~ (Directory editor) is a way to, well, edit directories (and files!) in Emacs. This is similar to ~oil.nvim~ These are only bindings as dired is pre-installed on Emacs. #+begin_src emacs-lisp (map! :leader :prefix "d" :desc "Dired" "d" #'dired :desc "Dired here" "h" #'dired-jump) #+end_src ~dired~ prompts for the directory to open first, while ~dired-jump~ opens dired in the current directory, pretty neat. *** eat ~eat~ (emulate a terminal) is a terminal emulator, in elisp, for emacs! It is not available as a doom emacs module, so we declare it manually. #+begin_src emacs-lisp (use-package! eat :defer t) #+end_src Then we add bindings for ~eat~ as to not have to run ~M-x~ everytime. #+begin_src emacs-lisp (map! :leader :prefix "o" :desc "Open Eat" "e" #'eat-other-window) ; dont steal the current window #+end_src ** Evil ~Mapping at its finest~ EVIl mode emulates VIM bindings in Emacs. Remove ~evil-snipe~ as it conflicts with other bindings #+begin_src emacs-lisp (remove-hook 'doom-first-input-hook #'evil-snipe-mode) #+end_src *** Lispy ~lispy~[fn:1] is a way to run vim motions in a S-exp aware way. As an example , running ~D~ on ~(message "Get rid of |everything" a b (+ 1 2))~, where ~|~ is the point would ruin the S-exp, but with ~lispy~, this turns into: ~(message "Get rid of ")~ Pretty neat, huh? #+begin_src emacs-lisp (use-package lispyville :init (general-add-hook '(emacs-lisp-mode-hook lisp-mode-hook) #'lispyville-mode) ;; lispy lisps :config (lispyville-set-key-theme '(operators c-w additional))) #+end_src *** Colemak The biggest block for last. This is a custom mapping i made for EVIl mode that takes the bindings from [[https://github.com/kalbasit/vim-colemak][kalbasit/vim-colemak]] and puts them into Emacs. I plan on packaging this to [[https://melpa.org][MELPA]] soon. Make ~o~ go right in org mode (previously ~inserted on newline~) #+begin_src emacs-lisp (after! evil-org (evil-define-key 'normal evil-org-mode-map "o" #'evil-forward-char)) #+end_src **** Main bindings #+begin_src emacs-lisp (evil-define-key '(normal visual operator) 'global "n" 'evil-backward-char "e" 'evil-next-line "i" 'evil-previous-line "o" 'evil-forward-char) #+end_src **** Motion #+begin_src emacs-lisp (evil-define-key '(normal visual operator) 'global "l" 'evil-backward-word-begin "L" 'evil-backward-WORD-begin "u" 'evil-forward-word-end "U" 'evil-forward-WORD-end "y" 'evil-forward-word-begin "Y" 'evil-forward-word-begin) (with-eval-after-load 'evil-ex (define-key evil-ex-completion-map (kbd "C-l") 'left-word) (define-key evil-ex-completion-map (kbd "C-y") 'right-word)) #+end_src **** Inserting #+begin_src emacs-lisp (evil-define-key 'normal 'global "s" 'evil-insert "S" 'evil-insert-line "t" 'evil-append "T" 'evil-append-line "h" 'evil-open-below "H" 'evil-open-above) #+end_src **** Changing #+begin_src emacs-lisp (evil-define-key '(normal visual) 'global "w" 'evil-change "W" 'evil-change-line) #+end_src **** Editing #+begin_src emacs-lisp (evil-define-key 'normal 'global "x" 'evil-delete-char "c" 'evil-yank "v" 'evil-paste-after "X" 'evil-delete-line "C" 'evil-yank-line "V" 'evil-paste-before) (evil-define-key 'visual 'global "x" 'evil-delete "c" 'evil-yank "v" 'evil-paste-after) #+end_src **** TODO Undo Doesnt work in org files #+begin_src emacs-lisp (evil-define-key 'normal 'global "z" 'evil-undo "Z" 'evil-redo) #+end_src **** Visual #+begin_src emacs-lisp (evil-define-key 'normal 'global "a" 'evil-visual-char "A" 'evil-visual-line "ga" 'evil-visual-restore) #+end_src **** Find/Search #+begin_src emacs-lisp (evil-define-key '(normal visual operator) 'global "p" 'evil-find-char-to "P" 'evil-find-char-to-backward "b" 'evil-repeat-find-char "B" 'evil-repeat-find-char-reverse "k" 'evil-search-next "K" 'evil-search-previous) #+end_src **** Inner change #+begin_src emacs-lisp (with-eval-after-load 'evil (evil-define-key '(operator visual) 'global "r" evil-inner-text-objects-map)) #+end_src **** Scroll #+begin_src emacs-lisp (define-prefix-command 'my-colemak-fold-map) (evil-define-key '(normal visual) 'global "j" 'my-colemak-fold-map) (define-key my-colemak-fold-map "j" 'evil-scroll-line-to-center) (define-key my-colemak-fold-map "n" 'evil-scroll-line-to-top) (define-key my-colemak-fold-map "e" 'evil-scroll-line-to-bottom) #+end_src **** Lookup #+begin_src emacs-lisp (evil-define-key 'normal 'global "gK" 'evil-lookup) #+end_src **** Window management #+begin_src emacs-lisp (map! :n "SPC w n" #'evil-window-left :n "SPC w e" #'evil-window-down :n "SPC w i" #'evil-window-up :n "SPC w o" #'evil-window-right) #+end_src ** Footnotes [fn:1] The actual vim motion part is provided by ~lispyville~, which adds evil support into