;;; config.el -*- lexical-binding: t; -*-
;; ============================================================================
;; Evil - Esc exits insert mode (like real vim)
;; ============================================================================
(define-key evil-insert-state-map (kbd "<escape>") 'evil-normal-state)
;; ============================================================================
;; Server - Allow emacsclient to connect to this GUI frame
;; ============================================================================
(unless (daemonp)
(require 'server)
(unless (server-running-p)
(server-start)))
;; ============================================================================
;; User Info
;; ============================================================================
(setq user-full-name "Kris Yotam"
user-mail-address "kris@krisyotam.com")
;; ============================================================================
;; Theme & Appearance - Plan9 (light default, dark toggle with nibelung)
;; ============================================================================
(setq doom-theme 'plan9)
;; Disable syntax highlighting
(global-font-lock-mode -1)
;; Toggle between light and dark mode
(defvar kris/dark-mode nil "Track current theme state.")
(defun kris/toggle-dark-mode ()
"Toggle between plan9 (light) and nibelung-dark themes."
(interactive)
(if kris/dark-mode
(progn
(load-theme 'plan9 t)
(setq kris/dark-mode nil)
(message "Light mode (Plan9)"))
(progn
(load-theme 'nibelung-dark t)
(setq kris/dark-mode t)
(message "Dark mode (Nibelung)"))))
;; Keybind: SPC t d (toggle dark mode)
(map! :leader
:desc "Toggle dark mode" "t d" #'kris/toggle-dark-mode)
;; Force fully opaque frame (no transparency)
(set-frame-parameter nil 'alpha-background 100)
(add-to-list 'default-frame-alist '(alpha-background . 100))
(set-frame-parameter nil 'alpha '(100 . 100))
(add-to-list 'default-frame-alist '(alpha . (100 . 100)))
;; Font configuration
(setq doom-font (font-spec :family "JetBrainsMono Nerd Font" :size 14)
doom-variable-pitch-font (font-spec :family "JetBrainsMono Nerd Font" :size 14)
doom-big-font (font-spec :family "JetBrainsMono Nerd Font" :size 20))
;; Line numbers (absolute, not relative)
(setq display-line-numbers-type t)
;; Larger fringe for aesthetics
(setq-default left-fringe-width 16
right-fringe-width 16)
;; ============================================================================
;; Custom Dashboard Banner
;; ============================================================================
(defun kris/dashboard-ascii-banner ()
"Custom ASCII banner for doom dashboard."
(let* ((banner '(
" "
" ██╗ ██╗██████╗ ██╗███████╗ ██╗ ██╗ ██████╗ ████████╗ █████╗ ███╗ ███╗"
" ██║ ██╔╝██╔══██╗██║██╔════╝ ╚██╗ ██╔╝██╔═══██╗╚══██╔══╝██╔══██╗████╗ ████║"
" █████╔╝ ██████╔╝██║███████╗ ╚████╔╝ ██║ ██║ ██║ ███████║██╔████╔██║"
" ██╔═██╗ ██╔══██╗██║╚════██║ ╚██╔╝ ██║ ██║ ██║ ██╔══██║██║╚██╔╝██║"
" ██║ ██╗██║ ██║██║███████║ ██║ ╚██████╔╝ ██║ ██║ ██║██║ ╚═╝ ██║"
" ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝"
" "))
(longest-line (apply #'max (mapcar #'length banner))))
(put-text-property
(point)
(dolist (line banner (point))
(insert (+doom-dashboard--center
+doom-dashboard--width
(concat line (make-string (max 0 (- longest-line (length banner))) 32)))
"\n"))
'face 'doom-dashboard-banner)))
(setq +doom-dashboard-ascii-banner-fn #'kris/dashboard-ascii-banner)
;; Plan9 theme colors for dashboard
(custom-set-faces!
'(doom-dashboard-menu-title :foreground "#B8996F") ; Muted yellow
'(doom-dashboard-menu-desc :foreground "#4A6FA5")) ; Blue - keybindings like SPC f r
;; Custom dashboard menu items
(setq +doom-dashboard-menu-sections
'(("Open notes"
:icon (nerd-icons-faicon "nf-fa-book" :face 'doom-dashboard-menu-title)
:when (featurep! :lang org +roam)
:action org-roam-node-find)
("Open project"
:icon (nerd-icons-octicon "nf-oct-repo" :face 'doom-dashboard-menu-title)
:action projectile-switch-project)
("Recently opened files"
:icon (nerd-icons-faicon "nf-fa-file_text" :face 'doom-dashboard-menu-title)
:action recentf-open-files)
("Open paper feeds"
:icon (nerd-icons-faicon "nf-fa-rss" :face 'doom-dashboard-menu-title)
:when (featurep! :app elfeed)
:action elfeed)
("Open private configuration"
:icon (nerd-icons-octicon "nf-oct-gear" :face 'doom-dashboard-menu-title)
:when (file-directory-p doom-user-dir)
:action doom/open-private-config)
("Open documentation"
:icon (nerd-icons-faicon "nf-fa-book" :face 'doom-dashboard-menu-title)
:action doom/help)))
;; ============================================================================
;; Org Mode Base Configuration
;; ============================================================================
(setq org-directory "~/notes/")
;; Pretty org-mode settings
(after! org
(setq org-hide-emphasis-markers t
org-link-descriptive t
org-pretty-entities t
org-startup-indented t
org-startup-folded 'showall ; Show everything expanded
org-startup-with-inline-images t
org-image-actual-width '(400)
org-cycle-open-archived-trees t ; Don't hide archived
org-catch-invisible-edits 'show-and-error
org-hide-drawer-startup nil ; Never hide drawers on startup
org-cycle-hide-drawers nil) ; Never collapse drawers with TAB
;; Force drawers to stay open
(remove-hook 'org-cycle-hook #'org-cycle-hide-drawers))
;; org-superstar for beautiful bullets
(after! org-superstar
(setq org-superstar-headline-bullets-list '("◉" "○" "◈" "◇" "▣")
org-superstar-item-bullet-alist '((?- . ?•) (?+ . ?➤))))
;; ============================================================================
;; Org-Roam Configuration (Obsidian-like functionality)
;; ============================================================================
(after! org-roam
(setq org-roam-directory "~/notes/"
org-roam-dailies-directory "0 - Journal/"
org-roam-db-gc-threshold most-positive-fixnum
org-roam-completion-everywhere t)
;; Define allowed values for constrained properties
(setq org-global-properties
'(("STATUS_ALL" . "seed growing evergreen wilted")
("CERTAINTY_ALL" . "certain highly-likely likely possible unlikely highly-unlikely remote impossible")
("IMPORTANCE_ALL" . "1 2 3 4 5 6 7 8 9 10")))
;; Templates for new notes with full metadata
(setq org-roam-capture-templates
'(("d" "default" plain "%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
":PROPERTIES:
:ID: ${slug}
:START: %<%Y-%m-%d>
:FINISH:
:CERTAINTY: possible
:STATUS: seed
:IMPORTANCE: 5
:PREVIEW:
:END:
#+title: ${title}
#+filetags:
")
:unnarrowed t)
("z" "zettel" plain "%?"
:target (file+head "6 - Zettlekasten/%<%Y%m%d%H%M%S>-${slug}.org"
":PROPERTIES:
:ID: ${slug}
:START: %<%Y-%m-%d>
:FINISH:
:CERTAINTY: possible
:STATUS: seed
:IMPORTANCE: 5
:PREVIEW:
:END:
#+title: ${title}
#+filetags: :zettel:
* Source
* Content
* References
")
:unnarrowed t)
("t" "topic" plain "%?"
:target (file+head "3 - Topics/${slug}.org"
":PROPERTIES:
:ID: ${slug}
:START: %<%Y-%m-%d>
:FINISH:
:CERTAINTY: possible
:STATUS: seed
:IMPORTANCE: 5
:PREVIEW:
:END:
#+title: ${title}
#+filetags: :topic:
")
:unnarrowed t)))
;; Daily notes templates
(setq org-roam-dailies-capture-templates
'(("d" "default" entry "* %?"
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n#+filetags: :journal:\n\n")))))
;; ============================================================================
;; Org-Roam-UI (Graph Visualization)
;; ============================================================================
(use-package! org-roam-ui
:after org-roam
:config
(setq org-roam-ui-sync-theme t
org-roam-ui-follow t
org-roam-ui-update-on-save t
org-roam-ui-open-on-start nil))
;; ============================================================================
;; MD-Roam (Support for existing Markdown files)
;; ============================================================================
(use-package! md-roam
:after org-roam
:config
(setq md-roam-file-extension "md")
(md-roam-mode 1)
;; Include .md files in org-roam
(setq org-roam-file-extensions '("org" "md")))
;; ============================================================================
;; LaTeX & Math Rendering
;; ============================================================================
(after! org
;; Use dvisvgm for crisp SVG previews
(setq org-preview-latex-default-process 'dvisvgm)
;; Styling: scale to match prose, pure black, NO background
(plist-put org-format-latex-options :scale 0.85)
(plist-put org-format-latex-options :foreground 'default)
(plist-put org-format-latex-options :background 'default)
;; Extended LaTeX packages for complex equations
(setq org-latex-packages-alist
'(("" "amsmath" t) ; Advanced math environments
("" "amssymb" t) ; Math symbols
("" "amsthm" t) ; Theorem environments
("" "mathtools" t) ; Extensions to amsmath
("" "physics" t) ; Physics notation (bra-ket, derivatives)
("" "siunitx" t) ; SI units
("" "mhchem" t) ; Chemical formulas
("" "cancel" t) ; Strike-through in equations
("" "mathrsfs" t) ; Script fonts (\mathscr)
("" "bm" t) ; Bold math
("" "bbm" t) ; Blackboard bold
("" "esint" t) ; More integral signs
("" "tikz" t) ; Diagrams
("" "pgfplots" t))) ; Plots
;; Custom LaTeX preamble for extra commands (pure black math - no blue)
(setq org-format-latex-header
(concat org-format-latex-header
"\n\\newcommand{\\R}{\\mathbb{R}}"
"\n\\newcommand{\\N}{\\mathbb{N}}"
"\n\\newcommand{\\Z}{\\mathbb{Z}}"
"\n\\newcommand{\\Q}{\\mathbb{Q}}"
"\n\\newcommand{\\C}{\\mathbb{C}}"
"\n\\newcommand{\\norm}[1]{\\left\\lVert#1\\right\\rVert}"
"\n\\newcommand{\\abs}[1]{\\left|#1\\right|}"
"\n\\DeclareMathOperator{\\Tr}{Tr}"
"\n\\DeclareMathOperator{\\rank}{rank}"
"\n\\DeclareMathOperator{\\sgn}{sgn}")))
;; Auto-toggle LaTeX fragments
(use-package! org-fragtog
:after org
:hook (org-mode . org-fragtog-mode))
;; ============================================================================
;; Split Pane Preview (Raw Left, Rendered Right)
;; ============================================================================
(defun kris/org-split-preview ()
"Open current org file in split view: raw on left, rendered HTML on right."
(interactive)
(let ((org-file (buffer-file-name)))
(delete-other-windows)
;; Left side: raw org (disable latex preview here)
(org-latex-preview-clear-overlays)
;; Right side: export and view
(split-window-right)
(other-window 1)
(org-export-to-file 'html
(concat (file-name-sans-extension org-file) ".html")
nil nil nil nil nil
(lambda (file) (browse-url-of-file file)))))
(defun kris/org-preview-in-browser ()
"Export current org buffer to HTML and open in browser."
(interactive)
(let ((html-file (concat (file-name-sans-extension (buffer-file-name)) ".html")))
(org-export-to-file 'html html-file nil nil nil nil nil)
(browse-url-of-file html-file)))
;; ============================================================================
;; Writing Environment
;; ============================================================================
;; Olivetti for centered writing - only when single window
(use-package! olivetti
:config
(setq olivetti-body-width 100)
(defun kris/olivetti-single-window ()
"Enable olivetti only when there's a single window."
(if (= (count-windows) 1)
(olivetti-mode 1)
(olivetti-mode -1)))
(defun kris/update-olivetti-all-windows ()
"Update olivetti state in all windows."
(dolist (win (window-list))
(with-current-buffer (window-buffer win)
(when (derived-mode-p 'org-mode 'markdown-mode 'LaTeX-mode 'text-mode)
(kris/olivetti-single-window)))))
(add-hook 'window-configuration-change-hook #'kris/update-olivetti-all-windows)
(add-hook 'org-mode-hook #'kris/olivetti-single-window)
(add-hook 'markdown-mode-hook #'kris/olivetti-single-window)
(add-hook 'LaTeX-mode-hook #'kris/olivetti-single-window)
(add-hook 'text-mode-hook #'kris/olivetti-single-window))
;; Mixed pitch mode for prose
(use-package! mixed-pitch
:hook (org-mode . mixed-pitch-mode))
;; Markdown rendering with LaTeX math support
(after! markdown-mode
(setq markdown-enable-math t
markdown-fontify-code-blocks-natively t
markdown-command "pandoc -f markdown -t html5 --mathjax --standalone"))
;; texfrag — inline LaTeX preview in markdown buffers (like org-fragtog for org)
;; Renders \(...\) inline and \[...\] display math as SVG overlays
(use-package! texfrag
:after markdown-mode
:config
(setq texfrag-preview-buffer-at-start t)
;; Use same dvisvgm pipeline and scale as org-mode previews
(setq texfrag-LaTeX-frag-alist
'(("\\\\(" "\\\\)" "\\(" "\\)")
("\\\\\\[" "\\\\\\]" "\\[" "\\]")
("\\\\begin{equation}" "\\\\end{equation}" "\\begin{equation}" "\\end{equation}")
("\\\\begin{equation\\*}" "\\\\end{equation\\*}" "\\begin{equation*}" "\\end{equation*}")
("\\\\begin{align}" "\\\\end{align}" "\\begin{align}" "\\end{align}")
("\\\\begin{align\\*}" "\\\\end{align\\*}" "\\begin{align*}" "\\end{align*}")))
;; Inherit LaTeX packages from org config
(setq texfrag-header-default
(concat "\\documentclass{article}\n"
"\\usepackage{amsmath}\n"
"\\usepackage{amssymb}\n"
"\\usepackage{amsthm}\n"
"\\usepackage{mathtools}\n"
"\\usepackage{physics}\n"
"\\usepackage{mathrsfs}\n"
"\\usepackage{bm}\n"
"\\usepackage{bbm}\n"
"\\usepackage{cancel}\n"
"\\newcommand{\\R}{\\mathbb{R}}\n"
"\\newcommand{\\N}{\\mathbb{N}}\n"
"\\newcommand{\\Z}{\\mathbb{Z}}\n"
"\\newcommand{\\Q}{\\mathbb{Q}}\n"
"\\newcommand{\\C}{\\mathbb{C}}\n"
"\\newcommand{\\norm}[1]{\\left\\lVert#1\\right\\rVert}\n"
"\\newcommand{\\abs}[1]{\\left|#1\\right|}\n"
"\\DeclareMathOperator{\\Tr}{Tr}\n"
"\\DeclareMathOperator{\\rank}{rank}\n"
"\\DeclareMathOperator{\\sgn}{sgn}\n"
"\\pagestyle{empty}\n"
"\\begin{document}\n"))
:hook (markdown-mode . texfrag-mode))
;; Markdown preview inside Emacs using eww
(defun kris/markdown-preview ()
"Preview current markdown buffer in eww with MathJax support."
(interactive)
(unless (derived-mode-p 'markdown-mode)
(user-error "Not in markdown-mode"))
(let ((output-file (concat (make-temp-file "md-preview-") ".html"))
(input-file (buffer-file-name)))
(if input-file
;; File exists - use pandoc on file
(progn
(save-buffer)
(shell-command
(format "pandoc '%s' -f markdown -t html5 --mathjax --standalone -o '%s'"
input-file output-file))
(eww-open-file output-file))
;; Buffer not saved - use buffer content
(let ((content (buffer-string)))
(with-temp-file output-file
(insert (shell-command-to-string
(format "echo %s | pandoc -f markdown -t html5 --mathjax --standalone"
(shell-quote-argument content)))))
(eww-open-file output-file)))))
;; Bind to SPC m P (capital P to avoid conflicts)
(map! :after markdown-mode
:map markdown-mode-map
:localleader
"P" #'kris/markdown-preview)
;; LaTeX / AUCTeX configuration
(after! tex
(setq TeX-auto-save t
TeX-parse-self t
TeX-save-query nil
TeX-PDF-mode t
TeX-view-program-selection '((output-pdf "PDF Tools"))
TeX-source-correlate-mode t
TeX-source-correlate-start-server t)
(add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer))
;; LaTeX keybindings - use b for build, V for view
(map! :after latex
:map LaTeX-mode-map
:localleader
"b" #'TeX-command-run-all ; build/compile
"V" #'TeX-view ; View PDF (capital V)
"L" #'TeX-recenter-output-buffer)
;; Also add global keybindings for LaTeX
(map! :after latex
:map LaTeX-mode-map
"C-c b" #'TeX-command-run-all
"C-c v" #'TeX-view)
;; ============================================================================
;; Keybindings
;; ============================================================================
(map! :leader
(:prefix ("n" . "notes")
:desc "Find node" "f" #'org-roam-node-find
:desc "Insert link" "i" #'org-roam-node-insert
:desc "Capture" "c" #'org-roam-capture
:desc "Buffer toggle" "b" #'org-roam-buffer-toggle
:desc "Dailies today" "t" #'org-roam-dailies-goto-today
:desc "Dailies date" "d" #'org-roam-dailies-goto-date
:desc "Graph UI" "g" #'org-roam-ui-open
:desc "Sync database" "s" #'org-roam-db-sync)
(:prefix ("m" . "math/preview")
:desc "Live preview (split)" "p" #'org-preview-html-mode
:desc "Preview in browser" "b" #'kris/org-preview-in-browser
:desc "Render all LaTeX" "l" #'org-latex-preview
:desc "Clear LaTeX renders" "c" #'org-latex-preview-clear-overlays)
(:prefix ("p" . "panes")
:desc "Open pane (split right)" "o" #'split-window-right
:desc "Close pane" "c" #'delete-window
:desc "Balance panes" "b" #'balance-windows
:desc "Grow pane" "g" #'enlarge-window-horizontally
:desc "Shrink pane" "s" #'shrink-window-horizontally
:desc "Pane left" "h" #'windmove-left
:desc "Pane down" "j" #'windmove-down
:desc "Pane up" "k" #'windmove-up
:desc "Pane right" "l" #'windmove-right
;; Legacy pane functions
:desc "Open note in new pane" "n" #'kris/open-note-in-new-pane
:desc "Reset to single pane" "r" #'kris/reset-to-single-pane))
;; ============================================================================
;; Sliding Panes / Kris Yotam - Mode
;; ============================================================================
;; Open notes side-by-side like stacked tabs in Obsidian
(defvar kris/pane-width 80
"Width of each sliding pane.")
(defun kris/open-note-in-new-pane ()
"Open a note in a new vertical pane to the right (sliding panes style)."
(interactive)
(split-window-right kris/pane-width)
(other-window 1)
(org-roam-node-find))
(defun kris/open-link-in-new-pane ()
"Open the link at point in a new pane to the right."
(interactive)
(let ((link (org-element-context)))
(when (eq (org-element-type link) 'link)
(split-window-right kris/pane-width)
(other-window 1)
(org-open-at-point))))
(defun kris/close-rightmost-pane ()
"Close the rightmost pane."
(interactive)
(let ((win (window-at (- (frame-width) 2) (/ (frame-height) 2))))
(when (and win (not (eq win (selected-window))))
(delete-window win))))
(defun kris/scroll-panes-left ()
"Scroll all panes left (close leftmost, keep focus)."
(interactive)
(delete-window (window-at 0 (/ (frame-height) 2))))
(defun kris/reset-to-single-pane ()
"Close all other panes, keep current note."
(interactive)
(delete-other-windows))
;; Follow links into new panes by default
(setq org-link-frame-setup
'((vm . vm-visit-folder-other-frame)
(vm-istrm . vm-visit-imap-folder-other-frame)
(gnus . org-gnus-no-new-news)
(file . find-file-other-window) ; Opens links in other window
(wl . wl-other-frame)))
;; Horizontal scroll/swipe to switch panes (mouse/trackpad)
(global-set-key (kbd "<mouse-6>") (lambda () (interactive) (other-window -1)))
(global-set-key (kbd "<mouse-7>") (lambda () (interactive) (other-window 1)))
(global-set-key (kbd "<S-wheel-left>") (lambda () (interactive) (other-window -1)))
(global-set-key (kbd "<S-wheel-right>") (lambda () (interactive) (other-window 1)))
(global-set-key (kbd "<S-wheel-up>") (lambda () (interactive) (other-window -1)))
(global-set-key (kbd "<S-wheel-down>") (lambda () (interactive) (other-window 1)))
;; Only show scrollbar on focused window
(scroll-bar-mode -1) ; Disable globally first
(defun kris/toggle-scrollbar-on-focus ()
"Show scrollbar only on selected window."
(walk-windows
(lambda (win)
(set-window-scroll-bars win nil nil nil nil)) ; Hide all
nil t)
(set-window-scroll-bars (selected-window) 12 'right nil nil)) ; Show on current
(add-hook 'window-selection-change-functions
(lambda (_) (kris/toggle-scrollbar-on-focus)))
(add-hook 'window-configuration-change-hook #'kris/toggle-scrollbar-on-focus)
;; ============================================================================
;; Performance Optimizations
;; ============================================================================
(setq gc-cons-threshold (* 100 1024 1024) ; 100MB
read-process-output-max (* 1024 1024) ; 1MB
company-idle-delay 0.2
which-key-idle-delay 0.3)
;; ============================================================================
;; Treemacs (Sidebar file tree)
;; ============================================================================
(after! treemacs
(setq treemacs-width 30
treemacs-position 'left))
;; ============================================================================
;; Elfeed - Academic Research Feed Reader
;; ============================================================================
(after! elfeed
(setq elfeed-feeds
'(
;; ========================================
;; LLMs & AI (PRIMARY FOCUS)
;; Training methods, alignment, advances
;; ========================================
("http://export.arxiv.org/rss/cs.LG" llm ml arxiv) ; Machine Learning
("http://export.arxiv.org/rss/cs.CL" llm nlp arxiv) ; Computation & Language
("http://export.arxiv.org/rss/cs.AI" llm ai arxiv) ; Artificial Intelligence
("http://export.arxiv.org/rss/cs.NE" llm neural arxiv) ; Neural & Evolutionary
("http://export.arxiv.org/rss/stat.ML" llm stats arxiv) ; Statistical ML
;; ========================================
;; Marine Biology
;; Cetaceans, orcas, migration, acoustics
;; ========================================
;; bioRxiv feeds
("http://connect.biorxiv.org/biorxiv_xml.php?subject=zoology" marine bio biorxiv)
("http://connect.biorxiv.org/biorxiv_xml.php?subject=animal_behavior_and_cognition" marine bio biorxiv)
("http://connect.biorxiv.org/biorxiv_xml.php?subject=ecology" marine bio biorxiv)
;; PubMed custom searches (cetaceans, orcas, marine acoustics)
("https://pubmed.ncbi.nlm.nih.gov/rss/search/1234567/?limit=50&utm_campaign=pubmed-2&fc=20231201000000" marine cetacean pubmed)
;; PLOS ONE Marine Mammals
("https://journals.plos.org/plosone/feed/atom?filterSubjects=Marine+mammals" marine plos)
;; Nature Marine Biology
("http://feeds.nature.com/subjects/marine-biology.rss" marine nature)
;; ========================================
;; Pedagogy & Education
;; STEAM and mathematics education
;; ========================================
("http://export.arxiv.org/rss/physics.ed-ph" pedagogy physics-ed arxiv) ; Physics Education
("http://export.arxiv.org/rss/math.HO" pedagogy math-ed arxiv) ; Math History/Overview
;; Education journals
("https://www.tandfonline.com/feed/rss/ujme20" pedagogy math-ed journal) ; Journal of Math Ed
("https://stemeducationjournal.springeropen.com/articles/most-recent/rss.xml" pedagogy stem journal)))
;; Default filter - show all unread
(setq elfeed-search-filter "@2-weeks-ago +unread")
;; ========================================
;; Quick filters for each research area
;; ========================================
(defun kris/elfeed-llm ()
"Show only LLM/AI papers."
(interactive)
(elfeed-search-set-filter "@6-months-ago +unread +llm"))
(defun kris/elfeed-marine ()
"Show only marine biology papers."
(interactive)
(elfeed-search-set-filter "@6-months-ago +unread +marine"))
(defun kris/elfeed-pedagogy ()
"Show only pedagogy/education papers."
(interactive)
(elfeed-search-set-filter "@6-months-ago +unread +pedagogy"))
(defun kris/elfeed-all ()
"Show all unread papers."
(interactive)
(elfeed-search-set-filter "@2-weeks-ago +unread"))
;; Keybindings for filters (in elfeed search mode)
(map! :map elfeed-search-mode-map
:n "L" #'kris/elfeed-llm
:n "M" #'kris/elfeed-marine
:n "P" #'kris/elfeed-pedagogy
:n "A" #'kris/elfeed-all
;; Fetch/update feeds
:n "g r" #'elfeed-search-fetch
:n "g R" #'elfeed-search-update--force
:n "R" #'elfeed-update)
;; Entry display settings
(setq elfeed-show-entry-switch #'pop-to-buffer
elfeed-show-entry-delete #'elfeed-kill-buffer)
;; Larger entry display for reading abstracts
(add-hook 'elfeed-show-mode-hook
(lambda ()
(setq-local shr-max-width 80)
(visual-line-mode 1))))
;; ============================================================================
;; Elfeed-Score - Numerical scoring for entries
;; ============================================================================
(use-package! elfeed-score
:after elfeed
:config
(elfeed-score-enable)
(setq elfeed-score-serde-score-file "~/.config/doom/elfeed-score.el")
;; Sort by score in search view
(setq elfeed-search-sort-function #'elfeed-score-sort)
;; Show score in search format
(setq elfeed-search-print-entry-function #'elfeed-score-print-entry)
;; Keybindings for scoring
(map! :map elfeed-search-mode-map
:n "=" #'elfeed-score-set-score
:n "+" #'(lambda () (interactive)
(elfeed-score-set-score (+ (elfeed-score-get-score (elfeed-search-selected :single)) 10)))
:n "-" #'(lambda () (interactive)
(elfeed-score-set-score (- (elfeed-score-get-score (elfeed-search-selected :single)) 10)))))
;; Elfeed keybindings
(map! :leader
(:prefix ("e" . "elfeed")
:desc "Open Elfeed" "e" #'elfeed
:desc "Update feeds" "u" #'elfeed-update
:desc "Update feed" "f" #'elfeed-search-fetch
:desc "Score explain" "s" #'elfeed-score-scoring-explain-entry))
;; ============================================================================
;; Research Workflow - Bibliography & Paper Management
;; Based on Ahmed Khaled's EmacsConf 2021 talk
;; ============================================================================
;; Library paths
(defconst kris/bib-libraries '("~/bib-lib/library.bib" "~/bib-lib/temp-lib.bib"))
(defconst kris/main-bib-library (nth 0 kris/bib-libraries))
(defconst kris/pdf-library-paths '("~/bib-lib/pdfs/" "~/bib-lib/temp-pdfs/"))
(defconst kris/main-pdf-library (nth 0 kris/pdf-library-paths))
(defconst kris/bib-notes-dir "~/bib-lib/roam/")
;; ============================================================================
;; arXiv Import Functions (defined globally, not inside use-package)
;; Uses org-ref style completing-read for library selection
;; ============================================================================
;; Custom arXiv import with library prompt (like org-ref)
(defun kris/arxiv-to-library (arxiv-id)
"Fetch arXiv paper by ARXIV-ID, prompt for library, download PDF, add BibTeX."
(interactive
(list (read-string "arXiv ID: "
(when (car kill-ring)
(let ((kill (car kill-ring)))
(when (string-match "\\([0-9]\\{4\\}\\.[0-9]\\{4,5\\}\\)" kill)
(match-string 1 kill)))))))
(let* ((bibfile (completing-read "Bibfile: " kris/bib-libraries nil t nil nil kris/main-bib-library))
(pdfdir (if (string-match "temp" bibfile)
(expand-file-name "temp-pdfs/" "~/bib-lib/")
kris/main-pdf-library)))
(kris/arxiv-fetch-and-add arxiv-id bibfile pdfdir)))
;; Import arXiv paper from current elfeed entry (press 'a')
(defun kris/elfeed-arxiv-to-library ()
"Import arXiv paper from current elfeed entry to local library.
Prompts for which bibliography file to use (like org-ref)."
(interactive)
(let* ((link (elfeed-entry-link elfeed-show-entry))
(match-idx (string-match "arxiv.org/abs/\\([0-9v.]+\\)" link))
(arxiv-id (when match-idx
(replace-regexp-in-string "v[0-9]+$" "" (match-string 1 link)))))
(if arxiv-id
(let* ((bibfile (completing-read "Bibfile: " kris/bib-libraries nil t nil nil kris/main-bib-library))
(pdfdir (if (string-match "temp" bibfile)
(expand-file-name "temp-pdfs/" "~/bib-lib/")
kris/main-pdf-library)))
(kris/arxiv-fetch-and-add arxiv-id bibfile pdfdir))
(message "No arXiv ID found in this entry"))))
;; Core fetch function - downloads PDF and adds BibTeX entry
(defun kris/arxiv-fetch-and-add (arxiv-id bibfile pdfdir)
"Fetch arXiv paper ARXIV-ID, save PDF to PDFDIR, add entry to BIBFILE."
(let* ((arxiv-id (replace-regexp-in-string "^https?://arxiv.org/abs/" "" arxiv-id))
(url (format "http://export.arxiv.org/api/query?id_list=%s" arxiv-id))
(pdf-url (format "https://arxiv.org/pdf/%s.pdf" arxiv-id)))
(message "Fetching arXiv:%s..." arxiv-id)
(url-retrieve url
(lambda (_status arxiv-id pdf-url bibfile pdfdir)
(goto-char (point-min))
(re-search-forward "\n\n")
(let* ((xml (libxml-parse-xml-region (point) (point-max)))
(entry (car (dom-by-tag xml 'entry)))
(title (string-trim (dom-text (car (dom-by-tag entry 'title)))))
(authors (mapcar (lambda (a) (dom-text (car (dom-by-tag a 'name))))
(dom-by-tag entry 'author)))
(published (dom-text (car (dom-by-tag entry 'published))))
(year (substring published 0 4))
(abs-url (format "https://arxiv.org/abs/%s" arxiv-id))
(first-author (car (split-string (car authors))))
(title-word (car (split-string (downcase title))))
(key (format "%s%s_%s" (downcase first-author) (substring year 2) title-word))
(pdf-file (expand-file-name (concat key ".pdf") pdfdir))
(bib-entry (format "@article{%s,
author = {%s},
url = {%s},
year = {%s},
journal = {arXiv},
title = {%s},
}
"
key
(mapconcat #'identity authors " and ")
abs-url
year
title)))
(message "Downloading PDF: %s" arxiv-id)
(url-copy-file pdf-url pdf-file t)
(with-temp-buffer
(insert bib-entry)
(append-to-file (point-min) (point-max) bibfile))
(message "Added: %s -> %s (bib: %s)" key pdf-file (file-name-nondirectory bibfile))))
(list arxiv-id pdf-url bibfile pdfdir))))
;; Function to reformat bibliography using biber & rebiber
(defun kris/reformat-bib-library (&optional filename)
"Format bibliography using biber & rebiber, update with conference versions."
(interactive)
(or filename (setq filename kris/main-bib-library))
(let ((cmnd (concat
(format "rebiber -i %s ; " filename)
(format "biber --tool --output_align --output_indent=2 --output_fieldcase=lower --output_file=%s %s"
filename filename))))
(async-shell-command cmnd "*bib-reformat*")))
;; Keybinding for elfeed - press 'a' to add arXiv paper
(map! :after elfeed
:map elfeed-show-mode-map
:n "a" #'kris/elfeed-arxiv-to-library)
;; ============================================================================
;; org-ref - Bibliography management
;; ============================================================================
(use-package! org-ref
:after org
:config
(setq bibtex-completion-bibliography kris/bib-libraries
bibtex-completion-library-path kris/pdf-library-paths
bibtex-completion-notes-path kris/bib-notes-dir
bibtex-dialect 'biblatex))
;; ============================================================================
;; org-roam-bibtex - Link notes to bibliography entries
;; ============================================================================
(use-package! org-roam-bibtex
:after org-roam
:config
(require 'org-ref)
(org-roam-bibtex-mode 1)
(setq orb-preformat-keywords '("citekey" "title" "url" "author-or-editor" "keywords" "file")
orb-process-file-keyword t
orb-file-field-extensions '("pdf"))
;; Capture template for bibliography notes
;; Stored in ~/notes/references/ (symlinked to ~/bib-lib/roam/references/)
(add-to-list 'org-roam-capture-templates
'("r" "bibliography reference" plain "%?"
:target (file+head "references/${citekey}.org"
":PROPERTIES:
:ROAM_REFS: cite:${citekey}
:END:
#+title: Notes on ${title}
#+filetags: :reference:
* Summary
* Key Points
* Quotes
* My Thoughts
")
:unnarrowed t)))
;; ============================================================================
;; Citar - Enhanced citation management
;; ============================================================================
(after! citar
(setq citar-bibliography kris/bib-libraries
citar-library-paths kris/pdf-library-paths
citar-notes-paths (list kris/bib-notes-dir)
citar-file-extensions '("pdf" "org" "md"))
;; Use with org-roam-bibtex
(setq citar-open-note-function 'orb-citar-edit-note)
;; Open PDFs externally with zathura by default
(setq citar-file-open-functions
'(("pdf" . (lambda (file) (start-process "zathura" nil "zathura" file)))
(t . find-file))))
;; Function to open PDF inside Emacs with pdf-tools
(defun kris/citar-open-pdf-in-emacs ()
"Open a PDF from the bibliography inside Emacs using pdf-tools."
(interactive)
(let ((citar-file-open-functions '(("pdf" . find-file) (t . find-file))))
(citar-open-files)))
;; ============================================================================
;; Zotero Integration via zotxt
;; ============================================================================
(use-package! zotxt
:after org
:config
;; Enable in org-mode
(add-hook 'org-mode-hook #'org-zotxt-mode))
;; ============================================================================
;; Research Keybindings
;; ============================================================================
(map! :leader
(:prefix ("r" . "research")
:desc "Import arXiv paper" "a" #'kris/arxiv-to-library
:desc "Open Zotero item" "z" #'org-zotxt-insert-reference-link
:desc "Insert citation" "c" #'citar-insert-citation
:desc "Open PDF (zathura)" "p" #'citar-open-files
:desc "Open PDF (emacs)" "P" #'kris/citar-open-pdf-in-emacs
:desc "Open notes" "n" #'citar-open-notes
:desc "Reformat bibliography" "r" #'kris/reformat-bib-library
:desc "Search bibliography" "s" #'citar-open))