#+PROPERTY: header-args :mkdirp yes :noweb yes #+PROPERTY: header-args:elisp :lexical t * Categories #+begin_src nix :tangle target/default.nix {lib, pkgs, config, ...}: let inherit (lib) mkIf mkMerge ; cfg = config.jfrikker; in { options.jfrikker = { enable = lib.mkEnableOption "Joe Frikker's configuration"; desktop.enable = lib.mkEnableOption "Desktop (GUI) applications"; redbook.enable = lib.mkEnableOption "Redbook-specific configuration"; <> }; config = mkIf cfg.enable (mkMerge [ <> ]); } #+end_src * Packages :PROPERTIES: :header-args:nix: :noweb-ref nix-configs :END: Home-manager "packages" are simply binaries that are placed on the path, without any attached configuration. The code blocks in this section contain package lists that go under the packages section above. ~SBCL~ is the current leading open-source common lisp compiler. It requires any libraries you want to be availble to be installed using withPackages. Many lisp projects I work on have this overridden in a ~nix~ devshell I think I'd also like to use it for scripting / exploratory development, though, so I'm installing it globally with a useful set of libraries. I should curate this somewhat carefully so it doesn't get carried away... #+BEGIN_SRC nix { home.packages = with pkgs; [ rlwrap (sbcl.withPackages (ps: with ps; [ alexandria cl-markup cl-mock clack clack-handler-woo dbi defstar dexador fare-csv group-by lack-request lack-response local-time log4cl mito myway prove prove-asdf replic serapeum shasht slynk smug split-sequence str sxql-composer trivia trivial-mmap trivial-ws trivial-open-browser trivial-timeout uuid ])) ]; } #+END_SRC [[https://github.com/str4d/rage][~rage~]] is a rust implementation of [[https://pkg.go.dev/filippo.io/age][~age~]]. It implements a simple system for generating public / private encryption keys, and encrypting / decrypting files with those keys. [[https://github.com/str4d/age-plugin-yubikey][~age-plugin-yubikey~]], as the name implies, allows generating and storing ~age~ keys on a Yubikey. ~rage~ is required over ~age~ when using ~age.el~, because it supports ~pinentry~. That allows it to prompt for yubikey pins using a dialog, rather than a command line prompt. #+BEGIN_SRC nix { home.packages = with pkgs; [ yubikey-manager ]; } #+END_SRC ~dust~, ~fd~, ~ripgrep~, and ~xh~ are rust re-implementations of a few longtime Linux command-line tools. They boost better speed, or simply more streamlined features. | Command | Original | | ~dust~ | ~du~ | | ~fd~ | ~find~ | | ~ripgrep~ | ~grep~ | | ~xh~ | ~curl~ | #+BEGIN_SRC nix { home.packages = with pkgs; [ dust fd ripgrep tokei xh ]; } #+END_SRC [[https://ledger-cli.org/][~ledger~]] is how I manage my budget. It's a plain-text accounting tool that's really awesome. #+BEGIN_SRC nix { home.packages = with pkgs; [ ledger ]; } #+END_SRC ~nil~ is an LSP server for nix files. #+BEGIN_SRC nix { home.packages = with pkgs; [ nil nixfmt ]; } #+END_SRC [[https://github.com/magic-wormhole/magic-wormhole][Magic Wormhole]] is a quick-and-easy way to securely transfer a file or directory between any two computers in the world, assuming that you have an out-of-band way to communicate a passphrase between them. Once installed, the actual command is ~wormhole~. #+begin_src nix { home.packages = with pkgs; [ magic-wormhole ]; } #+end_src These are the other programs I have installed globally. A few (like ~awscli~, ~mysql-client~, etc.) are still Redbook-specific; ideally I'd find another way to install them. #+BEGIN_SRC nix (mkIf cfg.redbook.enable { home.packages = with pkgs; [ openssh postgresql_15 ]; }) #+END_SRC These programs require a full desktop environment: #+BEGIN_SRC nix (mkIf cfg.desktop.enable { home.packages = with pkgs; [ google-chrome nerd-fonts.jetbrains-mono nerd-fonts.symbols-only (iosevka-bin.override { variant = "Aile"; }) (iosevka-bin.override { variant = "Etoile"; }) (iosevka-bin.override { variant = "SGr-IosevkaTermSS14"; }) nerd-fonts.symbols-only keymapp proton-pass wireshark ]; fonts.fontconfig.enable = true; }) #+END_SRC #+BEGIN_SRC nix (mkIf (cfg.redbook.enable && cfg.desktop.enable) { home.packages = with pkgs; [ linear notion-app teams ]; }) #+END_SRC ** Slack #+BEGIN_SRC nix (mkIf (cfg.redbook.enable && cfg.desktop.enable) { home.packages = with pkgs; [ (slack.overrideAttrs { version = "4.50.143"; src = pkgs.fetchurl { url = "https://downloads.slack-edge.com/desktop-releases/mac/arm64/4.50.143/Slack-4.50.143-macOS.dmg"; hash = "sha256-f5oYi8NPKXWpDcowFYMUjptbQs4V9hK2bm1TuaQeJn0="; }; }) ]; programs.emacs = { extraPackages = epkgs: with epkgs; [ ox-slack slack ]; extraConfig = builtins.readFile ./slack.el; }; }) #+END_SRC #+BEGIN_SRC elisp :tangle target/slack.el (use-package lui :defer t :custom (lui-time-stamp-format "[%Y-%m-%d %H:%M]")) (use-package slack :commands slack-start :custom (slack-render-profile-images-p nil) (slack-buffer-function #'switch-to-buffer)) #+END_SRC * Program Configurations :PROPERTIES: :header-args:nix: :noweb-ref nix-configs :END: This section contains applications whose config files will be generated by Home Manager. Home Manager calls these "programs" (as opposed to "packages", which are just binaries to put on the path). ** Simple Programs Although Home-Manager generates config files for these programs, they don't need their own dedicated configuration. I'm not sure what benefit there is to enabling ~jq~ as a Home Manager program, rather than just as an available package, but there doesn't seem to be a downside. #+BEGIN_SRC nix { programs = { bash.enable = true; gh.enable = true; home-manager.enable = true; jq.enable = true; lazygit.enable = true; zoxide.enable = true; }; } #+END_SRC ** Bat ~bat~ is a less alternative that performs syntax highlighting for a variety of languages. It also provides syntax-highlighting versions of a few other commands. It's honestly not incredibly useful, but still nice to have. #+BEGIN_SRC nix { programs.bat = { enable = true; config.style = "numbers"; }; } #+END_SRC ** Comma #+BEGIN_SRC nix { programs.nix-index.enable = true; programs.nix-index-database.comma.enable = true; } #+END_SRC ** Direnv ~direnv~ allows loading per-project paths as I navigate to their directories on the command line. I leverage this with the emacs ~envrc~ package. #+BEGIN_SRC nix { programs.direnv = { enable = true; nix-direnv.enable = true; }; } #+END_SRC ** Eza ~eza~ is an ~ls~ replacement that offers icons, colors, and some easier command-line options. #+BEGIN_SRC nix { programs.eza = { enable = true; enableBashIntegration = true; enableZshIntegration = true; icons = "auto"; }; } #+END_SRC ** Fzf ~fzf~ offers a variety of fuzzy finders right from the shell. I haven't been using this, so I'll turn it off for now. #+BEGIN_SRC nix { programs.fzf = { defaultCommand = "fd --type f"; fileWidgetCommand = "fd --type f"; fileWidgetOptions = ["--preview" "'bat --color=always --style=numbers --line-range=:500 {}'"]; changeDirWidgetCommand = "fd --type d"; tmux.enableShellIntegration = true; }; } #+END_SRC ** Git We all know what ~git~ does. #+BEGIN_SRC nix { programs.git = { enable = true; settings.user.name = "Joe Frikker"; settings.user.email = "joe@redbookcattle.com"; settings.github.user = "joe@redbookcattle.com"; settings.merge.conflictstyle = "zdiff3"; }; programs.delta = { enable = true; enableGitIntegration = true; }; } #+END_SRC ** Helix The best text editor ever? Heresy! #+BEGIN_SRC nix { programs.helix = { enable = true; defaultEditor = true; settings.editor = { auto-format = false; auto-pairs = false; auto-save.after-delay.enable = true; auto-save.after-delay.timeout = 1000; cursorline = true; idle-timeout = 0; rulers = [120]; text-width = 100; lsp.goto-reference-include-declaration = false; cursor-shape.insert = "bar"; smart-tab.supersede-menu = true; whitespace.render.newline = "all"; }; settings.keys.normal.space = { space = "file_picker"; "," = "buffer_picker"; # b = "@:sh wezterm cli spawn --cwd . -- tig blame %"; }; languages.language-server.eslint = { args = ["--stdio"]; command = "vscode-eslint-language-server"; config.nodePath = ""; config.quiet = false; config.rulesCustomizations = []; config.run = "onType"; config.validate = "on"; config.codeAction.disableRuleComment.enable = true; config.codeAction.disableRuleComment.location = "separateLine"; config.codeAction.showDocumentation.enable = true; config.problems.shortenToSingleLine = false; }; languages.language-server.jdtls.config.java = { autobuild.enabled = true; completion.maxResults = 1000; format.settings.url = "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml"; }; languages.language-server.rust-analyzer.config.files.watcher = "client"; languages.language = [ { name = "typescript"; language-servers = [ "typescript-language-server" "vscode-eslint-language-server" ]; indent.tab-width = 4; indent.unit = " "; } { name = "tsx"; language-servers = [ "typescript-language-server" "vscode-eslint-language-server" ]; indent.tab-width = 4; indent.unit = " "; } { name = "java"; indent.tab-width = 4; indent.unit = " "; } ]; }; } #+END_SRC ** Nh ~nh~ is an alternative frontend to home-manager and a few other nix tools that I'm trying out. It shows what's changing when you update your config, which is nice. #+BEGIN_SRC nix { programs.nh = { enable = true; }; } #+END_SRC ** Starship ~starship~ gives me a nice shell prompt, mostly showing the versions of relevant software I have installed. It's shell-agnostic, so it works with both bash and zsh. #+BEGIN_SRC nix { programs.starship = { enable = true; settings = { add_newline = false; container.disabled = true; git_branch.symbol = " "; git_status.ahead = "⇡\${count}"; git_status.behind = "⇣\${count}"; git_status.diverged = "⇕⇡\${ahead_count}⇣\${behind_count}"; git_status.format = "([\\[\$conflicted\$staged\$ahead_behind\\]](\$style) )"; java.symbol = " "; line_break.disabled = true; nix_shell.symbol = " "; nodejs.symbol = " "; package.disabled = true; palette = "catppuccin_frappe"; python.symbol = " "; package.symbol = " "; rust.symbol = " "; docker_context.disabled = true; shell.disabled = false; }; }; } #+END_SRC ** Tmux I'm using ~wezterm~ instead of ~tmux~ for now, so this is disabled, but I'm keeping the config here in case I need it later. #+BEGIN_SRC nix { programs.tmux = { baseIndex = 1; clock24 = true; customPaneNavigationAndResize = true; historyLimit = 50000; enable = true; escapeTime = 0; keyMode = "vi"; mouse = true; shell = pkgs.zsh + "/bin/zsh"; shortcut = "Space"; tmuxinator.enable = true; terminal = "xterm-256color"; extraConfig = '' bind-key -T copy-mode-vi 'v' send -X begin-selection bind-key -T copy-mode-vi 'y' send -X copy-selection-and-cancel bind-key -T launcher g popup -h 100% -w 100% -E lazygit bind-key g switch-client -Tlauncher set -g default-command "" ''; }; } #+END_SRC ** Wezterm :PROPERTIES: :header-args:lua: :noweb-ref wezterm-config :END: ~wezterm~ is a fast terminal emulator and multiplexer. It is configured using lua. We'll define its config in two blocks, so we can get lua syntax highlighting in org. First, the nix configuration: #+BEGIN_SRC nix (mkIf cfg.desktop.enable { programs.wezterm = { enable = true; extraConfig = '' local config = {} config.default_prog = { '${pkgs.zsh}/bin/zsh', '-l'} config.font_size = ${if pkgs.stdenv.isDarwin then "13.0" else "10.0"} '' + builtins.readFile ./wezterm.lua; }; }) #+END_SRC Then the real lua config: #+begin_src lua :tangle target/wezterm.lua :noweb-ref nil local config = {} <> return config #+end_src #+BEGIN_SRC lua config.default_prog = { '/Users/jfrikker/.nix-profile/bin/zsh', '-l' } config.font = wezterm.font 'Iosevka Term SS14' config.font_size = 13.0 -- config.font = wezterm.font("JetBrains Mono") config.inactive_pane_hsb = { saturation = 0.5, brightness = 0.5, } config.pane_focus_follows_mouse = true config.switch_to_last_active_tab_when_closing_tab = true config.tab_bar_at_bottom = true config.use_fancy_tab_bar = false config.window_decorations = "RESIZE" #+END_SRC I use a workspace switcher plugin, bouned to ~cmd-s~, that lets me quickly switch between per-project workspaces using a single OS window. #+BEGIN_SRC lua local workspace_switcher = wezterm.plugin.require("https://github.com/MLFlexer/smart_workspace_switcher.wezterm") workspace_switcher.zoxide_path='~/.nix-profile/bin/zoxide' #+END_SRC Set up a default window layout when switching to a workspace that hasn't been opened in this session yet. This will create three tabs, caled "edit", "run", and "term", and open helix in the first one. I use "edit" for running the editor (obviously), "run" for building / running the application, and "term" for miscellaneous other tasks. I sometimes split the "run" window into multiple panes if the project requires multiple things to run simultaneously. #+BEGIN_SRC lua wezterm.on('smart_workspace_switcher.workspace_switcher.created', function(window, path, workspace) local editor = window:active_tab() editor:set_title("edit") editor:panes()[1]:send_text("hx\n") local run = window:spawn_tab {} run:set_title "run" local term = window:spawn_tab {} term:set_title "term" editor:activate() end) #+END_SRC Put the current workspace name in the lower-right corner of the window. Taken from [https://alexplescan.com/posts/2024/08/10/wezterm/]. #+BEGIN_SRC lua local function segments_for_right_status(window) return { window:active_workspace(), } end wezterm.on('update-status', function(window, _) local SOLID_LEFT_ARROW = utf8.char(0xe0b2) local segments = segments_for_right_status(window) local color_scheme = window:effective_config().resolved_palette -- Note the use of wezterm.color.parse here, this returns -- a Color object, which comes with functionality for lightening -- or darkening the colour (amongst other things). local bg = wezterm.color.parse(color_scheme.background) local fg = color_scheme.foreground -- Each powerline segment is going to be coloured progressively -- darker/lighter depending on whether we're on a dark/light colour -- scheme. Let's establish the "from" and "to" bounds of our gradient. local gradient_to, gradient_from = bg gradient_from = gradient_to:lighten(0.2) -- Yes, WezTerm supports creating gradients, because why not?! Although -- they'd usually be used for setting high fidelity gradients on your terminal's -- background, we'll use them here to give us a sample of the powerline segment -- colours we need. local gradient = wezterm.color.gradient( { orientation = 'Horizontal', colors = { gradient_from, gradient_to }, }, #segments -- only gives us as many colours as we have segments. ) -- We'll build up the elements to send to wezterm.format in this table. local elements = {} for i, seg in ipairs(segments) do local is_first = i == 1 if is_first then table.insert(elements, { Background = { Color = 'none' } }) end table.insert(elements, { Foreground = { Color = gradient[i] } }) table.insert(elements, { Text = SOLID_LEFT_ARROW }) table.insert(elements, { Foreground = { Color = fg } }) table.insert(elements, { Background = { Color = gradient[i] } }) table.insert(elements, { Text = ' ' .. seg .. ' ' }) end window:set_right_status(wezterm.format(elements)) end) #+END_SRC Now we'll set up some keybindings: | Key | Action | | s-s | Switch Workspace | | s-- | Vertical Split | | s-\ | Horizontal split | | s-hjkl | Switch to the pane in that direction | | s-g | Launch ~lazygit~ in a new tab. The tab closes when ~lazygit~ exits. | | s-z | Toggle full-screen zoom for the active pane. | | s-t | Launch a terminal in a new tab. | | s-d | Close the current tab (with confirmation). | | s-123456789 | Switch to the tab with the corresponding number. | #+BEGIN_SRC lua config.leader = { key = ' ', mods = 'CTRL', timeout_milliseconds = 10000 } config.keys = { { key = "s", mods = "CMD", action = workspace_switcher.switch_workspace(), }, { key = "-", mods = "CMD", action = wezterm.action.SplitVertical {}, }, { key = "\\", mods = "CMD", action = wezterm.action.SplitHorizontal {}, }, { key = "h", mods = "CMD", action = wezterm.action.ActivatePaneDirection "Left", }, { key = "j", mods = "CMD", action = wezterm.action.ActivatePaneDirection "Down", }, { key = "k", mods = "CMD", action = wezterm.action.ActivatePaneDirection "Up", }, { key = "l", mods = "CMD", action = wezterm.action.ActivatePaneDirection "Right", }, { key = "g", mods = "CMD", action = wezterm.action.SpawnCommandInNewTab { args = { wezterm.home_dir .. "/.nix-profile/bin/zsh", "-l", "-i", "-c", "lazygit" } } }, { key = "t", mods = "CMD", action = wezterm.action.SpawnCommandInNewTab {} }, { key = 'z', mods = 'CMD', action = wezterm.action.TogglePaneZoomState, }, { key = 'd', mods = 'CMD', action = wezterm.action.CloseCurrentTab { confirm = true }, }, } for i = 1, 9 do -- CTRL+ALT + number to activate that tab table.insert(config.keys, { key = tostring(i), mods = 'LEADER', action = wezterm.action.ActivateTab(i - 1), }) end #+END_SRC ** Kitty #+BEGIN_SRC nix (mkIf cfg.desktop.enable { programs.kitty = { enable = true; font = { name = "Iosevka Term SS14"; package = pkgs.iosevka-bin.override { variant = "SGr-IosevkaTermSS14"; }; size = 13; }; themeFile = "Catppuccin-Frappe"; keybindings = { "ctrl+1" = "goto_tab 1"; "ctrl+2" = "goto_tab 2"; "ctrl+3" = "goto_tab 3"; "ctrl+4" = "goto_tab 4"; "ctrl+5" = "goto_tab 5"; "ctrl+6" = "goto_tab 6"; "ctrl+7" = "goto_tab 7"; "ctrl+8" = "goto_tab 8"; "ctrl+9" = "goto_tab 9"; }; }; }) #+END_SRC ** Ghostty Disabled until I can get it working on both linux and mac (if I want to). #+BEGIN_SRC nix :noweb-ref nil { programs.ghostty = { enable = false; package = pkgs.ghostty-bin; settings = { adjust-cell-height = "15%"; font-family = "Iosevka Term SS14"; font-size = 13; }; }; } #+END_SRC ** Zsh This is my default shell for now. Unfortunately, nix requires a posix-compliant shell, so bash or zsh it is. #+BEGIN_SRC nix { programs.zsh = { enable = true; oh-my-zsh.enable = true; syntaxHighlighting.enable = true; autosuggestion.enable = true; historySubstringSearch.enable = true; history.share = false; cdpath = ["~/source"]; }; } #+END_SRC Actually, nix *really* requires ~bash~. This is a plugin that allows using zsh from nix-shell. #+BEGIN_SRC nix { programs.zsh.plugins = [ { name = "zsh-nix-shell"; file = "nix-shell.plugin.zsh"; src = pkgs.fetchFromGitHub { owner = "chisui"; repo = "zsh-nix-shell"; rev = "v0.7.0"; sha256 = "149zh2rm59blr2q458a5irkfh82y3dwdich60s9670kl3cl5h2m1"; }; } ]; } #+END_SRC ** LLM :PROPERTIES: :header-args:elisp: :tangle target/emacs.el :END: #+BEGIN_SRC nix (let gptel = (epkgs: epkgs.melpaBuild { pname = "gptel"; version = "0.9.9.6"; src = pkgs.fetchFromGitHub { owner = "karthink"; repo = "gptel"; rev = "v0.9.9.6"; sha256 = "sha256-jurm3AQz+NiKZh7ssJkQYP73EmBEoyjzuC4phARYBak="; }; recipe = pkgs.writeText "gptel-recipe" '' (gptel :repo "karthink/gptel" :fetcher github) ''; packageRequires = [ epkgs.transient ]; }); in { programs.claude-code = { enable = true; }; programs.emacs.init.usePackage = { claude-code = { enable = true; package = (epkgs: epkgs.trivialBuild { pname = "claude-code"; version = "0.4.5"; src = pkgs.fetchFromGitHub { owner = "stevemolitor"; repo = "claude-code.el"; rev = "03199df8b3a1e9cd4857f0851f7a912ba524aff3"; sha256 = "sha256-5QJrWIu4EgnHcOqMwlrs2JBBx7aI9OaSJswesr6Apfk="; }; packageRequires = [ epkgs.inheritenv ]; }); bindKeyMap."C-c c" = "claude-code-command-map"; custom.claude-code-optimize-window-resize = false; }; gptel = { enable = true; package = gptel; command = ["gptel"]; bind."C-c g" = "gptel-menu"; custom = { gptel-default-mode = "'org-mode"; gptel-model = "'claude-sonnet-5"; gptel-org-branching-context = true; gptel-include-reasoning = false; }; config = '' <> ''; }; gptel-agent = { enable = true; package = (epkgs: epkgs.melpaBuild { pname = "gptel-agent"; version = "20260823"; src = pkgs.fetchFromGitHub { owner = "karthink"; repo = "gptel-agent"; rev = "326e0abb8c6db0e5c4c421bd9af495932ae94b62"; sha256 = "sha256-2eqY1MebzsDOpf3YEHy6o31TbpLA2iIPZMMVrscLYMk="; }; recipe = pkgs.writeText "gptel-agent-recipe" '' (gptel-agent :fetcher github :repo "karthink/gptel-agent" :files (:defaults "agents")) ''; packageRequires = [ (gptel epkgs) ]; }); after = ["gptel"]; config = '' <> ''; }; }; }) #+END_SRC #+BEGIN_SRC elisp :tangle no :noweb-ref emacs-gptel-config (setf (alist-get 'org-mode gptel-prompt-prefix-alist) "@user\n") (setf (alist-get 'org-mode gptel-response-prefix-alist) "@assistant\n") (gptel-make-tool :name "git_uncommitted_diff" :function (lambda () (magit-git-output "diff" "HEAD")) :description "Return a unified diff of all uncommitted changes (staged and unstaged) in the current git repository, relative to HEAD. Note: this does not include brand-new untracked files that have never been added to the index." :args nil :category "git") (gptel-make-tool :name "git_head_commit_diff" :function (lambda () (magit-git-output "show" "HEAD")) :description "Return the diff (including commit message) introduced by the HEAD commit in the current git repository, i.e. =git show HEAD=." :args nil :category "git") (gptel-make-tool :name "git_branch_diff" :function (lambda () (let* ((upstream (magit-get-upstream-branch)) (merge-base (and upstream (magit-git-string "merge-base" "HEAD" upstream)))) (if (not merge-base) "Could not determine upstream branch / merge base for the current branch." (magit-git-output "diff" merge-base)))) :description "Return a unified diff of all committed changes on the current branch relative to its upstream tracking branch's merge base. This shows everything this branch has added since it diverged from upstream." :args nil :category "git") #+END_SRC #+BEGIN_SRC elisp :tangle no :noweb-ref emacs-gptel-agent-config (require 'gptel-agent-tools-introspection) (gptel-agent-update) (gptel-make-preset 'coding :description "Preset for agentic coding" :backend "Claude" :model 'claude-sonnet-5 :use-tools t :max-tokens 128000 :tools '("Grep" "Read" "Glob" "Write" "Insert" "Edit" "Mkdir" "Diagnostics" "WebFetch" "WebSearch" "Bash" "git_uncommitted_diff" "git_head_commit_diff" "git_branch_diff")) (gptel-make-preset 'research :description "Preset for answering questions with web results" :backend "Claude" :model 'claude-sonnet-5 :use-tools t :max-tokens 128000 :confirm-tool-calls nil :tools '("WebFetch" "WebSearch")) (gptel-make-preset 'emacs-research :description "Preset for answering questions about the live emacs instance" :backend "Claude" :model 'claude-sonnet-5 :use-tools t :max-tokens 128000 :confirm-tool-calls nil :tools '("WebFetch" "WebSearch" "variable_completions" "command_completions" "function_completions" "variable_documentation" "function_documentation" "variable_value" "variable_source" "function_source" "symbol_manual_section" "library_source" "manual_node_contents" "manual_nodes" "manual_names" "features" "load_paths" "symbol_exists")) #+END_SRC ** Age :PROPERTIES: :header-args:elisp: :tangle target/emacs.el :END: #+BEGIN_SRC nix { home.sessionVariables = { PINENTRY_PROGRAM = "pinentry-mac"; }; home.packages = with pkgs; [ rage age-plugin-yubikey ]; programs.emacs = { extraPackages = epkgs: with epkgs; [ age ]; }; } #+END_SRC #+BEGIN_SRC elisp (use-package age :hook (after-init . age-file-enable) :custom (age-default-identity "~/source/dotfiles/keys/yubi1age.txt") (age-default-recipient "~/source/dotfiles/keys/myself.txt")) #+END_SRC * Emacs :PROPERTIES: :header-args:nix: :noweb-ref nix-configs :header-args:elisp: :tangle target/emacs.el :END: Emacs is a huge portion of this configuration, so although it's a home-manager "program", I'm breaking it out into its own top-level section. Many of the sections here have two code blocks: a nix code block listing the emacs packages to include, and an elisp code block to configure them in emacs. We'll put the actual emacs configs into separate files, rather than into nix string literals. This helps with a few places where nix's string interpolation was causing problems. #+BEGIN_SRC nix (mkIf cfg.desktop.enable { programs.emacs = { enable = true; init.enable = true; extraConfig = builtins.readFile ./emacs.el; }; }) #+END_SRC ** Keybindings These keybindings are scattered throughout this file, I'm collecting them all here to make it easier to track them. | Key | Normal Key | Action | Mode | | s-b | | [[id:6D1A8B35-3093-4DA6-A5AA-2A0D45112BAB][Switch project buffer]] | | | s-B | | [[id:6D1A8B35-3093-4DA6-A5AA-2A0D45112BAB][Switch buffer]] | | | s-f | C-x p f | [[id:A2F7FF5C-AFCC-44D4-9AA5-3498F6D6D5E0::workspace_keybinds][Open file in current project]] | | | s-F | C-x p p | [[id:A2F7FF5C-AFCC-44D4-9AA5-3498F6D6D5E0::workspace_keybinds][Switch project]] | | | S-s | C-x t RET | [[id:A2F7FF5C-AFCC-44D4-9AA5-3498F6D6D5E0::workspace_keybinds][Switch tab]] | | | C-c A | | [[id:3B0BFCDC-DFD5-42A0-97D8-3B032B128AF7][Org agenda]] | | | C-c a | | [[id:5CCB676B-9FF0-47B4-8AF2-10FD3899A646][Eglot code actions]] | prog | | C-c c | | Claude Code | | | C-c C | | [[id:3B0BFCDC-DFD5-42A0-97D8-3B032B128AF7][Org capture]] | | | C-c D | | [[id:5CCB676B-9FF0-47B4-8AF2-10FD3899A646][Show flymake diagnostics]] | prog | | C-c h | | [[id:6D1A8B35-3093-4DA6-A5AA-2A0D45112BAB][Consult shell history]] | | | C-c i | | [[id:5CCB676B-9FF0-47B4-8AF2-10FD3899A646][Eglot find implementation]] | prog | | C-c l | | [[id:3B0BFCDC-DFD5-42A0-97D8-3B032B128AF7][Org store link]] | | | C-c n | | [[id:3B0BFCDC-DFD5-42A0-97D8-3B032B128AF7][Org Roam prefix]] | | | C-c r | | [[id:5CCB676B-9FF0-47B4-8AF2-10FD3899A646][Eglot rename]] | prog | | C-c u | | [[id:5CCB676B-9FF0-47B4-8AF2-10FD3899A646][Cycle string inflection]] | various | | C-c M-g | | [[id:4F426E77-CE03-4BF8-8D1B-7791C7C02B28][Magit file dispatch]] | | ** Global Settings These are just some general settings that apply everywhere. First, a few packages: #+BEGIN_SRC nix { programs.emacs = { extraPackages = epkgs: with epkgs; [ doom-modeline free-keys ]; }; programs.emacs.init.usePackage.emacs.enable = true; } #+END_SRC #+begin_src elisp (use-package emacs :bind (:map minibuffer-local-map ("M-s" . consult-history) ("M-r" . consult-history))) #+end_src Turn off some default settings that I don't find useful. #+BEGIN_SRC elisp (setopt save-interprogram-paste-before-kill t) (setopt enable-recursive-minibuffers t) (setopt line-spacing 0.15) (setopt mouse-wheel-tilt-scroll t) (setq-default truncate-lines t) (setopt tab-bar-show nil) #+END_SRC Don't show the welcome screen, and keep the scratch buffer empty by default. These are not useful after you've started emacs more than a few times. #+begin_src elisp (setopt inhibit-startup-screen t) (setopt initial-scratch-message nil) #+end_src Turn off all the ugly graphical crap. #+begin_src elisp (menu-bar-mode -1) (scroll-bar-mode -1) (tool-bar-mode -1) #+end_src I don't find emacs's backup files useful, and they seem to litter my directories. I could maybe see a use if I wasn't using git. But, git is so easy to use, that I pretty much use it everywhere. I don't need a poor man's local history. Also, why do I have to set so many variables just to get emacs to stop putting extra files everywhere? #+begin_src elisp (setopt make-backup-files nil) (setopt create-lockfiles nil) (setopt auto-save-default nil) #+end_src Turn on ~view-mode~ automatically when the buffer is read-only. This just helps out with navigation a bit. I almost want to open files read-only by default, just so I can use ~view-mode~ (maybe with some other keybindings) all the time. But, I think one more invisible flag to manage is a bit much. #+begin_src elisp (setopt view-read-only t) #+end_src I don't really do anything with sentences. But, I also never put two spaces after a period. So, at least this setting is correct. #+begin_src elisp (setopt sentence-end-double-space nil) #+end_src I kill whole lines *way* more than partial lines. If the cursor is at the front of a line, this will just kill the whole line instead of requiring two ~C-k C-k~ presses to make it happen. I'm not sure when I'd ever want it to kill the line *without* the trailing newline, but I guess the one time ever that I do, I'll have to press two more keys :( #+begin_src elisp (setopt kill-whole-line t) #+end_src Show column numbers in the modeline. Now that I think about it, I'm not sure why I want this. I guess I could try turning it off someday? #+begin_src elisp (column-number-mode t) #+end_src Make sure which-key mode is always on. The on-the-fly keymap help it gives is indespensable. #+BEGIN_SRC elisp (which-key-mode) #+END_SRC Automatically refresh ~dired~ buffers when switching to them, instead of requiring an explicit refresh. #+BEGIN_SRC elisp (setopt dired-auto-revert-buffer t) #+END_SRC Similarly, highlighting the line the cursor is on is really useful for figuring out where you are, especially on monitors with a lot of vertical space. #+BEGIN_SRC elisp (add-hook 'after-init-hook 'global-hl-line-mode) #+END_SRC From the docs: "If ‘complete’, TAB first tries to indent the current line, and if the line was already indented, then try to complete the thing at point." Recommended by [[https://github.com/minad/corfu][the ~corfu~ readme]]. #+BEGIN_SRC nix { programs.emacs.init.usePackage.emacs.custom.tab-always-indent = "'complete"; } #+END_SRC Set the cursor to be a bar. I'm not sure what the benefit would be of a block cursor, since the current character isn't really "selected" like it would be in Helix. I switch this to a box when in god mode / read-only buffers, like vim would. #+BEGIN_SRC nix { programs.emacs.init.usePackage.emacs.config = '' (setq-default cursor-type 'bar) ''; } #+END_SRC Repeat-mode takes care of eliminating some annoying duplicate keypresses. I mostly use repeat-mode for two-character commands that need to be repeated; ~devil~ also has some single-key-with-modifier repeats set up that only apply in that mode. #+BEGIN_SRC nix { programs.emacs.init.usePackage.repeat = { enable = true; hook = ["after-init"]; }; } #+END_SRC Save the minibuffer history to disk, so recently-used options show up at the top of the picker across restarts. #+BEGIN_SRC nix { programs.emacs.init.usePackage.savehist = { enable = true; hook = ["after-init"]; }; } #+END_SRC Let's unbind some builtin keys that I don't use. I'm not sure if I'll replace them with anything or not, but at a minimum it'll avoid accidental keypresses and clean up which-key output. #+begin_src elisp :tangle no (require 'bind-key) (unbind-key "C-x RET") (unbind-key "C-x #") (unbind-key "C-x 5") (unbind-key "C-x 6") (unbind-key "C-x \\") (unbind-key "C-x `") (unbind-key "C-x m") (unbind-key "C-x l") (unbind-key "C-x i") (unbind-key "C-x u") (unbind-key "C-x @") (unbind-key "C-x C-b") (unbind-key "C-x C-d") (unbind-key "C-x C-l") (unbind-key "C-x C-p") (unbind-key "C-x C-u") (unbind-key "C-x C-z") #+end_src The doom modeline looks nice. I'm turning off IRC notifications, because it was slow to update (and it would update after every command, slowing down everything). I don't need the workspace name when I have the tab bar at the top of the window. The other settings just clear up some space, and make things look at little nicer. #+BEGIN_SRC nix { programs.emacs.init.usePackage.doom-modeline = { enable = true; hook = ["after-init"]; custom = { doom-modeline-irc = false; doom-modeline-workspace-name = false; doom-modeline-buffer-encoding = false; doom-modeline-buffer-file-name-style = "'relative-to-project"; }; config = '' (doom-modeline-remove-segment 'vcs 'main) ''; }; } #+END_SRC Ligature mode combines character sequences like <= into a single on-screen glyph. It looks kind of cool, so I'm turning it on in prog mode. The enabled ligatures are font-specific, taken from [[https://github.com/mickeynp/ligature.el/wiki#jetbrains-mono][the ligature wiki]]. #+BEGIN_SRC nix :noweb no { programs.emacs.init.usePackage.ligature = { enable = true; hook = ["(after-init . global-ligature-mode)"]; config = '' (ligature-set-ligatures 'prog-mode '("--" "---" "==" "===" "!=" "!==" "=!=" "=:=" "=/=" "<=" ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" "??" "???" "?:" "?." "?=" "<:" ":<" ":>" ">:" "<:<" "<>" "<<<" ">>>" "<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##" "###" "####" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#=" "^=" "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "" "/>" "" "->" "->>" "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" "==>" "=>" "=>>" ">=>" ">>=" ">>-" ">-" "-<" "-<<" ">->" "<-<" "<-|" "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>" "~>" "~-" "-~" "~@" "[||]" "|]" "[|" "|}" "{|" "[<" ">]" "|>" "<|" "||>" "<||" "|||>" "<|||" "<|>" "..." ".." ".=" "..<" ".?" "::" ":::" ":=" "::=" ":?" ":?>" "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__" "???" "<:<" ";;;")) ''; }; } #+END_SRC Use variable-width fonts in text buffers. ~mixed-pitch-mode~ keeps most code blocks as monospace. #+BEGIN_SRC nix { programs.emacs.init.usePackage.mixed-pitch = { enable = true; command = ["mixed-pitch-mode"]; hook = ["text-mode"]; init = '' (defun no-mixed-pitch () (mixed-pitch-mode -1)) (add-hook 'yaml-ts-mode-hook 'no-mixed-pitch) (add-hook 'mhtml-mode 'no-mixed-pitch) (add-hook 'nxml-mode 'no-mixed-pitch) ''; }; } #+END_SRC A basic ace-window setup. Allows switching between windows quickly (although avy can do the same...). #+BEGIN_SRC nix { programs.emacs.init.usePackage.ace-window = { enable = true; bind = { "M-o" = "ace-window"; "s-o" = "ace-window"; "C-c o" = "ace-window"; }; custom = { aw-keys = "'(?a ?s ?d ?f ?g ?h ?j ?k ?l)"; aw-dispatch-always = true; }; }; } #+END_SRC Set up a few project- / workspace-related key bindings. #+BEGIN_SRC nix { programs.emacs.init.usePackage = { project = { enable = true; bind = { "s-f" = "project-find-file"; "s-F" = "project-switch-project"; }; }; tab-bar = { enable = true; hook = ["after-init"]; bindLocal.tab-bar-mode-map."s-s" = "tab-bar-switch-to-tab"; }; }; } #+END_SRC All the beeping is annoying. This flashes a huge icon over the screen, which is still annoying, but less so? #+BEGIN_SRC nix { programs.emacs.init.usePackage.emacs.custom.visible-bell = true; } #+END_SRC I use tabs to manage multiple "workspaces". I don't tie a project to a tab or anything; often fixing a single bug might involve pulling in files from multiple projects. Rather, I open a file per bug / fix / issue, and leave it open until I'm done with that issue. ~tab-bar-history-mode~ allows quickly rolling back window changes; it can be used to undo a context change when drilling into a help page or something. I've set up repeat mode to make it easier to move backward / forward through history. #+BEGIN_SRC nix { programs.emacs.init.usePackage.tab-bar = { enable = true; hook = ["(after-init . tab-bar-history-mode)"]; config = '' (bind-keys :repeat-map tab-bar-repeat-map ("" . tab-bar-history-back) ("" . tab-bar-history-forward)) ''; }; } #+END_SRC Make compilation mode buffers auto-scroll to the first error: #+BEGIN_SRC nix { programs.emacs.init.usePackage.compile.enable = true; programs.emacs.init.usePackage.compile.custom.compilation-scroll-output = true; } #+END_SRC #+begin_src nix { programs.emacs.init.usePackage.register = { enable = true; defer = true; custom.register-preview-delay = 0.5; config = '' (advice-add 'register-preview :override #'consult-register-window) ''; }; } #+end_src *** Window Placement This section contains rules about where ~display-buffer~ should place windows. I guess I'm currently going for an IDE-style layout, with one main editor window, and auxiliary bottom and right side windows. I place windows in the bottom or on the right mostly depending on how wide I expect their content to be. #+begin_src nix { programs.emacs.init.usePackage = { window = { enable = true; custom = { split-width-threshold = 200; split-height-threshold = false; display-buffer-base-action = '''((display-buffer-reuse-window display-buffer-reuse-mode-window display-buffer-use-some-window))''; window-persistent-parameters = '''(cl-list* '(no-delete-other-windows . writable) '(no-other-window . writable) window-persistent-parameters)''; }; init = '' <> ''; config = '' <> ''; }; popper = { enable = true; hook = ["after-init" "(after-init . popper-echo-mode)"]; bind = { "C-q" = "popper-toggle"; "C-;" = "popper-cycle"; }; custom = { popper-display-control = false; popper-reference-buffers = ''(append jf/bottom-popups jf/right-popups)''; }; }; }; } #+end_src #+begin_src elisp :tangle no :noweb-ref emacs-window-init (defvar jf/bottom-popups '("\\*Flymake diagnostics .*" "\\*Embark Export: .*" "\\*Embark Collect .*" "\\*xref\\*" "\\*compilation\\*" "\\*pnpm:.*" "\\*sly-mrepl *" "\\*lispy-message\\*" "\\*occur\\*" ".*eshell\\*" "\\*SQL: .*")) (defvar jf/right-popups '("magit: *" "magit-log*" "\\*Messages\\*" "\\*trace-output\\*" "\\*EGLOT connections\\*" "magit-revision: .*" "\\*Help\\*" "\\*rustfmt\\*" "\\*slack: .*" "\\*forge: .*" "new-pullreq" "\\*claude:.*" "\\*Man .*" "\\*List .*" "\\*eldoc\\*")) #+end_src #+BEGIN_SRC elisp :tangle no :noweb-ref emacs-window-init (defun jf/window-children (window) (let ((result nil) (child (window-child window))) (while child (push child result) (setq child (window-next-sibling child))) (reverse result))) (defun jf/window-main-root (&optional frame) "Return FRAME's main (non-side) window or window subtree." (let* ((root (frame-root-window (or frame (selected-frame)))) (children (jf/window-children root))) (if (seq-some (lambda (w) (window-parameter w 'window-side)) children) (seq-find (lambda (w) (not (window-parameter w 'window-side))) children) root))) (defun jf/window-leaves (window) "Return the live leaf windows in WINDOW's subtree." (if (window-live-p window) (list window) (mapcan #'jf/window-leaves (jf/window-children window)))) (defun jf/topright-window (windows) "Return whichever of WINDOWS sits furthest up, then furthest right." (car (sort (copy-sequence windows) (lambda (a b) (let ((top-a (nth 1 (window-edges a))) (top-b (nth 1 (window-edges b)))) (if (= top-a top-b) (> (nth 0 (window-edges a)) (nth 0 (window-edges b))) (< top-a top-b))))))) (defun jf/display-buffer-right (buffer alist) "Display BUFFER on the right of the main editing area. Applies three rules, based on the shape of the frame's non-side windows: 1. One window -> split it and show BUFFER in the new right window. 2. Two windows side-by-side -> show BUFFER in the right one. If the right window is selected, swap the two windows first (keeping focus on the buffer that was active) so BUFFER doesn't evict it. 3. Any other layout -> show BUFFER in the upper-rightmost window." (let ((main (jf/window-main-root))) (cond ((window-live-p main) (let* ((new (split-window main nil 'right)) (width (max 1 (- (frame-width) 120))) (delta (window-resizable new (- width (window-width new)) t))) (window-resize new delta t) (window--display-buffer buffer new 'window alist))) ((and (window-left-child main) (= (length (jf/window-children main)) 2) (seq-every-p #'window-live-p (jf/window-children main))) (let* ((left (window-left-child main)) (right (window-next-sibling left))) (when (eq (selected-window) right) (window-swap-states left right) (select-window left)) (window--display-buffer buffer right 'reuse alist))) (t (window--display-buffer buffer (jf/topright-window (jf/window-leaves main)) 'reuse alist))))) #+END_SRC #+begin_src elisp :tangle no :noweb-ref emacs-window-config (setq display-buffer-alist `(((or ,@jf/bottom-popups) (display-buffer-in-side-window) (side . bottom) (slot . 0) (window-height . 0.33)) ((or ,@jf/right-popups) (display-buffer-reuse-window display-buffer-reuse-mode-window jf/display-buffer-right)))) #+end_src *** Tramp #+begin_src nix { programs.emacs.init.usePackage.tramp = { enable = true; package = (epkgs: epkgs.tramp.overrideAttrs { version = "2.8.2.3"; src = pkgs.fetchurl { url = "https://elpa.gnu.org/packages/tramp-2.8.2.3.tar"; sha256 = "sha256-8Rsn29+i7LDGDKFR1Q7HTml86MRmibN409D9QaGBci0="; }; }); config = '' <> ''; }; } #+end_src #+begin_src elisp :noweb no :noweb-ref emacs-tramp-config (with-eval-after-load 'compile (remove-hook 'compilation-mode-hook #'tramp-compile-disable-ssh-controlmaster-options)) #+end_src ** Editing This configuration affects general text editing, across all modes. I find Emacs to be almost unusable on a normal keyboard unless there's some way to avoid all the modifier keys. I tried Evil mode first. But although I love Vim, it just seemed to be hiding too much of the Emacs native functionality. The keybindings were also much harder to configure. I tried God mode, which has the benefit of re-using the normal Emacs keybindings and commands. It was annoying, though, because some read-only buffers (like magit buffers) have their own "normal-mode" commands that don't use modifiers. It felt jarring to switch to "insert mode" to use those keys. It's also hard to repeat sequences of M-f, M-b, etc. I could have made a ton of repeat maps, but I'm worried that having too many repeat maps will also be annoying (I'll have to explicitly cancel them more frequently). In the end, I opted for Devil mode. It avoids modifier keys, and has its own devil-mode-only repeat maps that don't interfere when using normal modifier key sequences. #+BEGIN_SRC nix { programs.emacs.init.usePackage.god-mode = { enable = true; bind."" = "(lambda () (interactive) (god-mode-all 1))"; bindLocal.god-local-mode-map = { i = "(lambda () (interactive) (god-mode-all -1))"; z = "repeat"; }; init = '' (defun my-god-mode-update-cursor-type () (setq cursor-type (cond ((and (featurep 'org-present) org-present-mode) nil) ((or (and (featurep 'god-mode) god-local-mode) buffer-read-only) 'box) (t 'bar)))) (add-hook 'post-command-hook 'my-god-mode-update-cursor-type) ''; config = '' (with-eval-after-load 'which-key (which-key-enable-god-mode-support)) ''; }; } #+END_SRC Avy allows jumping to any place on the screen with just a few keystrokes. I've stolen most of these ideas from Karthinks' article [[https://karthinks.com/software/avy-can-do-anything/][Avy can do Anything]]. I need to find more uses for this, in line with his article. #+BEGIN_SRC nix { programs.emacs.init.usePackage = { avy = { enable = true; functions = ["avy-action-oneshot"]; init = '' (defun avy-goto-char-timer-embark () (interactive) (let ((avy-action-oneshot (lambda (pt) (goto-char pt) (embark-act)))) (avy-goto-char-timer))) ''; bind = { "C-j" = "avy-goto-char-timer"; "M-j" = "avy-goto-char-timer-embark"; }; }; embark = { enable = true; bind."C-." = "embark-act"; }; lispy.bindLocal.lispy-mode-map."C-j" = "avy-goto-char-timer"; org.bindLocal.org-mode-map."C-j" = "avy-goto-char-timer"; }; } #+END_SRC Just "silently" wrap when searching at the bottom of a document. #+BEGIN_SRC nix { programs.emacs.init.usePackage.isearch = { enable = true; custom.isearch-wrap-pause = "'no-ding"; }; } #+END_SRC #+BEGIN_SRC nix { programs.emacs.init.usePackage.edit-indirect = { enable = true; bind."C-x 4 n" = "edit-indirect-region"; }; } #+END_SRC Some auto-formatting stuff #+begin_src nix { programs.emacs.init.usePackage.apheleia = { enable = true; config = '' (push '(prisma-format "prisma" "format" "--schema" filepath) apheleia-formatters) ''; }; } #+end_src ** Workflow #+BEGIN_SRC nix { programs.emacs.init.usePackage.buffer-terminator = { enable = true; hook = ["after-init"]; }; } #+END_SRC ** Language Support #+BEGIN_SRC nix { programs.emacs = { extraPackages = epkgs: with epkgs; [ (treesit-grammars.with-grammars (g: [ g.tree-sitter-bash g.tree-sitter-commonlisp g.tree-sitter-css g.tree-sitter-dockerfile g.tree-sitter-dot g.tree-sitter-elisp g.tree-sitter-graphql g.tree-sitter-gitcommit g.tree-sitter-gitignore g.tree-sitter-haskell g.tree-sitter-html g.tree-sitter-java g.tree-sitter-javascript g.tree-sitter-jq g.tree-sitter-json g.tree-sitter-json5 g.tree-sitter-just g.tree-sitter-ledger g.tree-sitter-llvm g.tree-sitter-make g.tree-sitter-nix g.tree-sitter-org g.tree-sitter-prisma g.tree-sitter-python g.tree-sitter-rust g.tree-sitter-rust-format-args g.tree-sitter-sql g.tree-sitter-toml g.tree-sitter-tsx g.tree-sitter-typescript g.tree-sitter-xml g.tree-sitter-yaml ])) ]; }; } #+END_SRC #+BEGIN_SRC nix { programs.emacs.init.usePackage = { electric-pair = { enable = true; hook = [ "after-init" ]; }; simple = { enable = true; config = '' (setq-default indent-tabs-mode nil) (setq-default tab-width 4) ''; }; ediff-wind = { enable = true; custom = { ediff-split-window-function = "'split-window-horizontally"; }; }; }; } #+END_SRC Show a vertical bar 100 characters out; ideally I don't want lines longer than this. 100 characters is about the max you can have and still fit two files side-by-side on the screen. #+begin_src nix { programs.emacs.init.usePackage.emacs = { enable = true; config = ''(setq-default fill-column 120)''; }; programs.emacs.init.usePackage.display-fill-column-indicator = { enable = true; hook = ["prog-mode"]; }; } #+end_src I had some weird problems with jsx code where the default M-q would just do a text fill rather than a re-indent. The solution seems to be to only do the ~syntax-ppss~ stuff if tree-sitter isn't available? It seems to work for now anyway... #+begin_src nix { programs.emacs.init.usePackage.prog-mode = { enable = true; defer = true; config = '' <> ''; }; } #+end_src #+begin_src elisp :tangle no :noweb-ref emacs-prog-mode-config (defun my-prog-fill-reindent-defun (&optional argument) "Refill or reindent the paragraph or defun that contains point. If the point is in a string or a comment, fill the paragraph that contains point or follows point. Otherwise, reindent the function definition that contains point or follows point." (interactive "P") (save-excursion (let ((treesit-text-node (and (treesit-available-p) (treesit-parser-list) (treesit-node-match-p (treesit-node-at (point)) 'text t)))) (if (or treesit-text-node (and (not (and (treesit-available-p) (treesit-parser-list))) (nth 8 (syntax-ppss))) (re-search-forward "\\s-*\\s<" (line-end-position) t)) (fill-paragraph argument (region-active-p)) (beginning-of-defun) (let ((start (point))) (end-of-defun) (indent-region start (point) nil)))))) (advice-add 'prog-fill-reindent-defun :override #'my-prog-fill-reindent-defun) #+end_src #+begin_src nix { programs.emacs.init.usePackage.ansi-color = { enable = true; defer = true; hook = ["(compilation-filter . ansi-color-compilation-filter)"]; }; } #+end_src *** Flymake #+begin_src nix { programs.emacs.init.usePackage.flymake = { enable = true; command = ["flymake-show-project-diagnostics" "flymake-goto-next-error" "flymake-goto-prev-error"]; bindLocal.flymake-project-diagnostics-mode-map = { "q" = "quit-window"; }; }; programs.emacs.init.usePackage.prog-mode = { enable = true; bindLocal.prog-mode-map = { "C-c D" = "flymake-show-project-diagnostics"; "M-n" = "flymake-goto-next-error"; "M-p" = "flymake-goto-prev-error"; }; }; } #+end_src *** Eglot #+begin_src nix { programs.emacs.init.usePackage.eglot.enable = true; } #+end_src I don't like UI elements that insert fake text into the file. They mess up the spacing in the file, and can make everything too wide. #+begin_src nix { programs.emacs.init.usePackage.eglot = { custom = { eglot-ignored-server-capabilities = "'(:inlayHintProvider :semanticTokensProvider)"; eglot-code-action-indications = false; eglot-events-buffer-config = "'(:size 300000 :format full)"; }; config = "(add-hook 'eglot-managed-mode-hook (lambda () (eglot-inlay-hints-mode -1)) nil t)"; }; } #+end_src Set up a few useful eglot keys. #+begin_src nix { programs.emacs.init.usePackage.eglot.bindLocal.eglot-mode-map = { "C-c a" = "eglot-code-actions"; "C-c r" = "eglot-rename"; "C-c i" = "eglot-find-implementation"; }; } #+end_src Eglot's yasnippet support uses the tab key, but I *also* use the tab key both for indentation, and for completion. That makes it really annoying to use; don't use yasnippet with eglot. #+begin_src nix { programs.emacs.init.usePackage.eglot.config = "(add-to-list 'eglot-stay-out-of 'yasnippet)"; } #+end_src *** Rust #+begin_src nix { programs.emacs.init.usePackage.rust-mode = { enable = true; mode = ["\"\\\\.rs\\\\'\""]; custom.rust-mode-treesitter-derive = true; }; } #+end_src *** YAML #+begin_src nix { programs.emacs.init.usePackage.yaml-pro = { enable = true; init = '' <> ''; hook = ["yaml-ts-mode"]; }; } #+end_src #+BEGIN_SRC elisp :tangle no :noweb-ref emacs-yaml-pro-init (defun yaml-validate () (interactive) (compile (concat "yamllint -d '{extends: default, rules: {indentation: {indent-sequences: false}, braces: {max-spaces-inside: 1}}}' " (buffer-file-name)) t)) #+END_SRC *** Lisp #+begin_src nix { programs.emacs.init.usePackage.inf-lisp = { enable = true; custom.inferior-lisp-program = "\"sbcl\""; }; programs.emacs.init.usePackage.sly = { enable = true; command = ["sly"]; }; programs.emacs.init.usePackage.sly-macrostep = { enable = true; after = ["sly"]; }; programs.emacs.init.usePackage.lispy = { enable = true; package = (epkgs: with epkgs; lispy.overrideAttrs { packageRequires = [ ace-window iedit swiper zoutline hydra ]; ignoreCompilationError = true; }); hook = ["emacs-lisp-mode-hook" "lisp-mode-hook" "lisp-data-mode-hook"]; bindLocal.lispy-mode-map = { i = "(lambda () (interactive) (if (and (featurep 'god-mode) god-global-mode) (god-mode-all -1) (special-lispy-tab)))"; }; custom.lispy-compat = "'(god-mode edebug)"; }; } #+end_src *** Typescript #+BEGIN_SRC nix { programs.emacs.init.usePackage = { typescript-ts-mode = { enable = true; mode = ["\"\\\\.ts\\\\'\"" "(\"\\\\.tsx\\\\'\" . tsx-ts-mode)"]; bindLocal = { typescript-ts-mode-map = { "C-c '" = "my/edit-indirect-tagged-template-at-point"; }; tsx-ts-mode-map = { "C-c '" = "my/edit-indirect-tagged-template-at-point"; }; }; custom.typescript-ts-mode-indent-offset = 2; config = '' <> ''; }; prisma-ts-mode = { enable = true; mode = ["\"\\\\.prisma\\\\'\""]; }; eglot = { config = ''(add-to-list 'eglot-server-programs '((prisma-ts-mode) "prisma-language-server" "--stdio"))''; }; pnpm-mode = { enable = true; command = ["pnpm-mode-pnpm-run"]; }; graphql-ts-mode = { enable = true; command = ["graphql-ts-mode"]; }; jq-mode = { enable = true; mode = ["\"\\\\.jq\\\\'\""]; interpreter = ["jq"]; }; }; } #+END_SRC #+BEGIN_SRC elisp :tangle no :noweb-ref emacs-typescript-ts-mode-config (defun my-tsx-ts-mode--indent-compatibility-b893426 () "Indent rules helper, to handle different releases of tree-sitter-tsx. Check if a node type is available, then return the right indent rules." ;; handle https://github.com/tree-sitter/tree-sitter-typescript/commit/b893426b82492e59388a326b824a346d829487e8 (condition-case nil (progn (treesit-query-capture 'tsx '((jsx_fragment) @capture)) `(((match "<" "jsx_fragment") parent 0) ((parent-is "jsx_fragment") parent typescript-ts-mode-indent-offset))) (treesit-query-error `(((match "<" "jsx_text") parent 0) ((parent-is "jsx_text") grand-parent typescript-ts-mode-indent-offset))))) (advice-add 'tsx-ts-mode--indent-compatibility-b893426 :override #'my-tsx-ts-mode--indent-compatibility-b893426) (defvar my/edit-indirect-tag-modes '(("graphql" . graphql-ts-mode) ("gql" . graphql-ts-mode) ("sql" . sql-mode) ("css" . css-mode)) "Map of tagged-template tag names to major modes.") (defun my/edit-indirect--node-bounds (node) "Return (BEG . END) for NODE's contents, excluding delimiters." (cons (1+ (treesit-node-start node)) (1- (treesit-node-end node)))) (defun my/edit-indirect-tagged-template-at-point () (interactive) (let* ((node (treesit-node-at (point))) (tmpl-node (treesit-parent-until node (lambda (n) (equal (treesit-node-type n) "template_string")))) (str-node (unless tmpl-node (treesit-parent-until node (lambda (n) (equal (treesit-node-type n) "string"))))) (target-node (or tmpl-node str-node))) (unless target-node (user-error "Point is not inside a string or template literal")) (let* ((tagged-node (and tmpl-node (treesit-node-parent tmpl-node))) (tag-node (and tagged-node (equal (treesit-node-type tagged-node) "call_expression") (treesit-node-child-by-field-name tagged-node "function"))) (tag-name (and tag-node (treesit-node-text tag-node t))) (mode (cond (tag-name (or (cdr (assoc tag-name my/edit-indirect-tag-modes)) major-mode)) (t 'text-mode))) (bounds (my/edit-indirect--node-bounds target-node)) (buf (edit-indirect-region (car bounds) (cdr bounds) t))) (with-current-buffer buf (funcall mode))))) #+END_SRC *** Java #+BEGIN_SRC nix { programs.emacs.init.usePackage = { java-ts-mode = { enable = true; init = ''(push '(java-mode . java-ts-mode) major-mode-remap-alist)''; config = '' <> ''; }; }; } #+END_SRC #+BEGIN_SRC elisp :tangle no :noweb-ref emacs-java-ts-mode-config (push `((n-p-gp nil "block" "lambda_expression") parent-bol java-ts-mode-indent-offset) (cdar java-ts-mode--indent-rules)) (push `((n-p-gp "}" "block" "lambda_expression") parent-bol 0) (cdar java-ts-mode--indent-rules)) (add-hook 'java-mode-hook (lambda () (c-set-offset 'arglist-intro '+))) (defvar jf/class-name-to-file-search-path (list "src/main/java" "src/test/java")) (defun jf/class-name-to-file (class-name) (let ((root (project-root (project-current t))) (fragment (format "%s.java" (replace-regexp-in-string "\\." "/" class-name))) (result)) (dolist (p jf/class-name-to-file-search-path result) (unless result (let ((path (format "%s/%s" p fragment))) (when (file-exists-p (concat root path)) (setq result path))))))) (defun jf/compile-class-to-file () (jf/class-name-to-file (concat (match-string 1) (match-string 2)))) ;(add-to-list 'compilation-error-regexp-alist-alist ; '(java-stack-trace . ; ("^[[:space:]]*at \\(\\(?:[[:lower:]]+\\.\\)+\\)[^(]+(\\([[:alnum:]]+\\)\\.java:\\([[:digit:]]+\\))" ; jf/compile-class-to-file 3))) ;(add-to-list 'compilation-error-regexp-alist 'java-stack-trace) #+END_SRC *** Nix #+begin_src nix { programs.emacs.init.usePackage = { nix-mode = { enable = true; mode = ["\"\\\\.nix\\\\'\""]; }; nix-flake = { enable = true; init = '' (defun project-nix-flake () (interactive) (nix-flake (project-root (project-current t)))) (bind-keys ("C-x p n" . project-nix-flake)) ''; command = [ "nix-flake" ]; }; }; } #+end_src xs *** SQL #+BEGIN_SRC nix { programs.emacs.init.usePackage.sql-indent = { enable = true; hook = ["(sql-mode . sqlind-minor-mode)"]; }; } #+END_SRC *** Haskell #+BEGIN_SRC nix { programs.emacs.init.usePackage.haskell-ts-mode = { enable = true; mode = ["\"\\\\.hs\\\\'\""]; custom.haskell-ts-use-indent = true; }; } #+END_SRC *** Lua #+BEGIN_SRC nix { programs.emacs.init.usePackage.lua-mode = { enable = true; mode = ["\"\\\\.lua\\\\'\""]; }; } #+END_SRC ** Envrc #+BEGIN_SRC nix { programs.emacs.init.usePackage.envrc = { enable = true; hook = ["(after-init . envrc-global-mode)"]; custom.envrc-show-summary-in-minibuffer = false; }; } #+END_SRC ** Magit #+BEGIN_SRC nix { programs.emacs.init.usePackage = { magit = { enable = true; bind = { "s-g" = "magit-status"; "C-x g" = "magit-status"; "C-c f" = "magit-status"; }; custom = { magit-save-repository-buffers = "'dontask"; magit-commit-show-diff = false; }; config = '' (set-face-attribute 'git-commit-summary nil :inherit nil) (defun magit-fetch-all-on-load () (magit-run-git-async "fetch" "--all")) (add-hook 'magit-status-mode-hook 'magit-fetch-all-on-load) (remove-hook 'magit-status-headers-hook 'magit-insert-tags-header) ''; }; forge = { enable = true; after = ["magit"]; config = '' (add-hook 'forge-edit-post-hook 'forge-create-pullreq-insert-single-commit-message) (setopt forge-status-buffer-default-topic-filters (forge--topics-spec :type 'topic :active nil :state 'open :order 'newest)) ''; }; diff-hl = { enable = true; hook = [ "(after-init . global-diff-hl-mode)" "(magit-post-refresh . diff-hl-magit-post-refresh)" ]; }; consult-gh = { enable = true; command = ["consult-gh"]; }; git-timemachine = { enable = true; command = ["git-timemachine"]; }; }; } #+END_SRC ** Vertico #+BEGIN_SRC nix { programs.emacs.init.usePackage = { consult = { enable = true; bind = { "s-b" = "consult-project-buffer"; "C-c h" = "consult-history"; "C-x b" = "consult-buffer"; "s-B" = "consult-buffer"; "C-x 4 b" = "consult-buffer-other-window"; "C-x 5 b" = "consult-buffer-other-frame"; "C-x r b" = "consult-bookmark"; "C-x p b" = "consult-project-buffer"; "M-#" = "consult-register-load"; "M-'" = "consult-register-store"; "C-M-#" = "consult-register"; "M-y" = "consult-yank-pop"; "M-g e" = "consult-compile-error"; "M-g f" = "consult-flymake"; "M-g g" = "consult-goto-line"; "M-g M-g" = "consult-goto-line"; "M-g o" = "consult-org-heading"; "M-g m" = "consult-mark"; "M-g k" = "consult-global-mark"; "M-g i" = "consult-imenu"; "M-g I" = "consult-imenu-multi"; "M-s r" = "consult-ripgrep"; "M-s l" = "consult-line"; "M-s L" = "consult-line-multi"; "M-s e" = "consult-isearch-history"; }; custom = { consult-narrow-key = ''"<"''; }; config = '' (consult-customize consult-theme :preview-key '(:debounce 0.2 any) consult-ripgrep consult-git-grep consult-grep consult-man consult-bookmark consult-recent-file consult-xref consult-source-bookmark consult-source-file-register consult-source-recent-file consult-source-buffer ;; :preview-key "M-." consult-source-project-recent-file :preview-key '(:debounce 0.4 any)) ''; }; consult-eglot.enable = true; isearch = { enable = true; bindLocal.isearch-mode-map = { "M-e" = "consult-isearch-history"; "M-s e" = "consult-isearch-history"; "M-s l" = "consult-line"; "M-s L" = "consult-line-multi"; }; }; xref = { enable = true; defer = true; custom = { xref-show-xrefs-function = "'consult-xref"; xref-show-definitions-function = "'consult-xref"; }; }; embark-consult = { enable = true; defer = true; }; orderless = { enable = true; defer = true; }; vertico = { enable = true; hook = ["after-init"]; custom = { vertico-count = 25; }; }; minibuffer = { enable = true; custom = { completion-styles = "'(orderless basic)"; }; }; marginalia = { enable = true; hook = ["after-init"]; }; corfu = { enable = true; hook = ["(after-init . global-corfu-mode)"]; custom = { corfu-cycle = true; corfu-popupinfo-mode = false; corfu-quit-at-boundary = false; corfu-on-exact-match = "'show"; }; }; cape = { enable = true; defer = true; init = "(add-hook 'completion-at-point-functions 'cape-file)"; }; }; } #+END_SRC ** Org #+BEGIN_SRC nix { programs.emacs.init.usePackage = { org = { enable = true; bind = { "C-c A" = "org-agenda"; "C-c C" = "org-capture"; "C-c l" = "org-id-store-link"; }; custom = { org-babel-load-languages = "'((emacs-lisp . t) (sql . t))"; org-confirm-babel-evaluate = false; org-link-keep-stored-after-insertion = true; org-capture-templates = '' '(("t" "Todo" entry (here) "** TODO %?\n:LOGBOOK:\n- State \"TODO\" %U\n:END:\n") ("w" "Work stuff") ("wp" "Work Project" entry (file+headline "~/org/todo/work.org.age" "Next Actions") "*** PROJ %?\n- State \"PROJ\" %U\n") ("wa" "Work Next Action" entry (file+headline "~/org/todo/work.org.age" "Next Actions") "*** TODO %?\n- State \"TODO\" %U\n") ("p" "Personal stuff") ("pp" "Personal Project" entry (file+headline "~/org/todo/personal.org.age" "Next Actions") "*** PROJ %?\n- State \"PROJ\" %U\n") ("pa" "Personal Next Action" entry (file+headline "~/org/todo/personal.org.age" "Next Actions") "*** TODO %?\n- State \"TODO\" %U\n")) ''; org-agenda-files = '''("~/org/todo/work.org" "~/org/todo/personal.org.age")''; org-agenda-todo-ignore-scheduled = "'future"; org-agenda-tags-todo-honor-ignore-options = true; org-enforce-todo-dependencies = true; org-agenda-dim-blocked-tasks = "'invisible"; }; config = '' (set-face-attribute 'org-level-1 nil :font "Iosevka Aile") (set-face-attribute 'org-level-2 nil :font "Iosevka Aile") (set-face-attribute 'org-level-3 nil :font "Iosevka Aile") (set-face-attribute 'org-level-4 nil :font "Iosevka Aile") (set-face-attribute 'org-level-5 nil :font "Iosevka Aile") (set-face-attribute 'org-level-6 nil :font "Iosevka Aile") (set-face-attribute 'org-level-7 nil :font "Iosevka Aile") (set-face-attribute 'org-level-8 nil :font "Iosevka Aile") ''; }; simple = { enable = true; hook = ["(org-mode . visual-line-mode)"]; }; org-modern = { enable = true; hook = ["org-mode"]; }; org-roam = { enable = true; defer = true; }; verb = { enable = true; defer = true; }; visual-fill-column = { enable = true; defer = true; }; }; } #+END_SRC #+BEGIN_SRC elisp :tangle nil :noweb nil ;; (require 'org-roam) ;; (bind-keys ("C-c n l" . org-roam-buffer-toggle) ;; ("C-c n f" . org-roam-node-find) ;; ("C-c n g" . org-roam-graph) ;; ("C-c n i" . org-roam-node-insert) ;; ("C-c n c" . org-roam-capture)) ;; (setq org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag))) ;; (org-roam-db-autosync-mode) ;; (define-key org-mode-map (kbd "C-c C-r") verb-command-map) #+END_SRC *** Presentations :PROPERTIES: :header-args:elisp: :tangle no :noweb-ref emacs-org-present-config :END: ~org-present~ is a cool little package. It lets you give presentations with org mode. Each top-level heading turns into a slide. When you switch to present mode, ~org-present~ removes all other windows, narrows the buffer to only one top-level heading at a time, and exposes keys to quickly navigate forward / backward between slides. It also exposes entry / exit hooks. Those hooks allow you to add custom styling to present mode. For example, you could make all the text bigger. #+begin_src nix { programs.emacs.init.usePackage.org-present = { enable = true; command = ["org-present"]; init = '' <> ''; config = '' <> ''; }; } #+end_src Let's customize the window appearance when presenting. First, we'll make fonts larger, by multiplying them by a constant factor. We'll use ~face-remap~ for that. When ~face-remap~ changes a face, it returns a "cookie" that can be used to restore the face to its old size. We'll store those cookies in a buffer-local variable. When we enter present mode, we'll enlarge the fonts. #+begin_src elisp :noweb-ref emacs-org-present-init (defvar-local my/org-present-face-remapping-cookies nil) #+end_src #+begin_src elisp (add-hook 'org-present-mode-hook (lambda () (require 'face-remap) (setq-local my/org-present-face-remapping-cookies nil) (dolist (face '((default :height 2.0) (header-line :height 4.0 :background "#303446") (org-document-title :height 1.75) (org-code :height 2.0) (org-verbatim :height 2.0) (org-block :height 1.25) (org-block-begin-line :height 0.7) (org-level-1 :height 3.0) (org-level-2 :height 3.0) (org-level-3 :height 3.0) (org-level-4 :height 3.0) (org-level-5 :height 3.0) (org-level-6 :height 3.0) (org-level-7 :height 3.0) (org-level-8 :height 3.0))) (push (apply #'face-remap-add-relative face) my/org-present-face-remapping-cookies)))) (add-hook 'org-present-mode-quit-hook (lambda () (dolist (cookie my/org-present-face-remapping-cookies) (face-remap-remove-relative cookie)))) #+end_src Let's add some empty space at the top as a margin. This is kind of a hack, but :shrug:. #+begin_src elisp (add-hook 'org-present-mode-hook (lambda () (setq header-line-format " "))) (add-hook 'org-present-mode-quit-hook (lambda () (setq header-line-format nil))) #+end_src To make this look more like a "real" presentation, we want to center the text in the window. We'll use ~visual-fill-column~ to do that. It does two things: 1. Causes text to wrap, like ~visual-line-mode~, but it wraps at the fill column instead of the edge of the window. 2. Centers the text block within the window (based on the fill column). Let's set it up when we enter present mode: #+begin_src elisp (add-hook 'org-present-mode-hook (lambda () (visual-fill-column-mode 1) (setq visual-fill-column-center-text t))) (add-hook 'org-present-mode-quit-hook (lambda () (visual-fill-column-mode 0))) #+end_src We want to display inline images, of course. #+begin_src elisp (add-hook 'org-present-mode-hook (lambda () (org-display-inline-images))) (add-hook 'org-present-mode-quit-hook (lambda () (org-remove-inline-images))) #+end_src And finally, we want to hide the cursor when presenting. #+begin_src elisp (add-hook 'org-present-mode-hook (lambda () (org-present-hide-cursor))) (add-hook 'org-present-mode-quit-hook (lambda () (org-present-show-cursor))) #+end_src ** Terminal #+BEGIN_SRC nix { programs.emacs.init.usePackage = { eat = { enable = true; command = ["eat"]; hook = ["(eshell-load . eat-eshell-mode)"]; }; }; } #+END_SRC ** Ledger #+BEGIN_SRC nix { programs.emacs.init.usePackage = { ledger-mode.enable = true; }; } #+END_SRC * Theme :PROPERTIES: :header-args:nix: :noweb-ref nix-configs :END: I like Catppuccin Frappe as my theme. Let's use it in as many places as we can. ** Bat #+BEGIN_SRC nix { programs.bat = { config.theme = "catppuccin-frappe"; themes = { catppuccin-frappe = { src = pkgs.fetchFromGitHub { owner = "catppuccin"; repo = "sublime-text"; # Bat uses sublime syntax for its themes rev = "3d8625d937d89869476e94bc100192aa220ce44a"; sha256 = "3ABUsfJpb6RO6AiuuSL5gwDofJIwC5tlEMzBrlY9/s0="; }; file = "Frappe.tmTheme"; }; }; }; } #+END_SRC ** Helix #+BEGIN_SRC nix { programs.helix.settings.theme = "catppuccin_frappe"; } #+END_SRC ** Tmux #+BEGIN_SRC nix { programs.tmux.plugins = [ { plugin = pkgs.tmuxPlugins.catppuccin; extraConfig = ''set -g @catppuccin_window_tabs_enabled on set -g @catppuccin_window_default_text "#W" set -g @catppuccin_window_current_text "#W"''; } ]; } #+END_SRC ** Lazygit #+BEGIN_SRC nix { xdg = { enable = false ; # configFile."lazygit/config.yml" = { # enable = true; # source = pkgs.fetchFromGitHub # { # owner = "catppuccin"; # repo = "lazygit"; # rev = "b2ecb6d41b6f54a82104879573c538e8bdaeb0bf"; # Replace with the latest commit hash # sha256 = "9BBmWRcjNaJE9T0RKVEJaSnkrbMom0CLYE8PzAT6yFw="; # } + /themes/frappe.yml; # }; }; } #+END_SRC ** Starship #+BEGIN_SRC nix { programs.starship.settings = builtins.fromTOML (builtins.readFile (pkgs.fetchFromGitHub { owner = "catppuccin"; repo = "starship"; rev = "3e3e54410c3189053f4da7a7043261361a1ed1bc"; # Replace with the latest commit hash sha256 = "soEBVlq3ULeiZFAdQYMRFuswIIhI9bclIU8WXjxd7oY="; } + /palettes/frappe.toml)); } #+END_SRC ** Wezterm :PROPERTIES: :header-args:lua: :noweb-ref wezterm-config :END: #+BEGIN_SRC lua config.color_scheme = 'Catppuccin Frappe' #+END_SRC ** Ghostty #+BEGIN_SRC nix { programs.ghostty.settings.theme = "Catppuccin Frappe"; } #+END_SRC ** Emacs #+BEGIN_SRC nix { programs.emacs = { extraPackages = epkgs: with epkgs; [ catppuccin-theme ]; extraConfig = ''(defvar my-base-font-size ${if pkgs.stdenv.isDarwin then "130" else "100"}) (require 'catppuccin-theme) (setopt catppuccin-flavor 'frappe) (load-theme 'catppuccin :no-confirm) ;; (set-face-attribute 'default nil :font "JetBrainsMono Nerd Font" :height 130) ;; (set-face-attribute 'fixed-pitch nil :font "JetBrainsMono Nerd Font" :height 130) (set-face-attribute 'default nil :font "Iosevka Term SS14" :height my-base-font-size) (set-face-attribute 'fixed-pitch nil :font "Iosevka Term SS14" :height my-base-font-size) (set-face-attribute 'variable-pitch nil :font "Iosevka Etoile" :height my-base-font-size) ''; }; } #+END_SRC