diff --git a/bin/copy-message-link b/bin/copy-message-link new file mode 100755 index 0000000..0b95f12 --- /dev/null +++ b/bin/copy-message-link @@ -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: *//' | tr -d '\n\r') + +if [[ -n $message_id ]]; then + printf 'neomutt://%s' "$message_id" | ~/dotfiles/bin/cb +fi diff --git a/bin/nvim-vimtex-callback b/bin/nvim-vimtex-callback index 61d0e35..2ecab4f 100755 --- a/bin/nvim-vimtex-callback +++ b/bin/nvim-vimtex-callback @@ -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 diff --git a/bin/open-message-link b/bin/open-message-link new file mode 100755 index 0000000..e2aa6d3 --- /dev/null +++ b/bin/open-message-link @@ -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" </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 diff --git a/config/emacs/pre-early-init.el b/config/emacs/pre-early-init.el index 4d06fa9..452b232 100644 --- a/config/emacs/pre-early-init.el +++ b/config/emacs/pre-early-init.el @@ -28,10 +28,23 @@ ;; (setq default-frame-alist '((ns-appearance . dark) ;; (ns-transparent-titlebar . t))) -(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") - ":")) +;; 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" + (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))) diff --git a/config/neomutt/keybinds b/config/neomutt/keybinds index fed42ac..82af6f0 100644 --- a/config/neomutt/keybinds +++ b/config/neomutt/keybinds @@ -81,6 +81,9 @@ bind compose S pgp-menu macro compose a "~/dotfiles/bin/yazi-pick-filesource /tmp/neomutt-yazi-pick~/dotfiles/bin/yazi-pick-file clean" "Attach file with yazi" bind compose d detach-file +# Copy message link to clipboard (message://message-id format) +macro index,pager Y "~/dotfiles/bin/copy-message-link" "Copy message link" + # Mark messages bind index,pager m noop macro index,pager mu "unset mark_old" "Mark as unread" diff --git a/config/nvim/init.lua b/config/nvim/init.lua index 087e786..918313f 100644 --- a/config/nvim/init.lua +++ b/config/nvim/init.lua @@ -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 ''" -e "macro index q ''" -e "push \"id:%s\""]], + 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 '') + end + end + end, { buffer = true, desc = 'Open link (supports neomutt://)' }) + end, + }) +end) + now(function() require('mail-count').setup { accounts = { diff --git a/config/nvim/lua/vimtex.lua b/config/nvim/lua/vimtex.lua index e56d666..853ae5b 100644 --- a/config/nvim/lua/vimtex.lua +++ b/config/nvim/lua/vimtex.lua @@ -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', 'll', 'VimtexCompile', vim.tbl_extend('force', opts, { desc = 'Compile LaTeX' })) + -- Compilation - saves servername for inverse search + vim.keymap.set('n', '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', 'lk', 'VimtexStop', vim.tbl_extend('force', opts, { desc = 'Stop compilation' })) vim.keymap.set('n', 'lc', 'VimtexClean', vim.tbl_extend('force', opts, { desc = 'Clean aux files' })) vim.keymap.set('n', 'lC', 'VimtexClean!', vim.tbl_extend('force', opts, { desc = 'Clean all files' })) - -- View PDF (forward search) - save servername first for inverse search - vim.keymap.set('n', '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', 'lv', 'VimtexView', vim.tbl_extend('force', opts, { desc = 'View PDF (forward search)' })) -- TOC vim.keymap.set('n', 'lt', 'VimtexTocToggle', vim.tbl_extend('force', opts, { desc = 'Toggle TOC' })) diff --git a/config/sioyek/keys_user.config b/config/sioyek/keys_user.config index 8a3dcf3..e30f0ab 100644 --- a/config/sioyek/keys_user.config +++ b/config/sioyek/keys_user.config @@ -48,7 +48,8 @@ visual_mark_under_cursor v toggle_visual_scroll # Synctex (for LaTeX integration) -synctex_under_cursor +# F4 toggles synctex mode, then right-click on text jumps to source +toggle_synctex # Color mode toggle toggle_dark_mode diff --git a/config/sioyek/prefs_user.config b/config/sioyek/prefs_user.config index da2e717..029784f 100644 --- a/config/sioyek/prefs_user.config +++ b/config/sioyek/prefs_user.config @@ -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 diff --git a/config/zathura/zathurarc b/config/zathura/zathurarc index 12314f1..79785b3 100644 --- a/config/zathura/zathurarc +++ b/config/zathura/zathurarc @@ -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}" diff --git a/darwin/homebrew.nix b/darwin/homebrew.nix index ca3c65e..cd65c11 100644 --- a/darwin/homebrew.nix +++ b/darwin/homebrew.nix @@ -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" + ]; + }) ]; } diff --git a/hosts/dango/default.nix b/hosts/dango/default.nix index a22fae5..92948f8 100644 --- a/hosts/dango/default.nix +++ b/hosts/dango/default.nix @@ -67,6 +67,7 @@ valgrind = false; hammerspoon = true; aerospace = true; + skim = true; }; }; diff --git a/packages/minimal-emacs-d/030825-init-file-debug.patch b/packages/minimal-emacs-d/030825-init-file-debug.patch deleted file mode 100644 index bfb71a2..0000000 --- a/packages/minimal-emacs-d/030825-init-file-debug.patch +++ /dev/null @@ -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) diff --git a/packages/minimal-emacs-d/default.nix b/packages/minimal-emacs-d/default.nix index 6513a6a..7bde9da 100644 --- a/packages/minimal-emacs-d/default.nix +++ b/packages/minimal-emacs-d/default.nix @@ -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/"