;; Initialize straight.el package manager
(setq straight-use-package-by-default t)
;; Bootstrap straight.el
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 6))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
;; Install use-package via straight.el
(straight-use-package 'use-package)
;; Optional: Basic UI tweaks
(menu-bar-mode -1) ;; Disable menu bar
(tool-bar-mode -1) ;; Disable toolbar
(scroll-bar-mode -1) ;; Disable scrollbar
(global-auto-revert-mode 1)
(horizontal-scroll-bar-mode -1)
(setq inhibit-startup-screen t) ;; Disable startup screen
;; Optional: Basic editor settings
(setq make-backup-files nil) ;; Disable backup files
(setq auto-save-default nil) ;; Disable auto-save
(setq ring-bell-function 'ignore) ;; Silence bell
(setq enable-recursive-minibuffers t)
(defalias 'yes-or-no-p 'y-or-n-p)
;; Add more packages here using (use-package ...)
(use-package disable-mouse
:config
(global-disable-mouse-mode)) ;; Disable mouse globally
;; Install and configure evil-mode
(use-package evil
:init
(setq evil-want-integration t) ;; This is optional since it's enabled by default
(setq evil-want-keybinding nil)
(setq evil-want-C-u-scroll t) ;; Enables Vim-like scrolling with Ctrl+u and Ctrl+d
(setq evil-want-C-i-jump nil) ;; Avoids conflicts with TAB behavior in Emacs
:config
(evil-define-key 'normal 'global
(kbd "C-c p s") 'persp-switch ;; Switch to another perspective (workspace)
(kbd "C-c p k") 'persp-kill ;; Kill a perspective
(kbd "C-c p n") 'persp-next ;; Move to the next perspective
(kbd "C-c p p") 'persp-prev) ;; Move to the previous perspective
(evil-mode 1))
;; Optionally, you might want to add evil-collection for better integration with Emacs' built-in packages
(use-package evil-collection
:after evil
:config
(evil-collection-init))
;; Install evil-org for Vim keybindings in org-mode
(use-package evil-org
:after (org evil)
:config
(add-hook 'org-mode-hook 'evil-org-mode)
(evil-org-set-key-theme '(navigation insert textobjects additional calendar))
(require 'evil-org-agenda)
(evil-org-agenda-set-keys))
;; Optional: Use evil-collection to enhance evil bindings across Emacs packages
(use-package evil-collection
:after evil
:config
(evil-collection-init))
;; Enable evil-mode in all buffers by default
(setq evil-default-state 'normal)
(use-package which-key
:ensure t
:config
(which-key-mode))
(use-package helpful
:ensure t
:bind
(("C-h f" . helpful-callable)
("C-h v" . helpful-variable)
("C-h k" . helpful-key)
("C-h x" . helpful-command)
("C-c C-d" . helpful-at-point)))
;; Install and configure Consult for enhanced buffer switching
(use-package consult
:bind
(("C-x b" . my/consult-perspective-buffer) ;; Replaces the default buffer list
("C-x C-b" . consult-bookmark) ;; Also remap C-x C-b
("M-y" . consult-yank-pop) ;; Enhanced yank (paste) menu
("C-s" . consult-line) ;; Search within current buffer
("C-M-l" . consult-imenu) ;; Jump to headings or sections
("M-g M-g" . consult-goto-line)) ;; Go to line with preview
:config
(defun my/consult-perspective-buffer ()
"Use `consult-buffer` to list buffers in the current Perspective."
(interactive)
(let ((persp-buffers (persp-current-buffers)))
(consult--buffer-action (consult--read
(mapcar #'buffer-name persp-buffers)
:prompt "Switch to perspective buffer: "
:sort nil
:require-match t
:category 'buffer))))
;; Customize consult-buffer to put the previous buffer first
(setq consult-buffer-sources
'(consult--source-hidden-buffer ;; Hidden buffers (optional)
consult--source-buffer ;; Buffers
consult--source-recent-file ;; Recent files
consult--source-bookmark))) ;; Bookmarks
;; Enable consult for buffer switching with Vertico
(use-package vertico
:bind (:map vertico-map
;; Bind C-j and C-k to move up and down in Vertico's minibuffer
("C-j" . vertico-next)
("C-k" . vertico-previous)
:map minibuffer-local-map
("M-h" . backward-kill-word))
:custom (vertico-cycle t)
:init (vertico-mode))
;; Install and configure Marginalia for annotated completions
(use-package marginalia
:straight t
:after vertico
:init
(marginalia-mode))
;; Optional: Customize Marginalia annotations if desired
(setq marginalia-annotators
'(marginalia-annotators-heavy marginalia-annotators-light nil))
;; Install and configure Orderless for flexible completion
(use-package orderless
:init
;; Set Orderless as the default completion style
(setq completion-styles '(orderless)
completion-category-defaults nil
completion-category-overrides '((file (styles partial-completion))))) ;; Partial completion for file paths
;; Install and configure Embark for context-aware actions
(use-package embark
:bind
(("C-." . embark-act) ;; Press C-. to trigger actions on the selected item
("C-;" . embark-dwim) ;; C-; for "Do What I Mean" smart context menu
("C-h B" . embark-bindings)) ;; Show all keybindings available for the current context
:init
;; Set embark-act as the prefix-help-command to show Embark's help when pressing the prefix
(setq prefix-help-command #'embark-prefix-help-command))
;; Integrate Embark with Consult for a smoother experience
(use-package embark-consult
:after (embark consult)
:hook
(embark-collect-mode . consult-preview-at-point-mode)) ;; Show previews for consult commands in embark collect
(use-package ace-window
:bind ("C-c o" . 'ace-window)
:init
(setq aw-dispatch-always t)
(setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)))
(use-package avy
:bind
("M-g g" . avy-goto-line) ;; Quickly jump to a line within the current window
("M-g w" . avy-goto-word-1) ;; Quickly jump to a word within the current window
("M-g c" . avy-goto-char) ;; Jump to a specific character in view
("M-o" . avy-goto-char-timer)) ;; Bind to M-o for quick character jumping
(use-package savehist
:init
(savehist-mode))
(use-package consult-dir
:bind (("C-x C-d" . consult-dir)))
;; Install and configure perspective.el with persistence
(use-package perspective
:custom
(persp-mode-prefix-key (kbd "C-c p")) ;; Change prefix if needed
(persp-state-default-file "~/.emacs.d/persp-state") ;; File to save workspace state
;; Enable saving and restoring of perspectives
(setq persp-state-default-file (expand-file-name "perspectives" user-emacs-directory))
(persp-state-save) ;; Automatically save on exit
(persp-state-load) ;; Automatically load on startup
(persp-mode-save-state-on-exit t) ;; Auto-save when exiting Emacs
:config
(persp-mode))
;; Install Projectile
(use-package projectile
:straight t
:config
(projectile-mode +1)
;; Set the main project directory or directories
(setq projectile-project-search-path '("~/projects")) ;; Customize to your project path
;; Use Vertico (or default completion) for Projectile
(setq projectile-completion-system 'default)
:bind-keymap
("C-c p" . projectile-command-map)) ;; Prefix for Projectile commands
;; Automatically create or switch to a perspective when switching projects
(defun my-projectile-perspective-switch (project)
"Switch to a perspective based on PROJECT name."
(let ((project-name (file-name-nondirectory (directory-file-name project))))
(persp-switch project-name)
(projectile-switch-project-by-name project)))
;; Automatically save and load perspectives on startup and exit
(defun my/load-perspectives ()
"Load perspectives from the last saved state."
(when (file-exists-p persp-state-default-file)
(persp-state-load persp-state-default-file)))
(add-hook 'emacs-startup-hook 'my/load-perspectives)
(add-hook 'kill-emacs-hook (lambda () (persp-state-save persp-state-default-file)))
(use-package magit
:commands magit-status
:config
(setq magit-display-buffer-function #'magit-display-buffer-fullframe-status-v1))
;; Org-mode configuration
(use-package org
:init
(visual-line-mode)
:config
(setq org-startup-indented t) ;; Enable indentation by default
(setq org-hide-leading-stars t)
(setq org-confirm-babel-evaluate nil)
(setq org-startup-with-inline-images t)
(setq org-startup-numerated t)
(setq org-ellipsis "...")
(setq org-directory "~/org/" ;; Directory for Org files
org-default-notes-file (concat org-directory "notes.org"))
(setq scroll-margin 2
scroll-conservatively 101
scroll-step 1)
:bind
("C-c c" . org-capture)) ;; Keybinding for org-capture
(use-package visual-fill-column
:custom
(visual-fill-column-center-text t)
(visual-fill-column-width 140)
:hook (org-mode . visual-fill-column-mode))
;; Customize specific keybindings in org-mode if desired
(with-eval-after-load 'evil-org
(evil-define-key 'normal evil-org-mode-map
(kbd "TAB") 'org-cycle ;; Make TAB cycle through content in normal mode
(kbd "M-h") 'org-metaleft ;; Promote heading
(kbd "M-l") 'org-metaright ;; Demote heading
(kbd "M-j") 'org-metadown ;; Move item down
(kbd "M-k") 'org-metaup)) ;; Move item up
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)))
;; Org Babel for code blocks
(use-package ob
:straight nil
:after (:all org)
:init
(require 'org-tempo)
(add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
(add-to-list 'org-structure-template-alist '("hs" . "src haskell"))
(add-to-list 'org-structure-template-alist '("sh" . "src shell"))
(add-to-list 'org-structure-template-alist '("py" . "src python"))
(add-to-list 'org-structure-template-alist '("uml" . "src plantuml"))
(setq org-confirm-babel-evaluate nil)
:config
;; load more languages for org-babel
(org-babel-do-load-languages
'org-babel-load-languages
'((shell . t)
(emacs-lisp . t)
(plantuml . t)
(dot . t)
(python . t)
(haskell . t)
(scheme . t)
)))
;; Org-roam configuration
(use-package org-roam
:init
(setq org-roam-directory (file-truename "~/org/roam"))
(setq org-roam-capture-templates
'(("l" "Literature Notes" plain
"%?"
:if-new (file+head "l-%<%Y%m%d%H%M%S>.org" "#+TITLE: ${title}\n")
:unnarrowed t)
("z" "Zettels" plain
"%?"
:if-new (file+head "z-%<%Y%m%d%H%M%S>.org" "#+TITLE: ${title}\n")
:unnarrowed t)
("s" "Structure Notes" plain
"%?"
:if-new (file+head "s-%<%Y%m%d%H%M%S>.org" "#+TITLE: ${title}\n")
:unnarrowed t)
("p" "Project Notes" plain
"%?"
:if-new (file+head "p-%<%Y%m%d%H%M%S>.org" "#+TITLE: ${title}\n")
:unnarrowed t)))
:config
;; Enable Org-roam DB autosync
(org-roam-db-autosync-mode)
;; Require org-id for handling IDs
(require 'org-id)
;; Ensure that Org uses IDs for internal links
(setq org-id-link-to-org-use-id t)
;; Track IDs globally across all Org files
;; (setq org-id-track-globally t)
;; Set the file where Org stores the IDs
(setq org-id-locations-file (expand-file-name ".org-id-locations" user-emacs-directory))
;; Automatically assign IDs to new headings when they are created
;; (add-hook 'org-insert-heading-hook 'org-id-get-create)
;; Optionally, provide a function to assign IDs to all headings in the current buffer
(defun my/org-add-ids-to-headings-in-file ()
"Add ID properties to all Org headings in the current file."
(interactive)
(org-map-entries 'org-id-get-create))
;; Bind a key to assign IDs to the current heading
(define-key org-mode-map (kbd "C-c n a") 'org-id-get-create)
:bind
;; Bind keys to commonly used Org-roam commands
(("C-c n f" . org-roam-node-find) ;; Find or create a node
("C-c n i" . org-roam-node-insert) ;; Insert a link to a node
("C-c n c" . org-roam-capture) ;; Capture a new note
("C-c n b" . org-roam-buffer-toggle) ;; Toggle backlinks buffer
("C-c n g" . org-roam-graph) ;; Generate a graph of notes
("C-c n a" . org-id-get-create)
("C-c n d" . org-roam-dailies-capture-today))) ;; Open today's daily note
(with-eval-after-load 'org-roam
(cl-defmethod org-roam-node-type ((node org-roam-node))
"Return the type of the NODE based on the first letter of the file name."
(let* ((filename (file-name-nondirectory (org-roam-node-file node)))
(first-letter (substring filename 0 1)))
(cond ((string-equal first-letter "z") "zettel")
((string-equal first-letter "p") "project")
((string-equal first-letter "l") "literature")
(t "unknown")))) ; default type if it doesn't match any of the cases
(setq org-roam-node-display-template
(concat "${type:15} ${title:75} " (propertize "${tags:75}" 'face 'org-tag))))
;; Org-roam-ui configuration
(use-package org-roam-ui
:after org-roam
:config
;; Synchronize theme with Emacs
(setq org-roam-ui-sync-theme t
org-roam-ui-follow t
org-roam-ui-update-on-save t))
Pulls the JSON from readwise
(require 'auth-source)
(require 'json)
(use-package request)
(defun get-readwise-token ()
"Retrieve the Readwise API token from ~/.authinfo."
(let ((secret (auth-source-search :host "readwise.io" :user "apikey" :require '(:secret))))
(if secret
(funcall (plist-get (car secret) :secret))
(error "No Readwise API token found in ~/.authinfo"))))
(defun format-iso8601-date (time)
"Format TIME as an ISO 8601 string."
(format-time-string "%Y-%m-%dT%H:%M:%SZ" time t))
(defun fetch-from-export-api (&optional updated-after)
"Fetch all of a user's highlights from Readwise's export API.
If UPDATED-AFTER is provided, format it to ISO 8601 for the query.
The results are saved to ~/org/readwise-highlights.json."
(let ((full-data '())
(next-page-cursor nil)
(token (get-readwise-token))
(formatted-date (when updated-after (format-iso8601-date updated-after))))
(cl-loop
do
(let ((params (append
(when next-page-cursor `(("pageCursor" . ,next-page-cursor)))
(when formatted-date `(("updatedAfter" . ,formatted-date))))))
(message "Making export API request with params %S..." params)
(request
"https://readwise.io/api/v2/export/"
:type "GET"
:params params
:headers `(("Authorization" . ,(concat "Token " token)))
:parser 'json-read
:sync t ;; synchronous for simplicity; can change to async if needed
:success (cl-function
(lambda (&key data &allow-other-keys)
(setq full-data (append full-data (alist-get 'results data)))
(setq next-page-cursor (alist-get 'nextPageCursor data))))
:error (cl-function
(lambda (&key error-thrown response &allow-other-keys)
(message "Error: %S" error-thrown)
(message "Response: %s" (request-response-data response))))))
until (not next-page-cursor))
;; Save the accumulated data to a JSON file
(let ((json-file "~/org/readwise-highlights.json"))
(with-temp-file json-file
(insert (json-encode `(("results" . ,full-data)))))
(message "Highlights saved to %s" json-file))
full-data))
;; Example usage:
;; Fetch all highlights from all time and save to JSON
;; (fetch-from-export-api)
;; Fetch highlights updated after a specific date and save to JSON
;; (fetch-from-export-api (format-iso8601-date (current-time)))
V2- save in order of location, not time
(require 'json)
(defun parse-readwise-json (file-path)
"Parse Readwise highlights from a local JSON file at FILE-PATH.
Returns a list of highlights in an Emacs Lisp structure."
(with-temp-buffer
(insert-file-contents file-path)
(let ((json-object-type 'alist) ;; JSON objects as alists
(json-array-type 'list)) ;; JSON arrays as lists
(json-read))))
(defun format-readwise-highlights-to-org (json-data output-file-path)
"Convert JSON-DATA from Readwise highlights to Org format and save it to OUTPUT-FILE-PATH."
(let ((org-content '()))
(dolist (entry (alist-get 'results json-data))
;; Book/Article Title and Metadata
(let ((title (or (alist-get 'title entry) "Untitled"))
(author (or (alist-get 'author entry) "Unknown"))
(source-url (or (alist-get 'source_url entry) "No URL"))
(summary (alist-get 'summary entry))
(highlights (alist-get 'highlights entry)))
(push (format "* %s\n :PROPERTIES:\n :AUTHOR: %s\n :URL: %s" title author source-url) org-content)
(when summary
(push (format " :SUMMARY: %s" summary) org-content))
(push " :END:\n" org-content)
;; Notes Section
(push "** Notes" org-content)
;; Sort the highlights by location
(setq highlights (sort highlights (lambda (a b)
(< (or (alist-get 'location a) 0)
(or (alist-get 'location b) 0)))))
(dolist (highlight highlights)
(let ((note (string-trim (or (alist-get 'note highlight) "")))
(highlight-text (string-trim (or (alist-get 'text highlight) "")))
(highlight-url (alist-get 'url highlight)))
;; Add note and supporting text
(if (not (string-empty-p note))
(progn
(push (format " - %s" note) org-content)
(push (format " - %s" highlight-text) org-content))
(push (format " - %s" highlight-text) org-content))
;; Format image links correctly
(when (and highlight-url (string-match-p (regexp-opt '(".jpg" ".png")) highlight-url))
(push (format " - [[%s]]" highlight-url) org-content))))
;; Links Section
(push "** Links\n" org-content)))
;; Save content to Org file
(with-temp-file output-file-path
(insert (mapconcat 'identity (nreverse org-content) "\n")))
(message "Highlights saved to %s" output-file-path)))
;; Example usage:
;; Load the JSON data from the file and convert it to Org format
(let ((json-data (parse-readwise-json "~/org/readwise-highlights.json"))
(output-file-path "~/org/readwise-highlights.org"))
(format-readwise-highlights-to-org json-data output-file-path))
Convert MD links to Org
(defun convert-markdown-images-to-org (&optional org-file)
"Convert Markdown-style image links  to Org-mode [[URL]] format.
If ORG-FILE is provided, modify that file; otherwise, modify the current buffer."
(interactive "fOrg file (optional): ")
(if org-file
(with-temp-buffer
(insert-file-contents org-file)
(goto-char (point-min))
(while (re-search-forward "!\\[.*?\\](\\(https?://[^)]+\\))" nil t)
(replace-match "[[\\1]]"))
(write-region (point-min) (point-max) org-file)
(message "Converted Markdown images in %s" org-file))
;; If no file is provided, run on the current buffer
(save-excursion
(goto-char (point-min))
(while (re-search-forward "!\\[.*?\\](\\(https?://[^)]+\\))" nil t)
(replace-match "[[\\1]]"))
(message "Converted Markdown images in the current buffer"))))
Fully integrated solution
(fetch-from-export-api)
;; Example usage:
;; Load the JSON data from the file and convert it to Org format
(let ((json-data (parse-readwise-json "~/org/readwise-highlights.json"))
(output-file-path "~/org/readwise-highlights.org"))
(format-readwise-highlights-to-org json-data output-file-path))
(convert-markdown-images-to-org "~/org/readwise-highlights.org")
(use-package ialign
:ensure t)
(use-package evil-nerd-commenter
:bind ("M-;" . 'evilnc-comment-or-uncomment-lines))
(use-package wgrep
:straight t
)
(use-package popper
:straight t
:init
;; Define buffers to treat as popups
(setq popper-reference-buffers
'("\\*Messages\\*"
"\\*Embark*"
"Output\\*$"
"eshell"
"\\*Async Shell Command\\*"
help-mode
compilation-mode
"^\\*projectile-scratch-buffer\\*"))
;; Group popups by Projectile projects
(setq popper-group-function #'popper-group-by-project)
:bind (("C-`" . popper-toggle)
("M-`" . popper-cycle)
("C-M-`" . popper-toggle-type))
:config
(popper-mode +1)
(popper-echo-mode +1))
(use-package modus-themes)
(use-package ef-themes)
(use-package modus-themes)
(use-package nano-theme
:straight (:host github :repo "rougier/nano-theme"))
(use-package telephone-line
:init
(setq telephone-line-primary-left-separator 'telephone-line-cubed-left
telephone-line-secondary-left-separator 'telephone-line-cubed-hollow-left
telephone-line-primary-right-separator 'telephone-line-cubed-right
telephone-line-secondary-right-separator 'telephone-line-cubed-hollow-right)
(setq telephone-line-height 24
telephone-line-evil-use-short-tag t)
(telephone-line-mode t))
(use-package all-the-icons)
(use-package nerd-icons-completion
:config
(nerd-icons-completion-mode))
(set-face-attribute 'default nil :font "Fira Code-10")
(use-package all-the-icons-completion
:straight t
:hook (marginalia-mode . all-the-icons-completion-marginalia-setup)
:init
(all-the-icons-completion-mode))
Save conversations