Files
dotfiles/home.org
T
2026-09-04 17:07:17 -04:00

2349 lines
75 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#+PROPERTY: header-args :mkdirp yes
#+PROPERTY: header-args:elisp :lexical t
* Categories
#+BEGIN_SRC nix :tangle target/base.nix
{lib, ...}: {
imports = lib.filter
(n: lib.strings.hasSuffix ".nix" n)
(lib.filesystem.listFilesRecursive ./base);
}
#+END_SRC
#+BEGIN_SRC nix :tangle target/desktop.nix
{lib, ...}: {
imports = lib.filter
(n: lib.strings.hasSuffix ".nix" n)
(lib.filesystem.listFilesRecursive ./desktop);
}
#+END_SRC
#+BEGIN_SRC nix :tangle target/redbook.nix
{lib, ...}: {
imports = lib.filter
(n: lib.strings.hasSuffix ".nix" n)
(lib.filesystem.listFilesRecursive ./redbook);
}
#+END_SRC
#+BEGIN_SRC nix :tangle target/redbook-desktop.nix
{lib, ...}: {
imports = lib.filter
(n: lib.strings.hasSuffix ".nix" n)
(lib.filesystem.listFilesRecursive ./redbook-desktop);
}
#+END_SRC
* Packages
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 :tangle target/base/sbcl.nix
{pkgs, ...}:
{
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 :tangle target/base/crypto.nix
{pkgs, ...}:
{
home.packages = with pkgs; [
rage
age-plugin-yubikey
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 :tangle target/base/rust-reimpls.nix
{pkgs, ...}:
{
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 :tangle target/base/ledger.nix
{pkgs, ...}:
{
home.packages = with pkgs; [
ledger
];
}
#+END_SRC
~nil~ is an LSP server for nix files.
#+BEGIN_SRC nix :tangle target/base/nil.nix
{pkgs, ...}:
{
home.packages = with pkgs; [
nil
];
}
#+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 :tangle target/base/magic-wormhole.nix
{pkgs, ...}:
{
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 :tangle target/redbook/other-packages.nix
{pkgs, ...}:
{
home.packages = with pkgs; [
postgresql
openssh
];
fonts.fontconfig.enable = true;
}
#+END_SRC
These programs require a full desktop environment:
#+BEGIN_SRC nix :tangle target/desktop/desktop-packages.nix
{pkgs, ...}:
{
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"; })
keymapp
proton-pass
wireshark
];
}
#+END_SRC
#+BEGIN_SRC nix :tangle target/redbook-desktop/desktop-packages.nix
{pkgs, ...}:
{
home.packages = with pkgs; [
linear
(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=";
};
})
teams
];
}
#+END_SRC
* Program Configurations
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 :tangle target/base/simple-programs.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 :tangle target/base/bat.nix
{
programs.bat = {
enable = true;
config.style = "numbers";
};
}
#+END_SRC
** Comma
#+BEGIN_SRC nix :tangle target/base/comma.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 :tangle target/base/direnv.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 :tangle target/base/eza.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 :tangle target/base/fzf.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 :tangle target/base/git.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 :tangle target/base/helix.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 <C-r>%<ret>";
};
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 :tangle target/base/nh.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 :tangle target/base/starship.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 :tangle target/base/tmux.nix
{pkgs, ...}:
{
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: :tangle target/desktop/wezterm.lua
: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 :tangle target/desktop/wezterm.nix
{
programs.wezterm = {
enable = true;
extraConfig = builtins.readFile ./wezterm.lua;
};
}
#+END_SRC
Then the real lua config:
#+BEGIN_SRC lua
local config = {}
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 :tangle target/desktop/kitty.nix
{pkgs, ...}: {
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
#+BEGIN_SRC nix :tangle target/desktop/ghostty.nix
{pkgs, ...}: {
programs.ghostty = {
enable = true;
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 :tangle target/base/zsh.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 :tangle target/base/zsh-nix-shell.nix
{pkgs, ...}:
{
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
** Claude Code
#+BEGIN_SRC nix :tangle target/base/claude-code.nix
{claude-code, ...}: {
programs.claude-code = {
enable = true;
package = claude-code;
};
}
#+END_SRC
* Emacs
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.
** 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
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-global.el
:END:
These are just some general settings that apply everywhere. First, a few packages:
#+BEGIN_SRC nix :tangle target/desktop/emacs-global.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
ace-window
doom-modeline
free-keys
ligature
mixed-pitch
popper
];
extraConfig = builtins.readFile ./emacs-global.el;
};
}
#+END_SRC
Turn off some default settings that I don't find useful.
#+BEGIN_SRC elisp
(setopt save-interprogram-paste-before-kill t)
(setopt kill-whole-line t)
(column-number-mode 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)
(tab-bar-mode)
#+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
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
(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 elisp
(setopt 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 elisp
(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 elisp
(repeat-mode)
#+END_SRC
Save the minibuffer history to disk, so recently-used options show up at the top of the picker
across restarts.
#+BEGIN_SRC elisp
(savehist-mode)
#+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 elisp
(setopt doom-modeline-irc nil)
(setopt doom-modeline-workspace-name nil)
(setopt doom-modeline-buffer-encoding nil)
(setopt doom-modeline-vcs-max-length 30)
(doom-modeline-mode)
#+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 elisp
(ligature-set-ligatures 'prog-mode
'("--" "---" "==" "===" "!=" "!==" "=!="
"=:=" "=/=" "<=" ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!"
"??" "???" "?:" "?." "?=" "<:" ":<" ":>" ">:" "<:<" "<>" "<<<" ">>>"
"<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##" "###" "####"
"#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#=" "^=" "<$>" "<$"
"$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "</" "</>" "/>" "<!--" "<#--"
"-->" "->" "->>" "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>"
"==>" "=>" "=>>" ">=>" ">>=" ">>-" ">-" "-<" "-<<" ">->" "<-<" "<-|"
"<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>" "~>" "~-" "-~"
"~@" "[||]" "|]" "[|" "|}" "{|" "[<" ">]" "|>" "<|" "||>" "<||"
"|||>" "<|||" "<|>" "..." ".." ".=" "..<" ".?" "::" ":::" ":=" "::="
":?" ":?>" "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__" "???"
"<:<" ";;;"))
(global-ligature-mode t)
#+END_SRC
Use variable-width fonts in text buffers. ~mixed-pitch-mode~ keeps most code blocks as monospace.
#+BEGIN_SRC elisp
(add-hook 'text-mode-hook 'mixed-pitch-mode)
(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 elisp
(bind-keys ("M-o" . ace-window)
("s-o" . ace-window)
("C-c o" . ace-window))
(setopt aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
(setopt aw-dispatch-always t)
#+END_SRC
Set up a few project- / workspace-related key bindings.
#+BEGIN_SRC elisp
(bind-keys ("s-f" . project-find-file)
("s-F" . project-switch-project)
;; ("s-s" . persp-switch)
)
(with-eval-after-load 'tab-bar
(bind-keys :map tab-bar-mode-map
("s-s" . tab-bar-switch-to-tab)))
#+END_SRC
#+RESULTS:
: tab-bar-switch-to-tab
Don't run a server. I'm not using this, I can always turn it back on if I need to.
#+BEGIN_SRC elisp
(server-mode -1)
#+END_SRC
All the beeping is annoying. This flashes a huge icon over the screen, which is still annoying, but
less so?
#+BEGIN_SRC elisp
(setopt visible-bell t)
#+END_SRC
I'm trying this out. This disables visual selections. It's bold, which is why I like it :)
#+BEGIN_SRC elisp
(transient-mark-mode -1)
#+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 elisp
(bind-keys :repeat-map tab-bar-repeat-map
("<left>" . tab-bar-history-back)
("<right>" . tab-bar-history-forward))
(tab-bar-history-mode)
#+END_SRC
Make compilation mode buffers auto-scroll to the first error:
#+BEGIN_SRC elisp
(setopt compilation-scroll-output 'first-error)
#+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 elisp
(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)))))
(setopt popper-reference-buffers
'("\\*Flymake diagnostics .*"
"\\*Embark Export: .*"
"\\*Embark Collect .*"
"\\*xref\\*"
"\\*compilation\\*"
"\\*pnpm:.*"
"\\*sly-mrepl *"
"\\*lispy-message\\*"
"\\*occur\\*"
".*eshell\\*"
"\\*SQL: .*"
"magit-revision: .*"
"\\*Help\\*"
"\\*rustfmt\\*"
"\\*slack: .*"
"\\*forge: .*"
"new-pullreq"
"\\*claude:.*"
"\\*Man .*"
"\\*List .*"
"\\*eldoc\\*"))
(setopt popper-display-control nil)
(popper-mode)
(popper-echo-mode)
(bind-keys ("C-q" . popper-toggle)
("C-;" . popper-cycle))
(defvar jf/fullscreen-popup--param 'jf/fullscreen-saved-configuration
"Key used to stash the window configuration a fullscreen popup replaced.
Stored per-tab when `tab-bar-mode' is active, otherwise as a plain frame
parameter; see `jf/tab-or-frame-parameter'.")
(defvar-local jf/fullscreen-popup-p nil
"Non-nil in a buffer currently being displayed as a fullscreen popup.")
(defun jf/current-tab-cell (&optional frame)
"Return FRAME's current tab's raw data cell, or nil without `tab-bar-mode'."
(and (bound-and-true-p tab-bar-mode)
(assq 'current-tab (tab-bar-tabs frame))))
(defun jf/tab-or-frame-parameter (parameter &optional frame)
"Like `frame-parameter', but scoped to FRAME's selected tab when tabs are in use.
A plain frame parameter is shared by every tab on that frame, which is wrong
for state like `jf/fullscreen-popup--param': switching tabs swaps in an
entirely different window layout, so \"the layout to restore\" has to travel
with the tab, not the frame."
(if-let ((tab (jf/current-tab-cell frame)))
(alist-get parameter tab)
(frame-parameter frame parameter)))
(defun jf/set-tab-or-frame-parameter (parameter value &optional frame)
"Set PARAMETER to VALUE, scoped like `jf/tab-or-frame-parameter'."
(if-let ((tab (jf/current-tab-cell frame)))
(let ((cell (assq parameter tab)))
(if cell
(setcdr cell value)
(setcdr tab (cons (cons parameter value) (cdr tab)))))
(set-frame-parameter frame parameter value)))
(defun jf/display-buffer-fullscreen (buffer alist)
"Display BUFFER, deleting other windows, saving the prior layout.
Intended for use in `display-buffer-alist' for popups (e.g. Magit
status/log) that should take over the frame while displayed, but
restore the previous window configuration once closed via either
`q' or `popper-toggle' -- both end up calling `quit-window', which
is where the restore actually happens; see
`jf/quit-window-restore-fullscreen'."
(unless (jf/tab-or-frame-parameter jf/fullscreen-popup--param)
(jf/set-tab-or-frame-parameter jf/fullscreen-popup--param
(current-window-configuration)))
(when-let ((window (or (display-buffer-reuse-window buffer alist)
(display-buffer-same-window buffer alist)
(display-buffer-pop-up-window buffer alist)
(display-buffer-use-some-window buffer alist))))
(let ((ignore-window-parameters t))
(delete-other-windows window))
(with-current-buffer buffer
(setq jf/fullscreen-popup-p t))
window))
(defun jf/quit-window-restore-fullscreen (orig-fn &optional kill window)
"Restore the saved window configuration after quitting a fullscreen popup.
Advice around `quit-window' so both `q' (which Magit and other
special-mode buffers bind to buffer-quitting commands that call
`quit-window') and Popper's close/toggle commands (which also call
`quit-window' directly) trigger the restore. Only restores once the
last fullscreen popup on the tab (or frame, without `tab-bar-mode')
has closed, so opening one fullscreen popup from within another
falls back to the outer one first."
(let* ((window (or window (selected-window)))
(was-fullscreen (buffer-local-value 'jf/fullscreen-popup-p
(window-buffer window)))
(frame (window-frame window)))
(funcall orig-fn kill window)
(when (and was-fullscreen
(not (and (window-live-p window)
(buffer-local-value 'jf/fullscreen-popup-p
(window-buffer window))))
(jf/tab-or-frame-parameter jf/fullscreen-popup--param frame))
(let ((conf (jf/tab-or-frame-parameter jf/fullscreen-popup--param frame)))
(jf/set-tab-or-frame-parameter jf/fullscreen-popup--param nil frame)
(set-window-configuration conf)))))
(advice-add 'quit-window :around #'jf/quit-window-restore-fullscreen)
(setopt split-width-threshold 200)
(setopt split-height-threshold nil)
(setopt display-buffer-base-action
'((display-buffer-reuse-window display-buffer-same-window
display-buffer-in-previous-window
display-buffer-use-some-window)))
(setq display-buffer-alist
'(((or "magit: *"
"magit-log*"
"\\*Messages\\*"
"\\*trace-output\\*"
"\\*EGLOT connections\\*")
(jf/display-buffer-fullscreen))
((or "\\*Flymake diagnostics .*"
"\\*Embark Export: .*"
"\\*Embark Collect .*"
"\\*xref\\*"
"\\*compilation\\*"
"\\*pnpm:.*"
"\\*sly-mrepl *"
"\\*lispy-message\\*"
"\\*occur\\*"
".*eshell\\*"
"\\*SQL: .*")
(display-buffer-in-side-window)
(side . bottom)
(slot . 0)
(window-height . 0.33)
(window-parameters (no-delete-other-windows . t)
(no-other-window . t)))
((or "magit-revision: .*"
"\\*Help\\*"
"\\*rustfmt\\*"
"\\*slack: .*"
"\\*forge: .*"
"new-pullreq"
"\\*claude:.*"
"\\*Man .*"
"\\*List .*"
"\\*Embark Actions\\*"
"\\*eldoc\\*")
(jf/display-buffer-right))))
(setopt window-persistent-parameters
(cl-list* '(no-delete-other-windows . writable)
'(no-other-window . writable)
window-persistent-parameters))
#+END_SRC
** Editing
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-editing.el
:END:
This configuration affects general text editing, across all modes. First, some packages:
#+BEGIN_SRC nix :tangle target/desktop/emacs-editing.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
avy
edit-indirect
embark
format-all
god-mode
# evil
# evil-collection
# evil-org
string-inflection
];
extraConfig = builtins.readFile ./emacs-editing.el;
};
}
#+END_SRC
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 elisp
;; (setopt evil-want-keybinding nil)
;; (require 'evil)
;; (evil-mode 1)
;; (require 'evil-collection)
;; (evil-collection-init)
;; (require 'evil-org)
;; (add-hook 'org-mode-hook 'evil-org-mode)
;; (evil-org-set-key-theme '(navigation insert textobjects additional calendar))
;; (require 'evil-org-agenda)
;; (evil-org-agenda-set-keys)
(require 'god-mode)
(require 'which-key)
(bind-keys :map god-local-mode-map
("i" . (lambda () (interactive) (god-mode-all -1)))
("z" . repeat))
(bind-keys ("<escape>" . (lambda () (interactive) (god-mode-all 1))))
(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)
(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 elisp
(defun avy-goto-char-timer-embark ()
(interactive)
(require 'avy)
(let ((avy-action-oneshot
(lambda (pt)
(unwind-protect
(save-excursion
(goto-char pt)
(embark-act))
(select-window
(cdr (ring-ref avy-ring 0)))))))
(avy-goto-char-timer)))
(bind-keys
("C-j" . avy-goto-char-timer)
("M-j" . avy-goto-char-timer-embark)
("C-." . embark-act))
(with-eval-after-load 'lispy
(bind-keys :map lispy-mode-map
("C-j" . avy-goto-char-timer)))
(with-eval-after-load 'org
(bind-keys :map org-mode-map
("C-j" . avy-goto-char-timer)))
#+END_SRC
Just "silently" wrap when searching at the bottom of a document.
#+BEGIN_SRC elisp
(setopt isearch-wrap-pause 'no-ding)
#+END_SRC
#+BEGIN_SRC elisp
(defun string-inflection-rust-style-cycle-function (str)
(cond
((string-inflection-snake-case-p str)
(string-inflection-upcase-function str))
((string-inflection-upcase-p str)
(string-inflection-pascal-case-function str))
(t
(string-inflection-snake-case-function str))))
(defun string-inflection-rust-style-cycle ()
(interactive)
(require 'string-inflection)
(string-inflection--symbol-or-region #'string-inflection-rust-style-cycle-function))
(bind-keys :repeat-map string-inflection-java-map
("u" . string-inflection-java-style-cycle))
(bind-keys :repeat-map string-inflection-rust-map
("u" . string-inflection-rust-style-cycle))
(bind-keys ("C-x 4 n" . edit-indirect-region))
#+END_SRC
** Workflow
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-workflow.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-workflow.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
buffer-terminator
];
extraConfig = builtins.readFile ./emacs-workflow.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(buffer-terminator-mode 1)
(bind-keys
("s-b" . consult-project-buffer))
#+END_SRC
** Language Support
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-languages.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-languages.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
consult-eglot
graphql-ts-mode
haskell-ts-mode
jenkinsfile-mode
jq-mode
(lispy.overrideAttrs {
packageRequires = [
ace-window iedit swiper zoutline hydra
];
ignoreCompilationError = true;
})
# lispyville
lua-mode
nix-mode
pnpm-mode
prisma-ts-mode
rust-mode
sly
sly-macrostep
sql-indent
tree-sitter
(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-nginx
g.tree-sitter-nix
g.tree-sitter-org
g.tree-sitter-php
g.tree-sitter-prisma
g.tree-sitter-python
g.tree-sitter-rust
g.tree-sitter-rust-format-args
g.tree-sitter-scss
g.tree-sitter-sql
g.tree-sitter-toml
g.tree-sitter-tsx
g.tree-sitter-typescript
g.tree-sitter-xml
g.tree-sitter-yaml
]))
yaml-pro
];
extraConfig = builtins.readFile ./emacs-languages.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(electric-pair-mode 1)
(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)
(global-tree-sitter-mode 1)
(setopt ediff-split-window-function 'split-window-horizontally)
(defun project-nix-flake ()
(interactive)
(nix-flake (project-root (project-current t))))
(bind-keys ("C-x p n" . project-nix-flake))
#+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 elisp
(setq-default fill-column 120)
(defun jf-init-fill-column ()
(display-fill-column-indicator-mode 1))
(add-hook 'prog-mode-hook 'jf-init-fill-column)
#+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 elisp
(with-eval-after-load 'prog-mode
(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 elisp
(add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)
#+END_SRC
*** Flymake
#+begin_src elisp
(autoload 'flymake-show-project-diagnostics "flymake")
(autoload 'flymake-goto-next-error "flymake")
(autoload 'flymake-goto-prev-error "flymake")
(with-eval-after-load 'prog-mode
(bind-keys :map prog-mode-map
("C-c D" . flymake-show-project-diagnostics)
("M-n" . flymake-goto-next-error)
("M-p" . flymake-goto-prev-error)))
(with-eval-after-load 'flymake
(bind-keys :map flymake-project-diagnostics-mode-map
("q" . quit-window)))
#+end_src
*** Eglot
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 elisp
(setopt eglot-ignored-server-capabilities '(:inlayHintProvider :semanticTokensProvider))
(add-hook 'eglot-managed-mode-hook (lambda () (eglot-inlay-hints-mode -1)) nil t)
(setopt eglot-code-action-indications nil)
(setopt eglot-events-buffer-config '(:size 300000 :format full))
#+END_SRC
Set up a few useful eglot keys.
#+begin_src elisp
(with-eval-after-load 'eglot
(bind-keys :map 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 elisp
(with-eval-after-load 'eglot
(add-to-list 'eglot-stay-out-of 'yasnippet))
#+end_src
*** Rust
#+BEGIN_SRC elisp
(setq major-mode-remap-alist '((rust-mode . rust-mode)))
(setopt rust-mode-treesitter-derive t)
(with-eval-after-load 'rust-prog-mode
(bind-keys :map rust-mode-map
("C-c u" . string-inflection-rust-style-cycle)))
#+END_SRC
*** YAML
#+BEGIN_SRC elisp
(defun yaml-validate ()
(interactive)
(compile (concat "yamllint -d '{extends: default, rules: {indentation: {indent-sequences: false}, braces: {max-spaces-inside: 1}}}' " (buffer-file-name)) t))
(add-hook 'yaml-ts-mode-hook 'yaml-pro-ts-mode)
#+END_SRC
*** Lisp
#+BEGIN_SRC elisp
(setopt inferior-lisp-program "sbcl")
(setopt lispy-compat '(god-mode edebug))
(add-hook 'emacs-lisp-mode-hook (lambda () (lispy-mode 1)))
(add-hook 'lisp-mode-hook (lambda () (lispy-mode 1)))
(add-hook 'lisp-data-mode-hook (lambda () (lispy-mode 1)))
(with-eval-after-load 'lispy
;; (require 'lispyville)
;; (add-hook 'lispy-mode-hook #'lispyville-mode)
(bind-keys :map lispy-mode-map
("i" . (lambda ()
(interactive)
(if (and (featurep 'god-mode) god-global-mode)
(god-mode-all -1)
(special-lispy-tab))))))
#+END_SRC
*** Typescript
#+BEGIN_SRC elisp
(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-mode))
(add-to-list 'major-mode-remap-alist '(typescript-mode . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-mode))
(add-to-list 'major-mode-remap-alist '(tsx-mode . tsx-ts-mode))
(setopt typescript-ts-mode-indent-offset 2)
(with-eval-after-load 'typescript-ts-mode
(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)))))
(with-eval-after-load 'typescript-ts-mode
(define-key typescript-ts-mode-map (kbd "C-c '") #'my/edit-indirect-tagged-template-at-point)
(define-key tsx-ts-mode-map (kbd "C-c '") #'my/edit-indirect-tagged-template-at-point))
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs '((prisma-ts-mode) "prisma-language-server" "--stdio")))
#+END_SRC
*** Java
#+BEGIN_SRC elisp
(push '(java-mode . java-ts-mode) major-mode-remap-alist)
(with-eval-after-load 'java-ts-mode
(bind-keys :map java-ts-mode-map
("C-c u" . string-inflection-java-style-cycle))
(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
*** SQL
#+BEGIN_SRC elisp
(add-hook 'sql-mode-hook 'sqlind-minor-mode)
#+END_SRC
*** Haskell
#+BEGIN_SRC elisp
(setopt haskell-ts-use-indent t)
#+END_SRC
** Envrc
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-envrc.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-envrc.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
envrc
];
extraConfig = builtins.readFile ./emacs-envrc.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(add-hook 'after-init-hook 'envrc-global-mode)
(setopt envrc-show-summary-in-minibuffer nil)
#+END_SRC
** Magit
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-magit.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-magit.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
consult-gh
diff-hl
forge
git-timemachine
magit
];
extraConfig = builtins.readFile ./emacs-magit.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(setopt magit-define-global-key-bindings 'recommended)
(bind-keys ("s-g" . magit))
(setopt magit-save-repository-buffers 'dontask)
(setopt magit-commit-show-diff nil)
(with-eval-after-load 'magit
(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)
(require 'forge)
(add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh))
(with-eval-after-load 'forge
(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)))
(global-diff-hl-mode)
#+END_SRC
** Vertico
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-vertico.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-vertico.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
cape
consult
corfu
embark-consult
marginalia
orderless
vertico
];
extraConfig = builtins.readFile ./emacs-vertico.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(bind-keys
;; ("C-c M-x" . consult-mode-command)
("C-c h" . consult-history)
;; ("C-c k" . consult-kmacro)
;; ("C-c m" . consult-man)
;; ([remap Info-search] . consult-info)
;; ("C-x M-:" . consult-complex-command)
("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 t b" . consult-buffer-other-tab)
("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)
:map 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)
:map minibuffer-local-map
("M-s" . consult-history)
("M-r" . consult-history))
(advice-add 'register-preview :override #'consult-register-window)
(setopt register-preview-delay 0.5)
(setopt xref-show-xrefs-function 'consult-xref)
(setopt xref-show-definitions-function 'consult-xref)
(eval-when-compile
(require 'consult))
(with-eval-after-load 'consult
(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)))
(setopt consult-narrow-key "<") ;; "C-+"
(setopt vertico-count 25)
(vertico-mode)
(setopt completion-styles '(orderless basic))
(marginalia-mode)
(setopt corfu-cycle t)
(setopt corfu-popupinfo-mode nil)
(setopt corfu-quit-at-boundary nil)
(setopt corfu-on-exact-match 'show)
(global-corfu-mode)
;; (setq completion-in-region-function #'consult-completion-in-region)
(add-hook 'completion-at-point-functions 'cape-file)
#+END_SRC
** Org
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-org.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-org.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
org-modern
org-present
org-roam
ox-slack
verb
visual-fill-column
];
extraConfig = builtins.readFile ./emacs-org.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(require 'org)
(bind-keys ("C-c A" . org-agenda)
("C-c C" . org-capture)
("C-c l" . org-id-store-link))
(setopt org-babel-load-languages '((emacs-lisp . t) (sql . t)))
(setopt org-confirm-babel-evaluate nil)
(add-hook 'org-mode-hook 'visual-line-mode)
(setopt org-link-keep-stored-after-insertion t
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 t
org-enforce-todo-dependencies t
org-agenda-dim-blocked-tasks 'invisible)
(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")
(defvar-local my/org-present-face-remapping-cookies nil)
(with-eval-after-load 'org-present
(require 'face-remap)
(require 'visual-fill-column)
(defun my/org-present-start ()
(setq-local my/org-present-face-remapping-cookies nil)
;; Tweak font sizes
(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))
;; Set a blank header line string to create blank space at the top
(setq header-line-format " ")
;; Display inline images automatically
(org-display-inline-images)
(org-present-hide-cursor)
;; Center the presentation and wrap lines
(visual-fill-column-mode 1)
(setq visual-fill-column-center-text t)
;; (visual-line-mode 1)
)
(defun my/org-present-end ()
;; Reset font customizations
(dolist (cookie my/org-present-face-remapping-cookies)
(face-remap-remove-relative cookie))
;; Clear the header line string so that it isn't displayed
(setq header-line-format nil)
;; Stop displaying inline images
(org-remove-inline-images)
(org-present-show-cursor)
;; Stop centering the document
(visual-fill-column-mode 0)
;; (visual-line-mode 0)
)
(add-hook 'org-present-mode-hook 'my/org-present-start)
(add-hook 'org-present-mode-quit-hook 'my/org-present-end))
;; (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)
(use-package org-modern
:hook org-mode-hook)
;; (define-key org-mode-map (kbd "C-c C-r") verb-command-map)
#+END_SRC
** Terminal
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-terminal.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-terminal.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
eat
];
extraConfig = builtins.readFile ./emacs-terminal.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(use-package eat
:commands (eat)
:hook (eshell-load-hook . eat-eshell-mode))
#+END_SRC
** Slack
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-slack.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-slack.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
slack
];
extraConfig = builtins.readFile ./emacs-slack.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(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
** Ledger
#+BEGIN_SRC nix :tangle target/desktop/emacs-ledger.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
ledger-mode
];
};
}
#+END_SRC
** Age
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-age.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-age.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
age
];
extraConfig = builtins.readFile ./emacs-age.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(use-package age
:custom
(setopt age-default-identity "~/source/dotfiles/keys/yubi1age.txt")
(age-default-recipient "~/source/dotfiles/keys/myself.txt")
:config
(setenv "PINENTRY_PROGRAM" "pinentry-mac")
(age-file-enable))
#+END_SRC
** Claude Code
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-claude-code.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-claude-code.nix
{pkgs, ...}:
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with 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 ];
})
];
extraConfig = builtins.readFile ./emacs-claude-code.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(use-package claude-code
:bind-keymap ("C-c c" . claude-code-command-map)
:custom (claude-code-optimize-window-resize nil))
#+END_SRC
* Theme
I like Catppuccin Frappe as my theme. Let's use it in as many places as we can.
** Bat
#+BEGIN_SRC nix :tangle target/base/bat-theme.nix
{pkgs, ...}:
{
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 :tangle target/base/helix-theme.nix
{
programs.helix.settings.theme = "catppuccin_frappe";
}
#+END_SRC
** Tmux
#+BEGIN_SRC nix :tangle target/base/tmux-theme.nix
{pkgs, ...}:
{
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 :tangle target/base/lazygit.nix
{pkgs, ...}:
{
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 :tangle target/base/starship_colors.nix
{pkgs, ...}:
{
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
#+BEGIN_SRC lua :tangle target/desktop/wezterm.lua
config.color_scheme = 'Catppuccin Frappe'
return config
#+END_SRC
** Ghostty
#+BEGIN_SRC nix :tangle target/desktop/ghostty-theme.nix
{
programs.ghostty.settings.theme = "Catppuccin Frappe";
}
#+END_SRC
** Emacs
:PROPERTIES:
:header-args:elisp: :tangle target/desktop/emacs-theme.el
:END:
#+BEGIN_SRC nix :tangle target/desktop/emacs-theme.nix
{
programs.emacs = {
enable = true;
extraPackages = epkgs: with epkgs; [
catppuccin-theme
];
extraConfig = builtins.readFile ./emacs-theme.el;
};
}
#+END_SRC
#+BEGIN_SRC elisp
(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 130)
(set-face-attribute 'fixed-pitch nil :font "Iosevka Term SS14" :height 130)
(set-face-attribute 'variable-pitch nil :font "Iosevka Etoile" :height 130)
#+END_SRC