diff options
| author | Simen A. W. Olsen <hello@simenandre.no> | 2026-09-26 22:14:18 +0200 |
|---|---|---|
| committer | Simen A. W. Olsen <hello@simenandre.no> | 2026-09-26 22:14:18 +0200 |
| commit | efff2bf50109544391cf4721db21c264d151faf7 (patch) | |
| tree | 96b99c1f14df7dc016e6c9c29774f8bd5e3c565c /modules/home | |
| parent | 7c3e313cfec6c67b8ea15afe92d18896393eec59 (diff) | |
| download | dotfiles-efff2bf50109544391cf4721db21c264d151faf7.tar.gz dotfiles-efff2bf50109544391cf4721db21c264d151faf7.zip | |
feat(home): configure aerc as mail client
Diffstat (limited to 'modules/home')
| -rw-r--r-- | modules/home/aerc.nix | 445 | ||||
| -rw-r--r-- | modules/home/default.nix | 2 |
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 |
