aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimen A. W. Olsen <hello@simenandre.no>2026-09-26 22:14:18 +0200
committerSimen A. W. Olsen <hello@simenandre.no>2026-09-26 22:14:18 +0200
commitefff2bf50109544391cf4721db21c264d151faf7 (patch)
tree96b99c1f14df7dc016e6c9c29774f8bd5e3c565c
parent7c3e313cfec6c67b8ea15afe92d18896393eec59 (diff)
downloaddotfiles-efff2bf50109544391cf4721db21c264d151faf7.tar.gz
dotfiles-efff2bf50109544391cf4721db21c264d151faf7.zip
feat(home): configure aerc as mail client
-rw-r--r--modules/home/aerc.nix445
-rw-r--r--modules/home/default.nix2
2 files changed, 447 insertions, 0 deletions
diff --git a/modules/home/aerc.nix b/modules/home/aerc.nix
new file mode 100644
index 0000000..3ad9e6b
--- /dev/null
+++ b/modules/home/aerc.nix
@@ -0,0 +1,445 @@
+{ pkgs, ... }:
+
+let
+ # Nix store paths are world-readable, so accounts.conf (IMAP/SMTP passwords)
+ # stays hand-managed in ~/.config/aerc and out of this module.
+ opener = if pkgs.stdenv.isDarwin then "open" else "xdg-open";
+
+ # Renders the markdown body of a composed mail into the text/html half of a
+ # multipart/alternative message (see [multipart-converters] below).
+ mdToHtml = pkgs.writeShellScriptBin "aerc-md2html" ''
+ body=$(${pkgs.cmark-gfm}/bin/cmark-gfm \
+ --extension autolink \
+ --extension table \
+ --extension strikethrough \
+ --extension tasklist)
+
+ cat <<HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="color-scheme" content="light dark">
+ <style>
+ body { font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-size: 15px; line-height: 1.5; color: #1a1a1a; background: #ffffff;
+ max-width: 40em; margin: 0; padding: 0; }
+ a { color: #2b5cd9; }
+ h1, h2, h3 { line-height: 1.25; margin: 1.2em 0 0.5em; }
+ h1 { font-size: 1.4em; }
+ h2 { font-size: 1.2em; }
+ h3 { font-size: 1.05em; }
+ p, ul, ol, table { margin: 0 0 1em; }
+ blockquote { margin: 0 0 1em; padding: 0 0 0 1em; border-left: 3px solid #d0d0d0; color: #555; }
+ code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }
+ code { background: #f2f2f2; padding: 0.1em 0.3em; border-radius: 3px; }
+ pre { background: #f2f2f2; padding: 0.8em; border-radius: 4px; overflow-x: auto; }
+ pre code { background: none; padding: 0; }
+ table { border-collapse: collapse; }
+ th, td { border: 1px solid #d0d0d0; padding: 0.3em 0.6em; text-align: left; }
+ hr { border: none; border-top: 1px solid #d0d0d0; margin: 1.5em 0; }
+ @media (prefers-color-scheme: dark) {
+ body { color: #e6e6e6; background: #1a1a1a; }
+ a { color: #7aa2f7; }
+ blockquote { border-left-color: #444; color: #aaa; }
+ code, pre { background: #262626; }
+ th, td, hr { border-color: #444; }
+ }
+ </style>
+ </head>
+ <body>
+ $body
+ </body>
+ </html>
+ HTML
+ '';
+
+ notify =
+ if pkgs.stdenv.isDarwin then
+ # -activate focuses Ghostty when the banner is clicked.
+ "${pkgs.terminal-notifier}/bin/terminal-notifier"
+ + " -group \"aerc-$AERC_ACCOUNT\""
+ + " -title \"$AERC_ACCOUNT/$AERC_FOLDER\""
+ + " -subtitle \"$AERC_FROM_NAME\""
+ + " -message \"$AERC_SUBJECT\""
+ + " -activate com.mitchellh.ghostty"
+ else
+ "${pkgs.libnotify}/bin/notify-send"
+ + " \"[$AERC_ACCOUNT/$AERC_FOLDER] $AERC_FROM_NAME\""
+ + " \"$AERC_SUBJECT\"";
+in
+{
+ home.file = {
+ ".config/aerc/aerc.conf".text = ''
+ [general]
+ default-save-path=~/Downloads
+ pgp-provider=auto
+ default-menu-cmd=fzf --reverse --prompt='> '
+
+ [ui]
+ styleset-name=tokyonight
+ mouse-enabled=true
+ threading-enabled=true
+ fuzzy-complete=true
+ keyhint=true
+ keyhint-delay=300ms
+ sidebar-width=24
+ index-columns=flags:4,name<20%,subject,date>=
+ timestamp-format=2006-01-02 15:04
+ this-day-time-format=15:04
+ this-week-time-format=Mon 15:04
+ this-year-time-format=02 Jan
+ empty-message=(no messages)
+ empty-dirlist=(no folders)
+
+ [statusline]
+ column-left=[{{.Account}}] {{.StatusInfo}}
+ column-center={{.PendingKeys}}
+ column-right={{.TrayInfo}}
+
+ [viewer]
+ pager=less -Rc
+ alternatives=text/plain,text/html
+ show-headers=false
+ header-layout=From|To,Cc|Bcc,Date,Subject
+ parse-http-links=true
+
+ [compose]
+ editor=nvim -c 'set ft=mail' -c 'set textwidth=72' -c 'set nolist'
+ header-layout=To|From,Subject
+ focus-body=true
+ empty-subject-warning=true
+ no-attachment-warning=^[^>]*attach(ed|ment)
+
+ [multipart-converters]
+ text/html=${mdToHtml}/bin/aerc-md2html
+
+ [hooks]
+ mail-received=${notify}
+
+ [filters]
+ text/plain=colorize
+ text/calendar=calendar
+ message/delivery-status=colorize
+ message/rfc822=colorize
+ text/html=! html
+ .headers=colorize
+
+ [openers]
+ x-scheme-handler/http*=${opener} {}
+ x-scheme-handler/mailto=${opener} {}
+ text/html=${opener} {}
+ application/pdf=${opener} {}
+ image/*=${opener} {}
+ '';
+
+ ".config/aerc/binds.conf".text = ''
+ # Part/field navigation is bound twice: C-j/C-k as aerc ships them, plus
+ # ]p/[p and Alt-j/Alt-k (right Option = Alt, see modules/home/ghostty.nix)
+ # which keep working if vim-tmux-navigator's tmux side is ever installed,
+ # since it claims C-h/j/k/l for any pane that is not running vim.
+
+ <C-p> = :prev-tab<Enter>
+ <C-n> = :next-tab<Enter>
+ \[t = :prev-tab<Enter> # Previous tab
+ \]t = :next-tab<Enter> # Next tab
+ <C-t> = :term<Enter>
+ ? = :help keys<Enter>
+ <C-q> = :prompt 'Quit?' quit<Enter>
+ <C-z> = :suspend<Enter>
+
+ [messages]
+ q = :prompt 'Quit?' quit<Enter>
+ . = :repeat
+
+ j = :next<Enter>
+ <Down> = :next<Enter>
+ k = :prev<Enter>
+ <Up> = :prev<Enter>
+ <C-d> = :next 50%<Enter>
+ <C-u> = :prev 50%<Enter>
+ <C-f> = :next 100%<Enter>
+ <C-b> = :prev 100%<Enter>
+ <PgDn> = :next 100%<Enter>
+ <PgUp> = :prev 100%<Enter>
+ gg = :select 0<Enter> # First message
+ G = :select -1<Enter>
+ zz = :align center<Enter>
+ zt = :align top<Enter>
+ zb = :align bottom<Enter>
+
+ J = :next-folder<Enter>
+ K = :prev-folder<Enter>
+ gi = :cf INBOX<Enter> # Inbox
+ ga = :cf Archive<Enter> # Archive
+ gd = :cf Drafts<Enter> # Drafts
+ gs = :cf Sent<Enter> # Sent
+ gj = :cf Junk<Enter> # Junk
+ gt = :cf Trash<Enter> # Trash
+ gf = :menu -dc fzf :cf<Enter> # Find folder (fzf)
+ c = :cf<space>
+
+ <Enter> = :view<Enter>
+ v = :mark -t<Enter>
+ <Space> = :mark -t<Enter>:next<Enter>
+ V = :mark -v<Enter>
+ tt = :toggle-threads<Enter> # Threads
+ ts = :toggle-sidebar<Enter> # Sidebar
+ tf = :toggle-folder<Enter> # Folder subscription
+ tc = :toggle-thread-context<Enter> # Thread context
+ zc = :fold<Enter>
+ zo = :unfold<Enter>
+ za = :fold -t<Enter>
+ zM = :fold -a<Enter>
+ zR = :unfold -a<Enter>
+ <tab> = :fold -t<Enter>
+
+ C = :compose<Enter>
+ r = :reply -aq<Enter>
+ R = :reply -q<Enter>
+ f = :forward<Enter>
+ b = :bounce<space>
+ d = :choose -o y 'Really delete this message' delete-message<Enter>
+ D = :delete<Enter>
+ a = :archive flat<Enter>
+ A = :unmark -a<Enter>:mark -T<Enter>:archive flat<Enter>
+ M = :move<space>
+ mm = :move<space> # Move to folder
+ mc = :copy<space> # Copy to folder
+ mr = :read<Enter> # Mark read
+ mu = :unread<Enter> # Mark unread
+ mf = :flag -t -x flagged<Enter> # Toggle flagged
+ mU = :unsubscribe<Enter> # Unsubscribe from list
+ s = :split<Enter>
+ S = :vsplit<Enter>
+ | = :pipe<space>
+ $ = :term<space>
+
+ / = :search<space>
+ \ = :filter<space>
+ n = :next-result<Enter>
+ N = :prev-result<Enter>
+ <Esc> = :clear<Enter>
+ <C-c> = :clear<Enter>
+ Fu = :filter -u<Enter> # Unread
+ Ff = :filter -x flagged<Enter> # Flagged
+ Fs = :filter -f<space> # From sender
+ Ft = :filter -t<space> # To recipient
+ Fd = :filter -d<space> # Date range
+ Fc = :clear<Enter> # Clear filter
+ F/ = :filter<space> # Filter prompt
+
+ [messages:folder=Drafts]
+ <Enter> = :recall<Enter>
+
+ [view]
+ # The viewer forwards every key it does not bind itself to the pager, so
+ # globals like ? and : never arrive: bind them again here.
+ $ex = :
+ ? = :help keys<Enter>
+ / = :toggle-key-passthrough<Enter>/
+ q = :close<Enter>
+ <Esc> = :close<Enter>
+ o = :open<Enter>
+ O = :open<space>
+ S = :save<space>
+ s = :save -fpa ~/Downloads<Enter>
+ | = :pipe<space>
+ d = :choose -o y 'Really delete this message' delete-message<Enter>
+ D = :delete<Enter>
+ a = :archive flat<Enter>
+ f = :forward<Enter>
+ r = :reply -aq<Enter>
+ R = :reply -q<Enter>
+ H = :toggle-headers<Enter>
+ th = :toggle-headers<Enter> # Headers
+ <C-j> = :next-part<Enter>
+ <C-Down> = :next-part<Enter>
+ <C-k> = :prev-part<Enter>
+ <C-Up> = :prev-part<Enter>
+ \]p = :next-part<Enter> # Next part
+ \[p = :prev-part<Enter> # Previous part
+ \]m = :next<Enter> # Next message
+ \[m = :prev<Enter> # Previous message
+ J = :next<Enter>
+ K = :prev<Enter>
+ gx = :menu -lc fzf :open-link<Enter> # Open link (fzf)
+ gy = :copy-link<space>
+
+ [view::passthrough]
+ $noinherit = true
+ $ex = <C-x>
+ <Esc> = :toggle-key-passthrough<Enter>
+
+ [compose]
+ $noinherit = true
+ $ex = <C-x>
+ $complete = <C-o>
+ <tab> = :next-field<Enter>
+ <backtab> = :prev-field<Enter>
+ <C-j> = :next-field<Enter>
+ <C-Down> = :next-field<Enter>
+ <C-k> = :prev-field<Enter>
+ <C-Up> = :prev-field<Enter>
+ <A-j> = :next-field<Enter>
+ <A-k> = :prev-field<Enter>
+ <A-p> = :switch-account -p<Enter>
+ <A-n> = :switch-account -n<Enter>
+ <C-p> = :prev-tab<Enter>
+ <C-n> = :next-tab<Enter>
+
+ [compose::editor]
+ $noinherit = true
+ $ex = <C-x>
+ <C-j> = :next-field<Enter>
+ <C-Down> = :next-field<Enter>
+ <C-k> = :prev-field<Enter>
+ <C-Up> = :prev-field<Enter>
+ <A-j> = :next-field<Enter>
+ <A-k> = :prev-field<Enter>
+ <C-p> = :prev-tab<Enter>
+ <C-n> = :next-tab<Enter>
+
+ [compose::review]
+ # aerc has no "always send HTML" setting, so the send key adds the
+ # text/html alternative first; Y sends the plain text part on its own.
+ y = :multipart text/html<Enter>:send<Enter> # Send (HTML + plain text)
+ Y = :send<Enter> # Send plain text only
+ h = :multipart text/html<Enter> # Add HTML alternative
+ H = :multipart -d text/html<Enter> # Drop HTML alternative
+ n = :abort<Enter> # Abort (discard message, no confirmation)
+ s = :sign<Enter> # Toggle signing
+ x = :encrypt<Enter> # Toggle encryption to all recipients
+ v = :preview<Enter> # Preview message
+ p = :postpone<Enter> # Postpone
+ q = :choose -o d discard abort -o p postpone postpone<Enter> # Abort or postpone
+ e = :edit<Enter> # Edit (body and headers)
+ a = :attach<space> # Add attachment
+ A = :menu -c 'fzf --multi' :attach<Enter> # Add attachment (fzf)
+ d = :detach<space> # Remove attachment
+
+ [terminal]
+ $noinherit = true
+ $ex = <C-x>
+ <C-p> = :prev-tab<Enter>
+ <C-n> = :next-tab<Enter>
+ '';
+
+ # tokyonight-storm, matching Ghostty (modules/home/ghostty.nix). Background
+ # is left at the terminal default so the panes blend with the window.
+ ".config/aerc/stylesets/tokyonight".text = ''
+ *.default=true
+ *.normal=true
+
+ default.fg=#c0caf5
+
+ error.fg=#f7768e
+ warning.fg=#ff9e64
+ success.fg=#9ece6a
+
+ title.fg=#1f2335
+ title.bg=#7aa2f7
+ title.bold=true
+ header.fg=#7aa2f7
+ header.bold=true
+ realname.fg=#c0caf5
+ email.fg=#565f89
+ subject.fg=#c0caf5
+ subject.bold=true
+ date.fg=#565f89
+
+ tab.fg=#545c7e
+ tab.bg=#1f2335
+ tab.selected.fg=#c0caf5
+ tab.selected.bg=#292e42
+ tab.selected.bold=true
+
+ border.fg=#292e42
+ border.bg=#292e42
+
+ spinner.fg=#7dcfff
+
+ msglist_default.fg=#a9b1d6
+ msglist_unread.fg=#c0caf5
+ msglist_unread.bold=true
+ msglist_read.fg=#737aa2
+ msglist_answered.fg=#bb9af7
+ msglist_forwarded.fg=#9d7cd8
+ msglist_flagged.fg=#e0af68
+ msglist_flagged.bold=true
+ msglist_deleted.fg=#565f89
+ msglist_deleted.dim=true
+ msglist_result.fg=#9ece6a
+ msglist_result.bold=true
+ msglist_thread_folded.fg=#7dcfff
+ msglist_thread_context.fg=#565f89
+ msglist_thread_orphan.fg=#ff9e64
+ msglist_marked.fg=#1f2335
+ msglist_marked.bg=#7dcfff
+ msglist_gutter.fg=#292e42
+ msglist_pill.fg=#1f2335
+ msglist_pill.bg=#7aa2f7
+ msglist_*.selected.bg=#292e42
+ msglist_*.selected.bold=true
+
+ dirlist_default.fg=#a9b1d6
+ dirlist_unread.fg=#7aa2f7
+ dirlist_unread.bold=true
+ dirlist_recent.fg=#7dcfff
+ dirlist_*.selected.bg=#292e42
+ dirlist_*.selected.bold=true
+
+ statusline_default.fg=#c0caf5
+ statusline_default.bg=#1f2335
+ statusline_error.fg=#f7768e
+ statusline_error.bold=true
+ statusline_warning.fg=#ff9e64
+ statusline_success.fg=#9ece6a
+ statusline_success.bold=true
+
+ completion_default.fg=#a9b1d6
+ completion_default.selected.bg=#292e42
+ completion_description.fg=#565f89
+ completion_gutter.bg=#1f2335
+ completion_pill.bg=#7aa2f7
+
+ keyhint_default.bg=#1f2335
+ keyhint_key.fg=#7dcfff
+ keyhint_key.bold=true
+ keyhint_desc.fg=#a9b1d6
+
+ part_switcher.bg=#1f2335
+ part_filename.fg=#7dcfff
+ part_mimetype.fg=#565f89
+ part_*.selected.bg=#292e42
+
+ selector_default.fg=#a9b1d6
+ selector_focused.fg=#c0caf5
+ selector_focused.bg=#292e42
+ selector_chooser.bold=true
+
+ [viewer]
+ url.fg=#7dcfff
+ url.underline=true
+ header.fg=#7aa2f7
+ header.bold=true
+ signature.fg=#565f89
+ signature.dim=true
+ diff_meta.fg=#c0caf5
+ diff_meta.bold=true
+ diff_chunk.fg=#7dcfff
+ diff_chunk_func.fg=#7dcfff
+ diff_chunk_func.bold=true
+ diff_add.fg=#9ece6a
+ diff_del.fg=#f7768e
+ diff_whitespace.bg=#f7768e
+ quote_1.fg=#7aa2f7
+ quote_2.fg=#bb9af7
+ quote_3.fg=#7dcfff
+ quote_4.fg=#565f89
+ quote_x.fg=#565f89
+ quote_x.dim=true
+ code.fg=#9ece6a
+ '';
+ };
+}
diff --git a/modules/home/default.nix b/modules/home/default.nix
index baa366e..2c4fbd3 100644
--- a/modules/home/default.nix
+++ b/modules/home/default.nix
@@ -2,6 +2,7 @@
{
imports = [
+ ./aerc.nix
./claude.nix
./git.nix
./ghostty.nix
@@ -29,6 +30,7 @@
};
home.packages = with pkgs; [
+ aerc
claude-code
dina.packages.${pkgs.stdenv.hostPlatform.system}.default
delta