From 96610b7ed0a63552dca1cbbb58644c7a1d075045 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 26 May 2026 11:57:43 +0200 Subject: [PATCH] feat(nvim,mail): floating PIM windows + notmuch/alot setup script - Replace tab/vsplit PIM approach with nvim_open_win floating windows: r opens a tiled full-screen overlay (alot top, calendar+abook bottom) n/g/f open individual centered floats with rounded border - Add setup/modules/optional-Modules/apps/mail-notmuch.sh: configures mbsync, msmtp, notmuch, alot from interactive prompts installs a systemd user timer for 5-min periodic mail sync Co-Authored-By: Claude Sonnet 4.6 --- nvim/init.lua | 75 ++++++---- .../optional-Modules/apps/mail-notmuch.sh | 139 ++++++++++++++++++ 2 files changed, 182 insertions(+), 32 deletions(-) create mode 100755 setup/modules/optional-Modules/apps/mail-notmuch.sh diff --git a/nvim/init.lua b/nvim/init.lua index f7ce487..4f6b595 100644 --- a/nvim/init.lua +++ b/nvim/init.lua @@ -159,7 +159,7 @@ vim.keymap.set("i", "", function() return vim.fn["coc#pum#visible"]() == 1 and vim.fn["coc#pum#confirm"]() or "" end, { expr = true, silent = true }) --- ── PIM sidebar (calendar / notmuch / abook) ────────────────────────────────── +-- ── PIM floating windows (calendar / alot / abook) ─────────────────────────── local function _pim_scratch(label, err) vim.cmd("enew") vim.bo.buftype = "nofile" @@ -176,62 +176,73 @@ local function _pim_close_win(win) end end --- individual toggles: n = notmuch/alot, g = calendar, f = find/abook +local function _pim_float(row, col, height, width, border) + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_open_win(buf, true, { + relative = "editor", + row = row, col = col, + height = height, width = width, + style = "minimal", + border = border or "none", + zindex = 50, + }) + return vim.api.nvim_get_current_win() +end + +-- n/g/f: individual centered floating windows local _solo = {} -local function toggle_solo(key, cmd, label, width) +local function toggle_solo(key, cmd, label) local win = _solo[key] if win and vim.api.nvim_win_is_valid(win) then _pim_close_win(win) _solo[key] = nil - else - local prev = vim.api.nvim_get_current_win() - vim.cmd("topleft vsplit") - vim.api.nvim_win_set_width(0, width) - local ok, err = pcall(vim.cmd, cmd) - if not ok then _pim_scratch(label, err) end - _solo[key] = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(prev) + return end + local h = math.floor(vim.o.lines * 0.85) + local w = math.floor(vim.o.columns * 0.85) + local r = math.floor((vim.o.lines - h - 2) / 2) + local c = math.floor((vim.o.columns - w - 2) / 2) + local win_id = _pim_float(r, c, h, w, "rounded") + local ok, err = pcall(vim.cmd, cmd) + if not ok then _pim_scratch(label, err) end + _solo[key] = win_id end -vim.keymap.set("n", "n", function() toggle_solo("n", "terminal alot", "alot", 55) end, { silent = true }) -vim.keymap.set("n", "g", function() toggle_solo("g", "Calendar", "calendar.vim", 35) end, { silent = true }) -vim.keymap.set("n", "f", function() toggle_solo("f", "terminal abook", "abook", 80) end, { silent = true }) +vim.keymap.set("n", "n", function() toggle_solo("n", "terminal alot", "alot") end, { silent = true }) +vim.keymap.set("n", "g", function() toggle_solo("g", "Calendar", "calendar.vim") end, { silent = true }) +vim.keymap.set("n", "f", function() toggle_solo("f", "terminal abook", "abook") end, { silent = true }) --- r = full-screen PIM tab (mail top, calendar bottom-left, abook bottom-right) -local _pim_tab = nil +-- r: full-screen tiled overlay (mail top, calendar bottom-left, abook bottom-right) +local _pim_wins = {} local function toggle_pim() - if _pim_tab and vim.api.nvim_tabpage_is_valid(_pim_tab) then - vim.cmd("tabclose " .. vim.api.nvim_tabpage_get_number(_pim_tab)) - _pim_tab = nil + if #_pim_wins > 0 and vim.api.nvim_win_is_valid(_pim_wins[1]) then + for _, w in ipairs(_pim_wins) do _pim_close_win(w) end + _pim_wins = {} return end - vim.cmd("tabnew") - _pim_tab = vim.api.nvim_get_current_tabpage() + local H = vim.o.lines - 2 + local W = vim.o.columns + local mail_h = math.floor(H * 0.6) + local bot_h = H - mail_h + local cal_w = math.floor(W * 0.5) - -- top pane: alot (mail), full width + local w1 = _pim_float(0, 0, mail_h, W) local ok, err = pcall(vim.cmd, "terminal alot") if not ok then _pim_scratch("alot", err) end - -- split bottom third for calendar + abook - vim.cmd("belowright split") - vim.cmd("resize 24") - - -- bottom-left: calendar - vim.cmd("leftabove vsplit") + local w2 = _pim_float(mail_h, 0, bot_h, cal_w) ok, err = pcall(vim.cmd, "Calendar") if not ok then _pim_scratch("calendar.vim", err) end - -- bottom-right: abook - vim.cmd("wincmd l") + local w3 = _pim_float(mail_h, cal_w, bot_h, W - cal_w) ok, err = pcall(vim.cmd, "terminal abook") if not ok then _pim_scratch("abook", err) end - -- focus mail - vim.cmd("wincmd k") + _pim_wins = { w1, w2, w3 } + vim.api.nvim_set_current_win(w1) end vim.keymap.set("n", "r", toggle_pim, { silent = true }) diff --git a/setup/modules/optional-Modules/apps/mail-notmuch.sh b/setup/modules/optional-Modules/apps/mail-notmuch.sh new file mode 100755 index 0000000..4d59c78 --- /dev/null +++ b/setup/modules/optional-Modules/apps/mail-notmuch.sh @@ -0,0 +1,139 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" + +log "Installing mail stack (isync, msmtp, notmuch, alot)..." +sudo pacman -S --noconfirm --needed isync msmtp notmuch alot + +# ── Credentials ─────────────────────────────────────────────────────────────── +read -rp "Full name : " FULL_NAME +read -rp "Email address : " EMAIL +read -rp "IMAP host (e.g. mail.example.com) : " IMAP_HOST +read -rp "IMAP port [993] : " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}" +read -rp "IMAP username [$EMAIL] : " IMAP_USER; IMAP_USER="${IMAP_USER:-$EMAIL}" +read -rsp "IMAP password : " IMAP_PASS; echo +read -rp "SMTP host (e.g. mail.example.com) : " SMTP_HOST +read -rp "SMTP port [587] : " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}" + +MAILDIR="$HOME/Mail" +mkdir -p "$MAILDIR" + +# ── mbsync ──────────────────────────────────────────────────────────────────── +log "Writing ~/.mbsyncrc..." +cat > ~/.mbsyncrc << EOF +IMAPAccount main +Host $IMAP_HOST +Port $IMAP_PORT +User $IMAP_USER +Pass $IMAP_PASS +SSLType IMAPS +CertificateFile /etc/ssl/certs/ca-certificates.crt + +IMAPStore main-remote +Account main + +MaildirStore main-local +SubFolders Verbatim +Path $MAILDIR/ +Inbox $MAILDIR/INBOX + +Channel main +Far :main-remote: +Near :main-local: +Patterns * +Create Both +SyncState * +Expunge Both +EOF +chmod 600 ~/.mbsyncrc + +# ── msmtp ───────────────────────────────────────────────────────────────────── +log "Writing ~/.msmtprc..." +cat > ~/.msmtprc << EOF +defaults +tls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt +logfile ~/.msmtp.log + +account main +host $SMTP_HOST +port $SMTP_PORT +auth on +user $IMAP_USER +password $IMAP_PASS +from $EMAIL + +account default : main +EOF +chmod 600 ~/.msmtprc + +# ── notmuch ─────────────────────────────────────────────────────────────────── +log "Configuring notmuch..." +notmuch config set user.name "$FULL_NAME" +notmuch config set user.email "$EMAIL" +notmuch config set database.path "$MAILDIR" +notmuch config set maildir.synchronize_flags true +notmuch config set new.tags "unread;inbox" + +# post-new hook: tag sent mail, remove inbox from trash +mkdir -p "$MAILDIR/.notmuch/hooks" +cat > "$MAILDIR/.notmuch/hooks/post-new" << 'EOF' +#!/bin/bash +notmuch tag +sent -inbox -- folder:Sent +notmuch tag +trash -inbox -unread -- folder:Trash +notmuch tag +draft -inbox -- folder:Drafts +notmuch tag +spam -inbox -unread -- folder:Spam folder:Junk +EOF +chmod +x "$MAILDIR/.notmuch/hooks/post-new" + +# ── alot ────────────────────────────────────────────────────────────────────── +log "Writing ~/.config/alot/config..." +mkdir -p ~/.config/alot +cat > ~/.config/alot/config << EOF +[accounts] + [[main]] + realname = $FULL_NAME + address = $EMAIL + sendmail_command = msmtp -a main + sent_box = maildir://$MAILDIR/Sent + draft_box = maildir://$MAILDIR/Drafts +EOF + +# ── systemd timer for periodic sync ─────────────────────────────────────────── +log "Installing mbsync systemd user timer (every 5 min)..." +mkdir -p ~/.config/systemd/user + +cat > ~/.config/systemd/user/mbsync.service << EOF +[Unit] +Description=Sync mail with mbsync +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/mbsync -a +ExecStartPost=/usr/bin/notmuch new +EOF + +cat > ~/.config/systemd/user/mbsync.timer << EOF +[Unit] +Description=Run mbsync every 5 minutes + +[Timer] +OnBootSec=2min +OnUnitActiveSec=5min +Unit=mbsync.service + +[Install] +WantedBy=timers.target +EOF + +systemctl --user daemon-reload +systemctl --user enable --now mbsync.timer + +# ── initial sync ────────────────────────────────────────────────────────────── +log "Running initial mail sync..." +mbsync -a +notmuch new + +log "Mail setup complete. Syncs automatically every 5 min via systemd timer." +log "Open alot with: alot | Check timer: systemctl --user status mbsync.timer"