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 <noreply@anthropic.com>main
parent
00b178103e
commit
96610b7ed0
|
|
@ -159,7 +159,7 @@ vim.keymap.set("i", "<CR>", function()
|
||||||
return vim.fn["coc#pum#visible"]() == 1 and vim.fn["coc#pum#confirm"]() or "<CR>"
|
return vim.fn["coc#pum#visible"]() == 1 and vim.fn["coc#pum#confirm"]() or "<CR>"
|
||||||
end, { expr = true, silent = true })
|
end, { expr = true, silent = true })
|
||||||
|
|
||||||
-- ── PIM sidebar (calendar / notmuch / abook) ──────────────────────────────────
|
-- ── PIM floating windows (calendar / alot / abook) ───────────────────────────
|
||||||
local function _pim_scratch(label, err)
|
local function _pim_scratch(label, err)
|
||||||
vim.cmd("enew")
|
vim.cmd("enew")
|
||||||
vim.bo.buftype = "nofile"
|
vim.bo.buftype = "nofile"
|
||||||
|
|
@ -176,62 +176,73 @@ local function _pim_close_win(win)
|
||||||
end
|
end
|
||||||
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 _solo = {}
|
||||||
|
|
||||||
local function toggle_solo(key, cmd, label, width)
|
local function toggle_solo(key, cmd, label)
|
||||||
local win = _solo[key]
|
local win = _solo[key]
|
||||||
if win and vim.api.nvim_win_is_valid(win) then
|
if win and vim.api.nvim_win_is_valid(win) then
|
||||||
_pim_close_win(win)
|
_pim_close_win(win)
|
||||||
_solo[key] = nil
|
_solo[key] = nil
|
||||||
else
|
return
|
||||||
local prev = vim.api.nvim_get_current_win()
|
end
|
||||||
vim.cmd("topleft vsplit")
|
local h = math.floor(vim.o.lines * 0.85)
|
||||||
vim.api.nvim_win_set_width(0, width)
|
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)
|
local ok, err = pcall(vim.cmd, cmd)
|
||||||
if not ok then _pim_scratch(label, err) end
|
if not ok then _pim_scratch(label, err) end
|
||||||
_solo[key] = vim.api.nvim_get_current_win()
|
_solo[key] = win_id
|
||||||
vim.api.nvim_set_current_win(prev)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.keymap.set("n", "n", function() toggle_solo("n", "terminal alot", "alot", 55) 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", 35) 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", 80) 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)
|
-- r: full-screen tiled overlay (mail top, calendar bottom-left, abook bottom-right)
|
||||||
local _pim_tab = nil
|
local _pim_wins = {}
|
||||||
|
|
||||||
local function toggle_pim()
|
local function toggle_pim()
|
||||||
if _pim_tab and vim.api.nvim_tabpage_is_valid(_pim_tab) then
|
if #_pim_wins > 0 and vim.api.nvim_win_is_valid(_pim_wins[1]) then
|
||||||
vim.cmd("tabclose " .. vim.api.nvim_tabpage_get_number(_pim_tab))
|
for _, w in ipairs(_pim_wins) do _pim_close_win(w) end
|
||||||
_pim_tab = nil
|
_pim_wins = {}
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.cmd("tabnew")
|
local H = vim.o.lines - 2
|
||||||
_pim_tab = vim.api.nvim_get_current_tabpage()
|
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")
|
local ok, err = pcall(vim.cmd, "terminal alot")
|
||||||
if not ok then _pim_scratch("alot", err) end
|
if not ok then _pim_scratch("alot", err) end
|
||||||
|
|
||||||
-- split bottom third for calendar + abook
|
local w2 = _pim_float(mail_h, 0, bot_h, cal_w)
|
||||||
vim.cmd("belowright split")
|
|
||||||
vim.cmd("resize 24")
|
|
||||||
|
|
||||||
-- bottom-left: calendar
|
|
||||||
vim.cmd("leftabove vsplit")
|
|
||||||
ok, err = pcall(vim.cmd, "Calendar")
|
ok, err = pcall(vim.cmd, "Calendar")
|
||||||
if not ok then _pim_scratch("calendar.vim", err) end
|
if not ok then _pim_scratch("calendar.vim", err) end
|
||||||
|
|
||||||
-- bottom-right: abook
|
local w3 = _pim_float(mail_h, cal_w, bot_h, W - cal_w)
|
||||||
vim.cmd("wincmd l")
|
|
||||||
ok, err = pcall(vim.cmd, "terminal abook")
|
ok, err = pcall(vim.cmd, "terminal abook")
|
||||||
if not ok then _pim_scratch("abook", err) end
|
if not ok then _pim_scratch("abook", err) end
|
||||||
|
|
||||||
-- focus mail
|
_pim_wins = { w1, w2, w3 }
|
||||||
vim.cmd("wincmd k")
|
vim.api.nvim_set_current_win(w1)
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.keymap.set("n", "r", toggle_pim, { silent = true })
|
vim.keymap.set("n", "r", toggle_pim, { silent = true })
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue