From efff2bf50109544391cf4721db21c264d151faf7 Mon Sep 17 00:00:00 2001 From: "Simen A. W. Olsen" Date: Sat, 26 Sep 2026 22:14:18 +0200 Subject: feat(home): configure aerc as mail client --- modules/home/aerc.nix | 445 +++++++++++++++++++++++++++++++++++++++++++++++ modules/home/default.nix | 2 + 2 files changed, 447 insertions(+) create mode 100644 modules/home/aerc.nix (limited to 'modules') 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 < + + + + + + + + $body + + + 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. + + = :prev-tab + = :next-tab + \[t = :prev-tab # Previous tab + \]t = :next-tab # Next tab + = :term + ? = :help keys + = :prompt 'Quit?' quit + = :suspend + + [messages] + q = :prompt 'Quit?' quit + . = :repeat + + j = :next + = :next + k = :prev + = :prev + = :next 50% + = :prev 50% + = :next 100% + = :prev 100% + = :next 100% + = :prev 100% + gg = :select 0 # First message + G = :select -1 + zz = :align center + zt = :align top + zb = :align bottom + + J = :next-folder + K = :prev-folder + gi = :cf INBOX # Inbox + ga = :cf Archive # Archive + gd = :cf Drafts # Drafts + gs = :cf Sent # Sent + gj = :cf Junk # Junk + gt = :cf Trash # Trash + gf = :menu -dc fzf :cf # Find folder (fzf) + c = :cf + + = :view + v = :mark -t + = :mark -t:next + V = :mark -v + tt = :toggle-threads # Threads + ts = :toggle-sidebar # Sidebar + tf = :toggle-folder # Folder subscription + tc = :toggle-thread-context # Thread context + zc = :fold + zo = :unfold + za = :fold -t + zM = :fold -a + zR = :unfold -a + = :fold -t + + C = :compose + r = :reply -aq + R = :reply -q + f = :forward + b = :bounce + d = :choose -o y 'Really delete this message' delete-message + D = :delete + a = :archive flat + A = :unmark -a:mark -T:archive flat + M = :move + mm = :move # Move to folder + mc = :copy # Copy to folder + mr = :read # Mark read + mu = :unread # Mark unread + mf = :flag -t -x flagged # Toggle flagged + mU = :unsubscribe # Unsubscribe from list + s = :split + S = :vsplit + | = :pipe + $ = :term + + / = :search + \ = :filter + n = :next-result + N = :prev-result + = :clear + = :clear + Fu = :filter -u # Unread + Ff = :filter -x flagged # Flagged + Fs = :filter -f # From sender + Ft = :filter -t # To recipient + Fd = :filter -d # Date range + Fc = :clear # Clear filter + F/ = :filter # Filter prompt + + [messages:folder=Drafts] + = :recall + + [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 + / = :toggle-key-passthrough/ + q = :close + = :close + o = :open + O = :open + S = :save + s = :save -fpa ~/Downloads + | = :pipe + d = :choose -o y 'Really delete this message' delete-message + D = :delete + a = :archive flat + f = :forward + r = :reply -aq + R = :reply -q + H = :toggle-headers + th = :toggle-headers # Headers + = :next-part + = :next-part + = :prev-part + = :prev-part + \]p = :next-part # Next part + \[p = :prev-part # Previous part + \]m = :next # Next message + \[m = :prev # Previous message + J = :next + K = :prev + gx = :menu -lc fzf :open-link # Open link (fzf) + gy = :copy-link + + [view::passthrough] + $noinherit = true + $ex = + = :toggle-key-passthrough + + [compose] + $noinherit = true + $ex = + $complete = + = :next-field + = :prev-field + = :next-field + = :next-field + = :prev-field + = :prev-field + = :next-field + = :prev-field + = :switch-account -p + = :switch-account -n + = :prev-tab + = :next-tab + + [compose::editor] + $noinherit = true + $ex = + = :next-field + = :next-field + = :prev-field + = :prev-field + = :next-field + = :prev-field + = :prev-tab + = :next-tab + + [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:send # Send (HTML + plain text) + Y = :send # Send plain text only + h = :multipart text/html # Add HTML alternative + H = :multipart -d text/html # Drop HTML alternative + n = :abort # Abort (discard message, no confirmation) + s = :sign # Toggle signing + x = :encrypt # Toggle encryption to all recipients + v = :preview # Preview message + p = :postpone # Postpone + q = :choose -o d discard abort -o p postpone postpone # Abort or postpone + e = :edit # Edit (body and headers) + a = :attach # Add attachment + A = :menu -c 'fzf --multi' :attach # Add attachment (fzf) + d = :detach # Remove attachment + + [terminal] + $noinherit = true + $ex = + = :prev-tab + = :next-tab + ''; + + # 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 -- cgit v1.3.1