aboutsummaryrefslogtreecommitdiffstats
path: root/modules/home/aerc.nix
blob: 3ad9e6b02d017d4af09530fe742943f360afbf78 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
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
    '';
  };
}