Skip to content

Latest commit

 

History

History
648 lines (578 loc) · 24.5 KB

emacs-init.org

File metadata and controls

648 lines (578 loc) · 24.5 KB

Emac’s Setup

Basic Setup

;; 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

Evil Mode

;; 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)))

Buffer Management

;; 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)))

Project Management

;; 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

Org-Mode

;; 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

;; 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

;; 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

;; 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))

Org-Readwise

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 ![](URL) 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")

Org-Download

Org-Read-ot-Later

Useful Packages

(use-package ialign
  :ensure t)
(use-package evil-nerd-commenter
  :bind ("M-;" . 'evilnc-comment-or-uncomment-lines))
(use-package wgrep
  :straight t
  )

Popper

(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))

Themes and Colors

(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))

File Navigating, Editing and Querying

Enable AI within Emacs

Save conversations

Grabbing articles from Internet

Other Evil packages

Copy/Download images and highlights

Git tracking

Dired