From 53dc8c95e525f922461dfc1768121ee02380e4ec Mon Sep 17 00:00:00 2001 From: Arpit Chakladar Date: Wed, 5 Aug 2026 17:58:58 +0530 Subject: feat: Rewrote neomutt keybindings, updated comments - Rewriting the neomutt keybindings by hand - Updated comments to be simpler --- modules/communication/default.nix | 2 +- modules/communication/neomutt/account/default.nix | 2 +- .../neomutt/account/flavors/gmail.nix | 2 +- modules/communication/neomutt/account/options.nix | 2 +- modules/communication/neomutt/assertions.nix | 2 +- modules/communication/neomutt/default.nix | 2 +- modules/communication/neomutt/keybindings.nix | 816 ++++++++++++--------- modules/communication/neomutt/macros.nix | 30 +- modules/communication/neomutt/neomutt.nix | 4 +- modules/communication/neomutt/sync/default.nix | 2 +- modules/communication/neomutt/sync/mbsync.nix | 2 +- modules/communication/neomutt/sync/notmuch.nix | 2 +- 12 files changed, 490 insertions(+), 378 deletions(-) (limited to 'modules/communication') diff --git a/modules/communication/default.nix b/modules/communication/default.nix index 4dc805b..70afc8d 100644 --- a/modules/communication/default.nix +++ b/modules/communication/default.nix @@ -1,4 +1,4 @@ -# Communication - Email client modules +# Email client modules { ... }: { imports = [ diff --git a/modules/communication/neomutt/account/default.nix b/modules/communication/neomutt/account/default.nix index e6e60d2..f0b1cb1 100644 --- a/modules/communication/neomutt/account/default.nix +++ b/modules/communication/neomutt/account/default.nix @@ -1,4 +1,4 @@ -# Account - Converts neomutt account options into home-manager email config +# Converts neomutt account options into home-manager email config { config, lib, ... }: { options.communication.neomutt.accounts = lib.mkOption { diff --git a/modules/communication/neomutt/account/flavors/gmail.nix b/modules/communication/neomutt/account/flavors/gmail.nix index 28ec979..ba39c58 100644 --- a/modules/communication/neomutt/account/flavors/gmail.nix +++ b/modules/communication/neomutt/account/flavors/gmail.nix @@ -1,4 +1,4 @@ -# Gmail - Gmail-specific defaults for folder names, mbsync patterns, and mailboxes +# Gmail-specific defaults for folder names, mbsync patterns, and mailboxes { config, lib, diff --git a/modules/communication/neomutt/account/options.nix b/modules/communication/neomutt/account/options.nix index 3dd6c9f..45e6744 100644 --- a/modules/communication/neomutt/account/options.nix +++ b/modules/communication/neomutt/account/options.nix @@ -1,4 +1,4 @@ -# Options - Email account option definitions (realName, address, folders, gpg, etc.) +# Email account option definitions { lib, ... }: { options = { diff --git a/modules/communication/neomutt/assertions.nix b/modules/communication/neomutt/assertions.nix index 5fad5c9..aa28502 100644 --- a/modules/communication/neomutt/assertions.nix +++ b/modules/communication/neomutt/assertions.nix @@ -1,4 +1,4 @@ -# Assertions - Validates neomutt dependencies (less, bat, kitty, chawan) are enabled +# Validates neomutt dependencies are enabled { config, ... }: { assertions = [ diff --git a/modules/communication/neomutt/default.nix b/modules/communication/neomutt/default.nix index 1512465..6d882e1 100644 --- a/modules/communication/neomutt/default.nix +++ b/modules/communication/neomutt/default.nix @@ -1,4 +1,4 @@ -# Neomutt - Email suite entry point (neomutt + mbsync + notmuch) +# Email suite entry point { config, lib, diff --git a/modules/communication/neomutt/keybindings.nix b/modules/communication/neomutt/keybindings.nix index 59712fa..e9a3490 100644 --- a/modules/communication/neomutt/keybindings.nix +++ b/modules/communication/neomutt/keybindings.nix @@ -1,359 +1,485 @@ -# Keybindings - Vim-style keyboard shortcuts for neomutt -# -# Covers the core menus: generic (fallback), index, pager, sidebar, -# attach, browser, compose. Menus like pgp/smime/mix/postpone/alias/query -# are intentionally left mostly on defaults - they're rarely touched and -# not worth risking an incorrect binding for. Press `?` inside neomutt -# in any menu to see the live, authoritative list of bound functions. { config, lib, ... }: + { - config.programs.neomutt.binds = lib.mkIf config.communication.neomutt.enable [ - # --- sidebar navigation (vim: Ctrl-k=up, Ctrl-j=down) --- - { - map = [ - "index" - "pager" - ]; - key = "\\Ck"; - action = "sidebar-prev"; - } - { - map = [ - "index" - "pager" - ]; - key = "\\Cj"; - action = "sidebar-next"; - } - { - # jump into the highlighted mailbox - map = [ - "index" - "pager" - ]; - key = "\\Co"; - action = "sidebar-open"; - } - { - map = [ - "index" - "pager" - ]; - key = "\\Cp"; - action = "sidebar-prev-new"; - } - { - map = [ - "index" - "pager" - ]; - key = "\\Cn"; - action = "sidebar-next-new"; - } - { - # show/hide the sidebar column entirely - map = [ - "index" - "pager" - ]; - key = "B"; - action = "sidebar-toggle-visible"; - } + config.programs.neomutt.binds = lib.mkIf config.communication.neomutt.enable ( + lib.mkMerge [ + # Force noop bindings to the top of neomuttrc + (lib.mkBefore [ + { + map = [ + "generic" + "index" + "pager" + "browser" + "attach" + "compose" + ]; + key = "z"; + action = "noop"; + } + { + map = [ + "generic" + "index" + "pager" + "browser" + "attach" + "compose" + ]; + key = "d"; + action = "noop"; + } + { + map = [ + "generic" + "index" + "pager" + "browser" + "attach" + "compose" + ]; + key = "g"; + action = "noop"; + } + ]) - # --- basic entry navigation (generic = fallback for every list-style menu) --- - { - map = [ "generic" ]; - key = "j"; - action = "next-entry"; - } - { - map = [ "generic" ]; - key = "k"; - action = "previous-entry"; - } - { - map = [ "generic" ]; - key = "gg"; - action = "first-entry"; - } - { - map = [ - "index" - "generic" - ]; - key = "G"; - action = "last-entry"; - } + # Other bindings + [ + # Sidebar navigation + { + map = [ + "index" + "pager" + ]; + key = "\\Ck"; + action = "sidebar-prev"; + } + { + map = [ + "index" + "pager" + ]; + key = "\\Cj"; + action = "sidebar-next"; + } + { + map = [ + "index" + "pager" + ]; + key = "\\Co"; + action = "sidebar-open"; + } + { + map = [ + "index" + "pager" + ]; + key = "\\Cn"; + action = "sidebar-toggle-visible"; + } - # --- scroll-to-position, straight from vim (zt/zz/zb) --- - { - map = [ "generic" ]; - key = "zt"; - action = "current-top"; - } - { - map = [ "generic" ]; - key = "zz"; - action = "current-middle"; - } - { - map = [ "generic" ]; - key = "zb"; - action = "current-bottom"; - } + # Basic movement + { + map = [ "generic" ]; + key = "j"; + action = "next-entry"; + } + { + map = [ "generic" ]; + key = "k"; + action = "previous-entry"; + } + { + map = [ "generic" ]; + key = "gg"; + action = "first-entry"; + } + { + map = [ + "index" + "generic" + ]; + key = "G"; + action = "last-entry"; + } + { + map = [ "pager" ]; + key = "h"; + action = "exit"; + } + { + map = [ "pager" ]; + key = "l"; + action = "view-attachments"; + } + { + map = [ "index" ]; + key = "l"; + action = "display-message"; + } - # --- pager scrolling --- - { - map = [ "pager" ]; - key = "j"; - action = "next-line"; - } - { - map = [ "pager" ]; - key = "k"; - action = "previous-line"; - } - { - map = [ "pager" ]; - key = "gg"; - action = "top"; - } - { - map = [ "pager" ]; - key = "G"; - action = "bottom"; - } - { - map = [ - "generic" - "index" - "pager" - ]; - key = "\\Cd"; - action = "half-down"; - } - { - map = [ - "generic" - "index" - "pager" - ]; - key = "\\Cu"; - action = "half-up"; - } - { - map = [ - "generic" - "index" - "pager" - ]; - key = "\\Cf"; - action = "next-page"; - } - { - # NOTE: Ctrl-B is deliberately left unbound in the pager map here - - # macros.nix claims it there for the chawan browser-view macro. - map = [ "index" ]; - key = "\\Cb"; - action = "previous-page"; - } + # Scrolling + { + map = [ "generic" ]; + key = "zt"; + action = "current-top"; + } + { + map = [ "generic" ]; + key = "zz"; + action = "current-middle"; + } + { + map = [ "generic" ]; + key = "zb"; + action = "current-bottom"; + } + { + map = [ "pager" ]; + key = "j"; + action = "next-line"; + } + { + map = [ "pager" ]; + key = "k"; + action = "previous-line"; + } + { + map = [ "pager" ]; + key = "gg"; + action = "top"; + } + { + map = [ "pager" ]; + key = "G"; + action = "bottom"; + } + { + map = [ + "generic" + "index" + "pager" + ]; + key = "\\Cd"; + action = "half-down"; + } + { + map = [ + "generic" + "index" + "pager" + ]; + key = "\\Cu"; + action = "half-up"; + } + { + map = [ + "generic" + "index" + "pager" + ]; + key = "\\Cf"; + action = "next-page"; + } + { + map = [ "index" ]; + key = "\\Cb"; + action = "previous-page"; + } - # --- h/l as "back / go into", vim left/right --- - { - map = [ "pager" ]; - key = "h"; - action = "exit"; - } - { - map = [ "pager" ]; - key = "l"; - action = "view-attachments"; - } - { - map = [ "index" ]; - key = "l"; - action = "display-message"; - } + # Searching and limiting + { + map = [ + "generic" + "index" + "pager" + ]; + key = "/"; + action = "search"; + } + { + map = [ + "generic" + "index" + "pager" + ]; + key = "?"; + action = "search-reverse"; + } + { + map = [ + "generic" + "index" + "pager" + ]; + key = "n"; + action = "search-next"; + } + { + map = [ + "generic" + "index" + "pager" + ]; + key = "N"; + action = "search-opposite"; + } + { + map = [ "index" ]; + key = "gf"; + action = "limit"; + } - # --- search (mostly default already, restated for a complete reference) --- - { - map = [ - "generic" - "index" - "pager" - ]; - key = "/"; - action = "search"; - } - { - map = [ - "generic" - "index" - "pager" - ]; - key = "?"; - action = "search-reverse"; - } - { - map = [ - "generic" - "index" - "pager" - ]; - key = "n"; - action = "search-next"; - } - { - map = [ - "generic" - "index" - "pager" - ]; - key = "N"; - action = "search-opposite"; - } + # Message states and threads + { + map = [ "index" ]; + key = "]"; + action = "next-unread"; + } + { + map = [ "index" ]; + key = "["; + action = "previous-unread"; + } + { + map = [ "index" ]; + key = "}"; + action = "next-new"; + } + { + map = [ "index" ]; + key = "{"; + action = "previous-new"; + } + { + map = [ "index" ]; + key = "za"; + action = "collapse-thread"; + } + { + map = [ "index" ]; + key = "zA"; + action = "collapse-all"; + } - # --- delete / undelete, vim dd --- - { - map = [ - "index" - "pager" - ]; - key = "dd"; - action = "delete-message"; - } - { - map = [ "index" ]; - key = "dT"; - action = "delete-thread"; - } - { - map = [ "index" ]; - key = "u"; - action = "undelete-message"; - } - { - map = [ "index" ]; - key = "U"; - action = "undelete-thread"; - } - { - map = [ "attach" ]; - key = "dd"; - action = "delete-entry"; - } - { - map = [ "attach" ]; - key = "u"; - action = "undelete-entry"; - } + # Core email actions + { + map = [ + "index" + "pager" + ]; + key = "m"; + action = "mail"; + } + { + map = [ + "index" + "pager" + ]; + key = "r"; + action = "reply"; + } + { + map = [ + "index" + "pager" + ]; + key = "R"; + action = "group-reply"; + } + { + map = [ + "index" + "pager" + ]; + key = "f"; + action = "forward-message"; + } + { + map = [ + "index" + "pager" + ]; + key = "c"; + action = "change-folder"; + } + { + map = [ + "index" + "pager" + ]; + key = "s"; + action = "save-message"; + } + { + map = [ + "index" + "pager" + ]; + key = "y"; + action = "copy-message"; + } + { + map = [ "index" ]; + key = "t"; + action = "tag-entry"; + } + { + map = [ "pager" ]; + key = "t"; + action = "tag-message"; + } + { + map = [ "index" ]; + key = "T"; + action = "tag-thread"; + } + { + map = [ + "index" + "pager" + ]; + key = "gh"; + action = "display-toggle-weed"; + } - # --- limit / filter the index, gf = filter, gF = clear filter --- - { - # prompts for a limit pattern; macros.nix binds gF to clear it - map = [ "index" ]; - key = "gf"; - action = "limit"; - } + # Deletion and undeletion + { + map = [ + "index" + "pager" + ]; + key = "dd"; + action = "delete-message"; + } + { + map = [ "index" ]; + key = "dT"; + action = "delete-thread"; + } + { + map = [ + "index" + "pager" + ]; + key = "u"; + action = "undelete-message"; + } + { + map = [ "index" ]; + key = "U"; + action = "undelete-thread"; + } + { + map = [ "attach" ]; + key = "dd"; + action = "delete-entry"; + } + { + map = [ "attach" ]; + key = "u"; + action = "undelete-entry"; + } - # --- unread / new / flagged navigation --- - { - map = [ "index" ]; - key = "]"; - action = "next-unread"; - } - { - map = [ "index" ]; - key = "["; - action = "previous-unread"; - } - { - map = [ "index" ]; - key = "}"; - action = "next-new"; - } - { - map = [ "index" ]; - key = "{"; - action = "previous-new"; - } - { - map = [ "index" ]; - key = "gn"; - action = "next-flagged"; - } - { - map = [ "index" ]; - key = "gp"; - action = "previous-flagged"; - } + # Menu specific + { + map = [ "attach" ]; + key = "l"; + action = "view-attach"; + } + { + map = [ "attach" ]; + key = "s"; + action = "save-entry"; + } + { + map = [ "attach" ]; + key = "p"; + action = "print-entry"; + } - # --- attach menu --- - { - map = [ "attach" ]; - key = "l"; - action = "view-attach"; - } - { - map = [ "attach" ]; - key = "s"; - action = "save-entry"; - } - { - map = [ "attach" ]; - key = "p"; - action = "print-entry"; - } + { + map = [ "browser" ]; + key = "j"; + action = "next-entry"; + } + { + map = [ "browser" ]; + key = "k"; + action = "previous-entry"; + } + { + map = [ "browser" ]; + key = "gg"; + action = "first-entry"; + } + { + map = [ "browser" ]; + key = "G"; + action = "last-entry"; + } + { + map = [ "browser" ]; + key = "l"; + action = "select-entry"; + } + { + map = [ "browser" ]; + key = "\\Cd"; + action = "half-down"; + } + { + map = [ "browser" ]; + key = "\\Cu"; + action = "half-up"; + } - # --- browser (folder / file picker) --- - { - map = [ "browser" ]; - key = "j"; - action = "next-entry"; - } - { - map = [ "browser" ]; - key = "k"; - action = "previous-entry"; - } - { - map = [ "browser" ]; - key = "gg"; - action = "first-entry"; - } - { - map = [ "browser" ]; - key = "G"; - action = "last-entry"; - } - { - # neomutt has no dedicated "go up a directory" function - the ".." - # row is a normal entry, so l/Enter on it goes up just fine - map = [ "browser" ]; - key = "l"; - action = "select-entry"; - } - { - map = [ "browser" ]; - key = "\\Cd"; - action = "half-down"; - } - { - map = [ "browser" ]; - key = "\\Cu"; - action = "half-up"; - } + { + map = [ "compose" ]; + key = "j"; + action = "next-entry"; + } + { + map = [ "compose" ]; + key = "k"; + action = "previous-entry"; + } + { + map = [ "compose" ]; + key = "y"; + action = "send-message"; + } + { + map = [ "compose" ]; + key = "a"; + action = "attach-file"; + } + { + map = [ "compose" ]; + key = "e"; + action = "edit-message"; + } + { + map = [ "compose" ]; + key = "p"; + action = "postpone-message"; + } - # --- compose menu --- - { - map = [ "compose" ]; - key = "j"; - action = "next-entry"; - } - { - map = [ "compose" ]; - key = "k"; - action = "previous-entry"; - } - ]; + # Global exit + { + map = [ + "generic" + "index" + "pager" + "browser" + "attach" + "compose" + ]; + key = "q"; + action = "exit"; + } + ] + ] + ); } diff --git a/modules/communication/neomutt/macros.nix b/modules/communication/neomutt/macros.nix index 4a1fd5d..9ad3593 100644 --- a/modules/communication/neomutt/macros.nix +++ b/modules/communication/neomutt/macros.nix @@ -1,10 +1,9 @@ -# Macros - neomutt macros for sync, URL extraction, flag legend, and quit +# neomutt macros for sync, URL extraction, flag legend, and quit { config, lib, ... }: { config.programs.neomutt.macros = lib.mkIf config.communication.neomutt.enable [ { - # sync the mailbox and run the external sync script, silencing the - # "press any key" prompt around it. Vim mnemonic: g-prefixed, "go sync". + # Sync the mailbox and run the external sync script map = [ "index" "pager" @@ -13,9 +12,7 @@ action = "set my_wait_key=$wait_key wait_key=no${lib.getExe config.scripts.neomutt-sync.package}set wait_key=$my_wait_key"; } { - # extract every URL from the message via urlscan into a picker menu, - # then follow the selected one. Vim mnemonic: gx, same as vim-plugins - # that open the link/file under the cursor. + # Extract every URL from the message via urlscan into a picker menu map = [ "index" "pager" @@ -24,42 +21,31 @@ action = "urlscan"; } { - # open the message body in interactive chawan -- real vim-style - # cursor navigation (hjkl, Tab/Shift-Tab between links, Enter to - # follow, B back, q to return to neomutt). Replaces the old - # urlscan-based link picker. - # Kept on Ctrl-B since that's common muscle-memory for "open this in - # a browser-like view" - previous-page is intentionally NOT bound to - # Ctrl-B in the pager (see binds.nix) so this doesn't get shadowed. + # Open the message body in interactive chawan with vim-style navigation map = [ "pager" ]; key = "\\Cb"; action = "set my_pipe_decode=$pipe_decode pipe_decode=yes${lib.getExe config.web.chawan.package} -o \"title='neomutt'\"set pipe_decode=$my_pipe_decode"; } { - # on a specific attachment (not just the top-level - # message): pipe-entry sends the selected attachment instead. - # Same gx mnemonic as the index/pager macro above. + # Pipe the selected attachment instead of the whole message map = [ "attach" ]; key = "gx"; action = "${lib.getExe config.web.chawan.package} -o \"title='neomutt'\" "; } { - # clear the current limit/filter and show every message again, - # the counterpart to gf (interactive limit prompt) in binds.nix + # Clear the current limit and show every message again map = [ "index" ]; key = "gF"; action = "all"; } { - # save and quit, vim-style (sync mailbox, then quit) - same ZZ - # you'd type in vim to write and exit + # Save the mailbox and quit map = [ "index" ]; key = "ZZ"; action = ""; } { - # quit without saving, vim-style counterpart to ZZ - same ZQ - # you'd type in vim to discard and exit + # Quit without saving map = [ "index" ]; key = "ZQ"; action = ""; diff --git a/modules/communication/neomutt/neomutt.nix b/modules/communication/neomutt/neomutt.nix index 997c948..e25d0b3 100644 --- a/modules/communication/neomutt/neomutt.nix +++ b/modules/communication/neomutt/neomutt.nix @@ -1,4 +1,4 @@ -# neomutt - Terminal email client +# Terminal email client { config, lib, @@ -44,7 +44,7 @@ }; sidebar.enable = true; sort = "reverse-threads"; - vimKeys = true; + vimKeys = false; unmailboxes = true; checkStatsInterval = 20; extraConfig = builtins.readFile ./.neomuttrc; diff --git a/modules/communication/neomutt/sync/default.nix b/modules/communication/neomutt/sync/default.nix index 2fcaa18..629a655 100644 --- a/modules/communication/neomutt/sync/default.nix +++ b/modules/communication/neomutt/sync/default.nix @@ -1,4 +1,4 @@ -# Sync - Mail synchronization modules (mbsync, notmuch) +# Mail synchronization modules { ... }: { imports = [ diff --git a/modules/communication/neomutt/sync/mbsync.nix b/modules/communication/neomutt/sync/mbsync.nix index 7642099..28b7b2c 100644 --- a/modules/communication/neomutt/sync/mbsync.nix +++ b/modules/communication/neomutt/sync/mbsync.nix @@ -1,4 +1,4 @@ -# Mbsync - Mailbox synchronization (isync/mbsync) +# Mailbox synchronization { config, lib, ... }: { config = lib.mkIf config.communication.neomutt.enable { diff --git a/modules/communication/neomutt/sync/notmuch.nix b/modules/communication/neomutt/sync/notmuch.nix index 277b232..6f806fb 100644 --- a/modules/communication/neomutt/sync/notmuch.nix +++ b/modules/communication/neomutt/sync/notmuch.nix @@ -1,4 +1,4 @@ -# Notmuch - Mail indexer for fast email search +# Mail indexer for fast email search { config, lib, ... }: { config = lib.mkIf config.communication.neomutt.enable { -- cgit v1.2.3