feat: open neomutt message from nvim

This commit is contained in:
Ray Andrew 2025-12-12 12:19:35 -06:00
parent a23a3f8a50
commit 26386b2ea7
Signed by: rayandrew
SSH key fingerprint: SHA256:XYrYrxF0Z3A72n8P/p6mqPRNQZT22F88XcLsG+kX4xw
14 changed files with 193 additions and 70 deletions

9
bin/copy-message-link Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
# Extract Message-ID from email and copy as neomutt:// link to clipboard
# Used by neomutt macro - reads email from stdin
message_id=$(grep -i '^Message-ID:' | head -1 | sed 's/Message-ID: *<//i; s/>//' | tr -d '\n\r')
if [[ -n $message_id ]]; then
printf 'neomutt://%s' "$message_id" | ~/dotfiles/bin/cb
fi

View file

@ -1,15 +1,27 @@
#!/bin/bash
# VimTeX inverse search callback wrapper
# This script is called by sioyek for inverse search
# It uses nvr to connect to the running Neovim instance
# Inverse search callback for sioyek/vimtex/skim
# Usage: nvim-vimtex-callback LINE FILE
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVERNAME_FILE="/tmp/vimtex-servername"
# VimTeX calls: nvim --headless -c "VimtexInverseSearch LINE 'FILE'"
# Parse the VimtexInverseSearch command to extract LINE and FILE
# Then use nvr --remote +LINE FILE which is more reliable
# Read servername if available and server still exists
SERVERNAME_ARG=""
if [[ -f $SERVERNAME_FILE ]]; then
SERVERNAME=$(cat "$SERVERNAME_FILE")
if [[ -n $SERVERNAME && -e $SERVERNAME ]]; then
SERVERNAME_ARG="--servername $SERVERNAME"
fi
fi
# If first arg is a number, assume sioyek direct format: LINE FILE
if [[ $1 =~ ^[0-9]+$ ]]; then
LINE="$1"
FILE="$2"
exec "$SCRIPT_DIR/path-shim" nvr $SERVERNAME_ARG --remote-silent +"$LINE" "$FILE"
fi
# Otherwise parse VimTeX format: --headless -c "VimtexInverseSearch LINE 'FILE'"
CMD=""
while [[ $# -gt 0 ]]; do
case $1 in
@ -28,16 +40,9 @@ while [[ $# -gt 0 ]]; do
done
if [[ -n $CMD ]]; then
# Extract line number and file from: VimtexInverseSearch LINE 'FILE'
if [[ $CMD =~ VimtexInverseSearch\ ([0-9]+)\ \'(.+)\' ]]; then
LINE="${BASH_REMATCH[1]}"
FILE="${BASH_REMATCH[2]}"
if [[ -f $SERVERNAME_FILE ]]; then
SERVERNAME=$(cat "$SERVERNAME_FILE")
exec "$SCRIPT_DIR/path-shim" nvr --servername "$SERVERNAME" --remote-silent +"$LINE" "$FILE"
else
exec "$SCRIPT_DIR/path-shim" nvr --remote-silent +"$LINE" "$FILE"
fi
exec "$SCRIPT_DIR/path-shim" nvr $SERVERNAME_ARG --remote-silent +"$LINE" "$FILE"
fi
fi

56
bin/open-message-link Executable file
View file

@ -0,0 +1,56 @@
#!/bin/bash
# Open a neomutt:// link in neomutt via notmuch
# Usage: open-message-link [--current] neomutt://message-id
# --current: Open in current terminal (for nvim integration)
# Without flag: Opens in Ghostty on workspace 8 (like open-mail)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
set -euo pipefail
open_in_current=false
if [[ ${1:-} == "--current" ]]; then
open_in_current=true
shift
fi
url="${1:-}"
if [[ -z $url ]]; then
echo "Usage: open-message-link [--current] neomutt://message-id" >&2
exit 1
fi
# Extract message ID from URL
message_id="${url#neomutt://}"
notmuch_url="notmuch://?query=id:${message_id}"
if $open_in_current; then
exec neomutt -f "$notmuch_url"
else
# Create a temp script with PATH setup embedded
tmpscript=$(mktemp /tmp/open-mail-XXXXXX.sh)
cat >"$tmpscript" <<EOF
#!/bin/bash
export PATH="/etc/profiles/per-user/\$USER/bin:/run/current-system/sw/bin:\$PATH"
export NOTMUCH_CONFIG="\$HOME/.config/notmuch/config"
rm -f "$tmpscript"
exec neomutt -f '$notmuch_url'
EOF
chmod +x "$tmpscript"
if [[ "$(uname)" == "Darwin" ]]; then
aerospace workspace 8
open -na Ghostty --args --title="Mail" -e "$tmpscript"
else
# Linux (i3/sway)
if command -v swaymsg &>/dev/null; then
swaymsg workspace 8
ghostty --title="Mail" -e "$tmpscript" &
elif command -v i3-msg &>/dev/null; then
i3-msg workspace 8
ghostty --title="Mail" -e "$tmpscript" &
else
rm -f "$tmpscript"
exec neomutt -f "$notmuch_url"
fi
fi
fi

View file

@ -28,10 +28,23 @@
;; (setq default-frame-alist '((ns-appearance . dark)
;; (ns-transparent-titlebar . t)))
;; Use dynamic GCC paths to avoid recompilation when Homebrew updates GCC
(when-let* ((gcc-base "/opt/homebrew/opt/gcc/lib/gcc/current")
((file-directory-p gcc-base)))
(setenv "LIBRARY_PATH"
(mapconcat 'identity
'(
"/opt/homebrew/opt/gcc/lib/gcc/15"
"/opt/homebrew/opt/libgccjit/lib/gcc/15"
"/opt/homebrew/opt/gcc/lib/gcc/15/gcc/aarch64-apple-darwin24/15")
":"))
(string-join
(delq nil
(list gcc-base
(let ((jit-path "/opt/homebrew/opt/libgccjit/lib/gcc/current"))
(when (file-directory-p jit-path) jit-path))
;; Find the arch-specific directory (e.g., aarch64-apple-darwin24/15)
(car (last (file-expand-wildcards
(concat gcc-base "/gcc/aarch64-apple-darwin*/*"))))))
":")))
;; Native compilation settings to reduce unnecessary recompilation
(when (native-comp-available-p)
(setq native-comp-async-report-warnings-errors 'silent)
(setq native-compile-prune-cache t)
;; Redirect eln-cache to writable location (default goes to read-only ~/.emacs.d/)
(startup-redirect-eln-cache (expand-file-name "eln-cache/" minimal-emacs-var-dir)))

View file

@ -81,6 +81,9 @@ bind compose S pgp-menu
macro compose a "<shell-escape>~/dotfiles/bin/yazi-pick-file<enter><enter-command>source /tmp/neomutt-yazi-pick<enter><shell-escape>~/dotfiles/bin/yazi-pick-file clean<enter>" "Attach file with yazi"
bind compose d detach-file
# Copy message link to clipboard (message://message-id format)
macro index,pager Y "<pipe-message>~/dotfiles/bin/copy-message-link<enter>" "Copy message link"
# Mark messages
bind index,pager m noop
macro index,pager mu "<enter-command>unset mark_old<enter><tag-prefix><toggle-new><sync-mailbox>" "Mark as unread"

View file

@ -68,6 +68,52 @@ add 'EdenEast/nightfox.nvim'
now(function() require('vimtex').setup() end)
-- Message link handler for neomutt/notmuch integration
now(function()
local function open_neomutt_link(url)
local message_id = url:gsub('^neomutt://', '')
vim.notify('Opening: ' .. message_id, vim.log.levels.INFO)
-- Use vfolder-from-query then display the message (hide sidebar and index, q quits directly)
-- Note: nvim terminal doesn't support neomutt's directcolor, so we disable colors for clean display
local search_cmd = string.format(
[[neomutt -e "set color_directcolor=no sidebar_visible=no pager_index_lines=0" -e "color normal default default" -e "color hdrdefault default default" -e "color quoted default default" -e "color signature default default" -e "color attachment default default" -e "color header default default '.*'" -e "color body default default '.*'" -e "macro pager q '<exit><quit>'" -e "macro index q '<quit>'" -e "push \"<vfolder-from-query>id:%s<enter><display-message>\""]],
message_id
)
require('snacks').terminal(search_cmd, {
win = { position = 'bottom', height = 0.4 },
})
end
vim.api.nvim_create_user_command(
'OpenMessageLink',
function(opts) open_neomutt_link(opts.args) end,
{ nargs = 1, desc = 'Open email message link in neomutt' }
)
-- gx override: opens neomutt:// links, falls back to default for others
-- Use BufEnter to override markview's buffer-local gx mapping
vim.api.nvim_create_autocmd('BufEnter', {
pattern = '*.md',
callback = function()
vim.keymap.set('n', 'gl', function()
local line = vim.api.nvim_get_current_line()
local url = line:match 'neomutt://[^%)%s>]+'
if url then
open_neomutt_link(url)
else
-- Use markview's link opener or fallback
local ok, markview = pcall(require, 'markview')
if ok and markview.actions and markview.actions.openLink then
markview.actions.openLink()
else
vim.ui.open(vim.fn.expand '<cfile>')
end
end
end, { buffer = true, desc = 'Open link (supports neomutt://)' })
end,
})
end)
now(function()
require('mail-count').setup {
accounts = {

View file

@ -1,18 +1,14 @@
-- VimTeX configuration with Sioyek as PDF viewer
-- VimTeX configuration
local M = {}
function M.setup()
-- Use Sioyek as the PDF viewer
vim.g.vimtex_view_method = 'sioyek'
-- Sioyek - cross-platform PDF viewer with vim-like keybindings
vim.g.vimtex_view_method = 'general'
vim.g.vimtex_view_general_viewer = 'sioyek'
vim.g.vimtex_view_general_options = '--forward-search-file @tex --forward-search-line @line @pdf'
-- Sioyek executable (use 'sioyek' if in PATH)
vim.g.vimtex_view_sioyek_exe = 'sioyek'
-- Enable synctex for forward/inverse search
vim.g.vimtex_view_sioyek_sync = 1
-- Use custom wrapper script for inverse search (uses nvr instead of --headless)
vim.g.vimtex_callback_progpath = vim.fn.expand '~/dotfiles/bin/nvim-vimtex-callback'
-- Skim (macOS)
-- vim.g.vimtex_view_method = 'skim'
-- Compiler settings
vim.g.vimtex_compiler_method = 'latexmk'
@ -51,23 +47,21 @@ function M.setup()
callback = function()
local opts = { buffer = true, silent = true }
-- Compilation
vim.keymap.set('n', '<localleader>ll', '<cmd>VimtexCompile<cr>', vim.tbl_extend('force', opts, { desc = 'Compile LaTeX' }))
-- Compilation - saves servername for inverse search
vim.keymap.set('n', '<localleader>ll', function()
local f = io.open('/tmp/vimtex-servername', 'w')
if f then
f:write(vim.v.servername)
f:close()
end
vim.cmd 'VimtexCompile'
end, vim.tbl_extend('force', opts, { desc = 'Compile LaTeX' }))
vim.keymap.set('n', '<localleader>lk', '<cmd>VimtexStop<cr>', vim.tbl_extend('force', opts, { desc = 'Stop compilation' }))
vim.keymap.set('n', '<localleader>lc', '<cmd>VimtexClean<cr>', vim.tbl_extend('force', opts, { desc = 'Clean aux files' }))
vim.keymap.set('n', '<localleader>lC', '<cmd>VimtexClean!<cr>', vim.tbl_extend('force', opts, { desc = 'Clean all files' }))
-- View PDF (forward search) - save servername first for inverse search
vim.keymap.set('n', '<localleader>lv', function()
-- Save servername to file for inverse search callback
local servername = vim.v.servername
local f = io.open('/tmp/vimtex-servername', 'w')
if f then
f:write(servername)
f:close()
end
vim.cmd 'VimtexView'
end, vim.tbl_extend('force', opts, { desc = 'View PDF (forward search)' }))
-- View PDF (forward search)
vim.keymap.set('n', '<localleader>lv', '<cmd>VimtexView<cr>', vim.tbl_extend('force', opts, { desc = 'View PDF (forward search)' }))
-- TOC
vim.keymap.set('n', '<localleader>lt', '<cmd>VimtexTocToggle<cr>', vim.tbl_extend('force', opts, { desc = 'Toggle TOC' }))

View file

@ -48,7 +48,8 @@ visual_mark_under_cursor v
toggle_visual_scroll <F7>
# Synctex (for LaTeX integration)
synctex_under_cursor <C-click>
# F4 toggles synctex mode, then right-click on text jumps to source
toggle_synctex <F4>
# Color mode toggle
toggle_dark_mode <C-i>

View file

@ -35,9 +35,9 @@ page_separator_color 0.1569 0.2078 0.2431
ui_background_color 0.0157 0.0824 0.1255
ui_text_color 0.7451 0.8118 0.8549
# Startup - use custom color mode by default
# Startup - use custom color mode and enable synctex by default
should_launch_new_window 0
startup_commands toggle_custom_color
startup_commands toggle_custom_color;toggle_synctex
# Smooth scrolling
smooth_scroll_speed 3.0
@ -48,5 +48,8 @@ default_zoom_level 1.0
zoom_inc_factor 1.2
# Inverse search - click PDF to jump to Neovim source
# VimTeX passes --inverse-search with servername, this is fallback
inverse_search_command nvr --remote-silent +%2 %1
# %1 = filename, %2 = line number
inverse_search_command /Users/rayandrew/dotfiles/bin/nvim-vimtex-callback %2 %1
# Control+click triggers synctex inverse search
control_click_command synctex_under_cursor

View file

@ -43,3 +43,7 @@ set recolor-darkcolor "#becfda"
# Selection settings
set selection-clipboard clipboard
set selection-notification true
# Synctex inverse search (Ctrl+click in PDF jumps to source)
set synctex true
set synctex-editor-command "~/dotfiles/bin/path-shim nvr --remote-silent +%{line} %{input}"

View file

@ -66,6 +66,7 @@
valgrind = mkEnableOption "Enable valgrind";
hammerspoon = mkEnableOption "Enable hammerspoon";
aerospace = mkEnableOption "Enable aerospace";
skim = mkEnableOption "Enable skim";
};
config = lib.mkMerge [
@ -389,5 +390,10 @@
"aerospace"
];
})
(lib.mkIf config.custom.brew.skim {
homebrew.casks = [
"skim"
];
})
];
}

View file

@ -67,6 +67,7 @@
valgrind = false;
hammerspoon = true;
aerospace = true;
skim = true;
};
};

View file

@ -1,13 +0,0 @@
diff --git a/early-init.el b/early-init.el
index dfea2f0..d60b6ef 100644
--- a/early-init.el
+++ b/early-init.el
@@ -32,7 +32,7 @@ turned on.")
(defvar minimal-emacs-frame-title-format "%b Emacs"
"Template for displaying the title bar of visible and iconified frame.")
-(defvar minimal-emacs-debug (bound-and-true-p init-file-debug)
+(defvar minimal-emacs-debug nil
"Non-nil to enable debug.")
(defvar minimal-emacs-gc-cons-threshold (* 16 1024 1024)

View file

@ -14,15 +14,10 @@ stdenv.mkDerivation {
src = pkgs.fetchFromGitHub {
owner = "jamescherti";
repo = "minimal-emacs.d";
rev = "e44aa459d5eb5af2f868dc490e4d05efca308915";
# rev = "08f077545a0f45a1701333406fd1afe8be77a752";
sha256 = "sha256-ABHv+TUQpBoXkg75iL2ROJoGjT+iUZQHZD9b4Z8Q4kQ=";
rev = "4b43566ec9bc9b1a68e288b955d673d0ab68ddd9";
sha256 = "sha256-Z4NgOrwNXuQKIEh4Z/H324sK3zVY3AdxwOUFUSkXta4=";
};
patches = [
./030825-init-file-debug.patch
];
installPhase = ''
mkdir $out
cp -r * "$out/"