adding content
This commit is contained in:
5
Emacs/20241210004247-emacs_moc.org
Executable file → Normal file
5
Emacs/20241210004247-emacs_moc.org
Executable file → Normal file
@@ -14,8 +14,9 @@ The purpose of this file is to store things related to emacs (that being package
|
||||
- [[id:45CC3AF5-5E20-4B03-A36C-8D4BDD5CBB13][Emacs Evil Mode]]
|
||||
- [[id:7d2e867e-f091-4362-a583-453f732207fe][Emacs ORG Publish]]
|
||||
- [[id:168b6b10-d670-4f9d-89cf-aeb59aa644a1][Emacs Org Noter]]
|
||||
- [[id:10b1f18e-b221-4ab8-bce4-b6f20ad417db][emacs-calendar]]
|
||||
|
||||
- [[id:10b1f18e-b221-4ab8-bce4-b6f20ad417db][Emacs Calendar/Agenda]]
|
||||
- [[id:1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9][Emacs Config]]
|
||||
|
||||
* Resources
|
||||
- A link that contains all the key bindings for emacs: [[http://xahlee.info/emacs/emacs/emacs_keybinding_list.html][Keybindings list]]
|
||||
- the theme 'ef-dream' repository: [[https://github.com/protesilaos/ef-themes][ef-themes]]
|
||||
|
||||
0
Emacs/20241210004329-org_roam.org
Executable file → Normal file
0
Emacs/20241210004329-org_roam.org
Executable file → Normal file
0
Emacs/20241210004453-gtd.org
Executable file → Normal file
0
Emacs/20241210004453-gtd.org
Executable file → Normal file
3
Emacs/20250218174735-emacs_stuff_keybindings.org
Executable file → Normal file
3
Emacs/20250218174735-emacs_stuff_keybindings.org
Executable file → Normal file
@@ -16,3 +16,6 @@ Type `C-h m` to get information for a major mode.
|
||||
* Projectile:
|
||||
- `SPC-p p` - switch project
|
||||
- `SPC-p f` - find project
|
||||
|
||||
|
||||
* Use C-u SPC X P to run powershell with args
|
||||
|
||||
0
Emacs/20250326002128-emacs_stuff_elisp.org
Executable file → Normal file
0
Emacs/20250326002128-emacs_stuff_elisp.org
Executable file → Normal file
0
Emacs/20250329231658-emacs_stuff_magit.org
Executable file → Normal file
0
Emacs/20250329231658-emacs_stuff_magit.org
Executable file → Normal file
0
Emacs/20250420012258-emacs_stuff_evil.org
Executable file → Normal file
0
Emacs/20250420012258-emacs_stuff_evil.org
Executable file → Normal file
0
Emacs/20250806155335-emacs_stuff_org_publish.org
Executable file → Normal file
0
Emacs/20250806155335-emacs_stuff_org_publish.org
Executable file → Normal file
80
Emacs/20260401100130-emacs_org_noter.org
Normal file
80
Emacs/20260401100130-emacs_org_noter.org
Normal file
@@ -0,0 +1,80 @@
|
||||
:PROPERTIES:
|
||||
:ID: 168b6b10-d670-4f9d-89cf-aeb59aa644a1
|
||||
:END:
|
||||
#+title: Emacs Org Noter
|
||||
#+filetags: :emacs:org:notes:
|
||||
|
||||
**For the config, see [[id:1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9][Emacs Config]] for the Source of truth**
|
||||
|
||||
In order to create notes for a book:
|
||||
1. Create an empty org roam file
|
||||
2. Add the relative file path in metadata for that node, with the title ~NOTER_DOCUMENT~
|
||||
See the example below (auto save and page will get filled in)
|
||||
3. Hover over the Notes heading created and click ~SPC-n n~ to open org noter
|
||||
|
||||
#+begin_src org
|
||||
,* Ikigai Notes
|
||||
:PROPERTIES:
|
||||
:NOTER_DOCUMENT: ../../../pdfs/_misc/Ikigai.pdf
|
||||
:NOTER_AUTO_SAVE_LAST_LOCATION: t
|
||||
:NOTER_PAGE: 61
|
||||
:END:
|
||||
#+end_src
|
||||
|
||||
* Config needed:
|
||||
#+begin_src emacs-lisp
|
||||
|
||||
(setq org-noter-notes-search-path
|
||||
'("D:\\_nextcloud\\master-folder\\org_files\\org_roam\\"))
|
||||
(setq org-noter-always-create-frame nil)
|
||||
(setq org-noter-property-doc-file "NOTER_DOCUMENT")
|
||||
|
||||
(require 'pdf-tools)
|
||||
(pdf-tools-install)
|
||||
(use-package pdf-view-restore
|
||||
:after pdf-tools
|
||||
:config
|
||||
(add-hook 'pdf-view-mode-hook 'pdf-view-restore-mode)
|
||||
(setq pdf-view-restore-filename "~/.emacs.d/.pdf-view-restore"))
|
||||
|
||||
#+end_src
|
||||
|
||||
For non pdfs notes:
|
||||
#+begin_src emacs-lisp
|
||||
|
||||
(setq org-capture-templates
|
||||
'(("d" "Daily TODO" entry
|
||||
(file+headline (lambda () (z/find-current-week-file)) "TODOs")
|
||||
"* TODO %?\nSCHEDULED: %t\n"
|
||||
:empty-lines 1)
|
||||
|
||||
("c" "Add to calendar.org" entry
|
||||
(file+headline "~/master-folder/org_files/calendar.org" "Calendar")
|
||||
"* TODO %?\nSCHEDULED: %^t\n"
|
||||
:empty-lines 1)
|
||||
|
||||
("n" "Add notes" entry
|
||||
(file+headline (lambda () (z/find-current-week-file)) "Notes")
|
||||
"* Note: %?\n"
|
||||
:empty-lines 1)
|
||||
|
||||
("w" "Weekly Review" entry
|
||||
(file+function (lambda () (z/find-current-week-file)) z/goto-weekly-review)
|
||||
"* Review: %?\nEntered on %U\n"
|
||||
:empty-lines 1)
|
||||
|
||||
("b" "Book Note" entry
|
||||
(file+headline (lambda () (buffer-file-name)) "Notes")
|
||||
"** p. %^{Page}\n[%^{Type|Insight|Important|Quote|Confusing}] %?\n")
|
||||
))
|
||||
|
||||
#+end_src
|
||||
|
||||
* Keybindings:
|
||||
#+begin_src emacs-lisp
|
||||
(dt/leader-keys
|
||||
"n" '(:ignore t :wk "Org Noter")
|
||||
"n n" '(org-noter :wk "Open org-noter")
|
||||
)
|
||||
#+end_src
|
||||
|
||||
66
Emacs/20260401100839-emacs_calendar.org
Normal file
66
Emacs/20260401100839-emacs_calendar.org
Normal file
@@ -0,0 +1,66 @@
|
||||
:PROPERTIES:
|
||||
:ID: 10b1f18e-b221-4ab8-bce4-b6f20ad417db
|
||||
:END:
|
||||
#+title: Emacs Calendar/Agenda
|
||||
#+filetags: :emacs:workflow:work:
|
||||
|
||||
**For the config, see [[id:1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9][Emacs Config]] for the Source of truth**
|
||||
|
||||
* Config:
|
||||
#+begin_src emacs-lisp
|
||||
(require 'org-caldav)
|
||||
(require 'url)
|
||||
(require 'icalendar)
|
||||
(modify-coding-system-alist 'file "caldav-work\\.org\\'" 'utf-8-unix)
|
||||
(setq org-icalendar-timezone "Europe/London")
|
||||
(setq calendar-time-zone 60) ;; minutes offset from UTC (BST = +60)
|
||||
(setq calendar-standard-time-zone-name "GMT")
|
||||
(setq calendar-daylight-time-zone-name "BST")
|
||||
(setq org-caldav-timezone "Europe/London")
|
||||
(defvar my/outlook-ics-url
|
||||
"https://outlook.office365.com/owa/calendar/b910ea835e414663831e505f3d10baa2@microlise.com/d2568a0134fb49af92d39c1669c4729317662288011094411305/calendar.ics")
|
||||
|
||||
(defvar my/outlook-org-file
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-work.org")
|
||||
|
||||
(defvar my/ics-python-script
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\ics_to_org.py")
|
||||
|
||||
(defun my/sync-outlook-calendar ()
|
||||
"Fetch and expand Outlook ICS (including recurrences) into org file."
|
||||
(interactive)
|
||||
(message "Syncing Outlook calendar...")
|
||||
(let* ((cmd (format "python \"%s\" \"%s\" \"%s\""
|
||||
my/ics-python-script
|
||||
my/outlook-ics-url
|
||||
my/outlook-org-file))
|
||||
(result (shell-command-to-string cmd)))
|
||||
(message "Outlook calendar sync: %s" (string-trim result))))
|
||||
|
||||
;; Sync on startup and every 30 minutes
|
||||
(my/sync-outlook-calendar)
|
||||
(run-with-timer (* 30 60) (* 30 60) #'my/sync-outlook-calendar)
|
||||
;; --- CalDAV for native Nextcloud calendars ---
|
||||
(setq org-caldav-url "https://nextcloud.zainezq.com/remote.php/dav/calendars/zaine/")
|
||||
(setq org-caldav-calendars
|
||||
'((:calendar-id "personal"
|
||||
:inbox "D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-personal.org"
|
||||
:files nil)
|
||||
(:calendar-id "zxh"
|
||||
:inbox "D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-family.org"
|
||||
:files nil)))
|
||||
|
||||
;; --- Agenda files ---
|
||||
(setq org-agenda-files
|
||||
'("D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-personal.org"
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-work.org"
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-family.org"
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\org_roam\\Books\\20250724230557-books_org_agenda.org"))
|
||||
|
||||
|
||||
|
||||
#+end_src
|
||||
|
||||
** Explanation:
|
||||
- This configuration sets up Emacs to sync with both a Nextcloud calendar (via CalDAV) and an Outlook calendar (via ICS). It uses a Python script to fetch and convert the Outlook ICS data into an org file format that Emacs can read. The Nextcloud calendar is synced directly using the org-caldav package. Both calendars are included in the org-agenda files, so that I can view all events in one place. The sync for the Outlook calendar runs every 30 minutes to keep it up to date.
|
||||
- To sync the Outlook calendar, you call the ~my/sync-outlook-calendar~ function, which runs the Python script to fetch and convert the ICS data. The Nextcloud calendar is synced automatically by org-caldav whenever you open Emacs or refresh the calendar.
|
||||
599
Emacs/20260407083539-emacs_config.org
Normal file
599
Emacs/20260407083539-emacs_config.org
Normal file
@@ -0,0 +1,599 @@
|
||||
:PROPERTIES:
|
||||
:ID: 1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9
|
||||
:END:
|
||||
#+title: Emacs Config
|
||||
#+filetags: :emacs:org:functions:
|
||||
|
||||
|
||||
* Org Noter
|
||||
#+begin_src emacs-lisp
|
||||
|
||||
(setq org-noter-notes-search-path
|
||||
'("D:\\_nextcloud\\master-folder\\org_files\\org_roam\\"))
|
||||
(setq org-noter-always-create-frame nil)
|
||||
(setq org-noter-property-doc-file "NOTER_DOCUMENT")
|
||||
|
||||
(require 'pdf-tools)
|
||||
(pdf-tools-install)
|
||||
(use-package pdf-view-restore
|
||||
:after pdf-tools
|
||||
:config
|
||||
(add-hook 'pdf-view-mode-hook 'pdf-view-restore-mode)
|
||||
(setq pdf-view-restore-filename "~/.emacs.d/.pdf-view-restore"))
|
||||
|
||||
|
||||
|
||||
#+end_src
|
||||
|
||||
* CALDAV and org agenda calendars:
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(require 'org-caldav)
|
||||
(require 'url)
|
||||
(require 'icalendar)
|
||||
(modify-coding-system-alist 'file "caldav-work\\.org\\'" 'utf-8-unix)
|
||||
(setq org-icalendar-timezone "Europe/London")
|
||||
(setq calendar-time-zone 60) ;; minutes offset from UTC (BST = +60)
|
||||
(setq calendar-standard-time-zone-name "GMT")
|
||||
(setq calendar-daylight-time-zone-name "BST")
|
||||
(setq org-caldav-timezone "Europe/London")
|
||||
(defvar my/outlook-ics-url
|
||||
"https://outlook.office365.com/owa/calendar/b910ea835e414663831e505f3d10baa2@microlise.com/d2568a0134fb49af92d39c1669c4729317662288011094411305/calendar.ics")
|
||||
|
||||
(defvar my/outlook-org-file
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-work.org")
|
||||
|
||||
(defvar my/ics-python-script
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\ics_to_org.py")
|
||||
|
||||
(defun my/sync-outlook-calendar ()
|
||||
"Fetch and expand Outlook ICS (including recurrences) into org file."
|
||||
(interactive)
|
||||
(message "Syncing Outlook calendar...")
|
||||
(let* ((cmd (format "python \"%s\" \"%s\" \"%s\""
|
||||
my/ics-python-script
|
||||
my/outlook-ics-url
|
||||
my/outlook-org-file))
|
||||
(result (shell-command-to-string cmd)))
|
||||
(message "Outlook calendar sync: %s" (string-trim result))))
|
||||
|
||||
;; Sync on startup and every 30 minutes
|
||||
(my/sync-outlook-calendar)
|
||||
(run-with-timer (* 30 60) (* 30 60) #'my/sync-outlook-calendar)
|
||||
;; --- CalDAV for native Nextcloud calendars ---
|
||||
(setq org-caldav-url "https://nextcloud.zainezq.com/remote.php/dav/calendars/zaine/")
|
||||
(setq org-caldav-calendars
|
||||
'((:calendar-id "personal"
|
||||
:inbox "D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-personal.org"
|
||||
:files nil)
|
||||
(:calendar-id "zxh"
|
||||
:inbox "D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-family.org"
|
||||
:files nil)))
|
||||
|
||||
;; --- Agenda files ---
|
||||
(setq org-agenda-files
|
||||
'("D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-personal.org"
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-work.org"
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-family.org"
|
||||
"D:\\_nextcloud\\master-folder\\org_files\\org_roam\\Books\\20250724230557-books_org_agenda.org"))
|
||||
|
||||
|
||||
|
||||
#+end_src
|
||||
|
||||
* Notes when reading physical book
|
||||
#+begin_src emacs-lisp
|
||||
("b" "Book Note" entry
|
||||
(file+headline (lambda () (buffer-file-name)) "Notes")
|
||||
"** p. %^{Page}\n[%^{Type|Insight|Important|Quote|Confusing}] %?\n")
|
||||
|
||||
#+end_src
|
||||
|
||||
* Org image inserter:
|
||||
#+begin_src emacs-lisp
|
||||
;;; --- Org image inserter (improved search UX) -----------------------------
|
||||
(use-package vertico
|
||||
:init (vertico-mode))
|
||||
|
||||
(use-package orderless
|
||||
:custom
|
||||
(completion-styles '(orderless basic))
|
||||
(completion-category-defaults nil))
|
||||
|
||||
|
||||
(defgroup zq/org-assets nil
|
||||
"Insert image links from predefined asset profiles."
|
||||
:group 'org)
|
||||
|
||||
(defcustom zq/org-asset-profiles
|
||||
'(("org-roam" . "D:\\_nextcloud\\master-folder\\org_files\\org_roam\\assets")
|
||||
("org-web" . "D:\\_nextcloud\\master-folder\\org_files\\org_web\\assets"))
|
||||
"Alist of (PROFILE-NAME . ASSETS-DIR)."
|
||||
:type '(alist :key-type string :value-type directory)
|
||||
:group 'zq/org-assets)
|
||||
|
||||
(defcustom zq/org-image-extensions
|
||||
'("png" "jpg" "jpeg" "gif" "svg" "webp" "mov" "mp4" "mp3")
|
||||
"File extensions considered when offering candidates."
|
||||
:type '(repeat string)
|
||||
:group 'zq/org-assets)
|
||||
|
||||
(defun zq/org--image-regexp ()
|
||||
(concat "\\." (regexp-opt zq/org-image-extensions t) "\\'"))
|
||||
|
||||
(defun zq/org--image-files (dir)
|
||||
(when (and dir (file-directory-p dir))
|
||||
(directory-files-recursively dir (zq/org--image-regexp))))
|
||||
|
||||
(defun zq/org--parse-name (filename)
|
||||
"Split FILENAME into (DATE . REST)."
|
||||
(if (string-match "^\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)-\\(.*\\)" filename)
|
||||
(cons (match-string 1 filename)
|
||||
(match-string 2 filename))
|
||||
(cons nil filename)))
|
||||
|
||||
(defun zq/org--display-name (path)
|
||||
(let* ((name (file-name-nondirectory path))
|
||||
(dir (file-name-nondirectory (directory-file-name (file-name-directory path))))
|
||||
(parsed (zq/org--parse-name name))
|
||||
(date (car parsed))
|
||||
(rest (cdr parsed)))
|
||||
(format "%s (%s | %s)" rest (or date "no-date") dir)))
|
||||
|
||||
(defun zq/org--build-candidates (files)
|
||||
"Return an alist of (DISPLAY . FULLPATH), handling duplicates."
|
||||
(let ((table (make-hash-table :test 'equal))
|
||||
result)
|
||||
(dolist (path files)
|
||||
(let* ((display (zq/org--display-name path))
|
||||
(existing (gethash display table)))
|
||||
(if existing
|
||||
;; duplicate → include full filename to disambiguate
|
||||
(push (cons (file-name-nondirectory path) path) result)
|
||||
(puthash display t table)
|
||||
(push (cons display path) result))))
|
||||
result))
|
||||
|
||||
|
||||
(defun zq/org-insert-asset-image (&optional profile use-file-prefix)
|
||||
"Insert an Org link to an image chosen from a PROFILE’s assets dir.
|
||||
|
||||
Features:
|
||||
- Fuzzy search (with Vertico/Orderless/Ivy/Helm)
|
||||
- Clean names (date stripped)
|
||||
- Relative paths inserted
|
||||
|
||||
With prefix arg (C-u), use file: links."
|
||||
(interactive
|
||||
(list (completing-read
|
||||
"Profile: "
|
||||
(mapcar #'car zq/org-asset-profiles)
|
||||
nil t nil nil "org-roam")
|
||||
current-prefix-arg))
|
||||
(let* ((base-dir (cdr (assoc profile zq/org-asset-profiles)))
|
||||
(_ (unless (and base-dir (file-directory-p base-dir))
|
||||
(user-error "Invalid directory for profile '%s'" profile)))
|
||||
|
||||
(files (or (zq/org--image-files base-dir)
|
||||
(user-error "No assets found in %s" base-dir)))
|
||||
|
||||
(candidates (zq/org--build-candidates files))
|
||||
|
||||
;; user selects clean name, we resolve full path
|
||||
(selected (completing-read "Image: " candidates nil t))
|
||||
(abs-path (cdr (assoc selected candidates)))
|
||||
|
||||
(here (or (buffer-file-name) default-directory))
|
||||
(rel (file-relative-name abs-path (file-name-directory here)))
|
||||
|
||||
(link (format "[[%s%s]]"
|
||||
(if use-file-prefix "file:" "")
|
||||
rel)))
|
||||
|
||||
(insert link)
|
||||
|
||||
(when (derived-mode-p 'org-mode)
|
||||
(org-display-inline-images t t))))
|
||||
|
||||
#+end_src
|
||||
|
||||
* Weekly org review file:
|
||||
#+begin_src emacs-lisp
|
||||
;; Create weekly review files for Sundays, with catch-up command to fill in any missing ones up to the current week.
|
||||
(require 'seq)
|
||||
|
||||
(defvar zaine/weekly-review-base-dir
|
||||
"D:/_nextcloud/master-folder/org_files/org_web/blogs/"
|
||||
"Base directory for weekly review blog entries.")
|
||||
|
||||
(defun zaine/weekly-review--month-dir (time)
|
||||
"Return the directory for TIME in the format YYYY/MM-month."
|
||||
(let ((year (format-time-string "%Y" time))
|
||||
(month-num (format-time-string "%m" time))
|
||||
(month-name (downcase (format-time-string "%B" time))))
|
||||
(expand-file-name (format "%s/%s-%s" year month-num month-name)
|
||||
zaine/weekly-review-base-dir)))
|
||||
|
||||
(defun zaine/weekly-review--normalize-to-review-time (time)
|
||||
"Normalize TIME to 12:00 on the same date."
|
||||
(let* ((decoded (decode-time time))
|
||||
(day (nth 3 decoded))
|
||||
(month (nth 4 decoded))
|
||||
(year (nth 5 decoded)))
|
||||
(encode-time 0 0 12 day month year)))
|
||||
|
||||
(defun zaine/weekly-review--sunday-of-week (&optional time)
|
||||
"Return the most recent Sunday on or before TIME, normalized to 12:00."
|
||||
(let* ((time (or time (current-time)))
|
||||
(decoded (decode-time time))
|
||||
(dow (nth 6 decoded))
|
||||
(day (nth 3 decoded))
|
||||
(month (nth 4 decoded))
|
||||
(year (nth 5 decoded)))
|
||||
(encode-time 0 0 12 (- day dow) month year)))
|
||||
|
||||
(defun zaine/weekly-review--next-sunday (&optional time)
|
||||
"Return the next Sunday strictly after TIME, normalized to 12:00.
|
||||
If TIME is a Sunday, returns the following Sunday."
|
||||
(let* ((time (or time (current-time)))
|
||||
(decoded (decode-time time))
|
||||
(dow (nth 6 decoded))
|
||||
(days-ahead (if (= dow 0) 7 (- 7 dow))))
|
||||
(zaine/weekly-review--normalize-to-review-time
|
||||
(time-add time (days-to-time days-ahead)))))
|
||||
|
||||
(defun zaine/weekly-review--coming-sunday (&optional time)
|
||||
"Return the coming Sunday relative to TIME, normalized to 12:00.
|
||||
If TIME is a Sunday, return that day. Otherwise return the next Sunday."
|
||||
(let* ((time (or time (current-time)))
|
||||
(decoded (decode-time time))
|
||||
(dow (nth 6 decoded)))
|
||||
(if (= dow 0)
|
||||
(zaine/weekly-review--normalize-to-review-time time)
|
||||
(zaine/weekly-review--next-sunday time))))
|
||||
|
||||
(defun zaine/weekly-review--file-name (time)
|
||||
"Return the weekly review filename for TIME."
|
||||
(format-time-string "%d-%m-week-review.org" time))
|
||||
|
||||
(defun zaine/weekly-review--slug (time)
|
||||
"Return the weekly review slug for TIME."
|
||||
(format-time-string "%d-%m-%y-week-review" time))
|
||||
|
||||
(defun zaine/weekly-review--title (time)
|
||||
"Return the title for TIME."
|
||||
(format "%s - Weekly Review"
|
||||
(format-time-string "[%d-%m-%Y]" time)))
|
||||
|
||||
(defun zaine/weekly-review--header (time)
|
||||
"Return Org header text for weekly review at TIME."
|
||||
(concat
|
||||
(format "#+TITLE: %s\n" (zaine/weekly-review--title time))
|
||||
"#+OPTIONS: num:nil\n"
|
||||
(format "#+DATE: %s\n" (format-time-string "<%Y-%m-%d %a 12:00>" time))
|
||||
"#+filetags: :review:\n"
|
||||
"#+COMMENTS: t\n"
|
||||
(format "#+SLUG: %s\n\n" (zaine/weekly-review--slug time))))
|
||||
|
||||
(defun zaine/weekly-review--all-review-files ()
|
||||
"Return all weekly review files under `zaine/weekly-review-base-dir`."
|
||||
(when (file-directory-p zaine/weekly-review-base-dir)
|
||||
(directory-files-recursively
|
||||
zaine/weekly-review-base-dir
|
||||
"^[0-9]\\{2\\}-[0-9]\\{2\\}-week-review\\.org$")))
|
||||
|
||||
(defun zaine/weekly-review--extract-date-from-file (file)
|
||||
"Extract review date from FILE path.
|
||||
Expected path like:
|
||||
.../blogs/YYYY/MM-month/DD-MM-week-review.org"
|
||||
(when (string-match
|
||||
"/blogs/\\([0-9]\\{4\\}\\)/[0-9]\\{2\\}-[[:lower:]]+/\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)-week-review\\.org$"
|
||||
file)
|
||||
(let* ((year (string-to-number (match-string 1 file)))
|
||||
(day (string-to-number (match-string 2 file)))
|
||||
(month (string-to-number (match-string 3 file))))
|
||||
(encode-time 0 0 12 day month year))))
|
||||
|
||||
(defun zaine/weekly-review--latest-existing-sunday ()
|
||||
"Return the latest Sunday review time found, or nil if none exist."
|
||||
(let* ((files (zaine/weekly-review--all-review-files))
|
||||
(dates (delq nil (mapcar #'zaine/weekly-review--extract-date-from-file files))))
|
||||
(when dates
|
||||
(seq-reduce
|
||||
(lambda (latest current)
|
||||
(if (time-less-p latest current) current latest))
|
||||
(cdr dates)
|
||||
(car dates)))))
|
||||
|
||||
(defun zaine/weekly-review--create-file-for-sunday (sunday)
|
||||
"Create a weekly review file for SUNDAY if it doesn't exist.
|
||||
Opens the file and returns its path. If it already exists, just opens it."
|
||||
(let* ((target-dir (zaine/weekly-review--month-dir sunday))
|
||||
(file-name (zaine/weekly-review--file-name sunday))
|
||||
(file-path (expand-file-name file-name target-dir)))
|
||||
(make-directory target-dir t)
|
||||
(unless (file-exists-p file-path)
|
||||
(with-temp-file file-path
|
||||
(insert (zaine/weekly-review--header sunday))))
|
||||
file-path))
|
||||
|
||||
;;; Public commands
|
||||
|
||||
(defun zaine/weekly-review-catchup ()
|
||||
"Create all missing weekly review files from the oldest gap up to this week's Sunday.
|
||||
If today is Monday 30 March, catches up through Sunday 29 March.
|
||||
Creates one file per call so you can review each before continuing."
|
||||
(interactive)
|
||||
(let* ((past-sunday (zaine/weekly-review--sunday-of-week (current-time)))
|
||||
(latest (zaine/weekly-review--latest-existing-sunday))
|
||||
(next-needed (if latest
|
||||
(zaine/weekly-review--normalize-to-review-time
|
||||
(time-add latest (days-to-time 7)))
|
||||
past-sunday)))
|
||||
(if (time-less-p past-sunday next-needed)
|
||||
(message "Weekly reviews are already up to date through %s."
|
||||
(format-time-string "%d-%m-%Y" past-sunday))
|
||||
(let ((file-path (zaine/weekly-review--create-file-for-sunday next-needed)))
|
||||
(find-file file-path)
|
||||
(message "Created missing review for Sunday %s — call again to catch up further."
|
||||
(format-time-string "%d-%m-%Y" next-needed))))))
|
||||
|
||||
(defun zaine/weekly-review-create-coming ()
|
||||
"Create the weekly review for the coming Sunday.
|
||||
If today is Wednesday 1 April, creates the file for Sunday 5 April
|
||||
in the April directory, creating it if needed.
|
||||
If today is Sunday, creates today's review.
|
||||
If the file already exists, just opens it."
|
||||
(interactive)
|
||||
(let* ((coming-sunday (zaine/weekly-review--coming-sunday (current-time)))
|
||||
(file-path (zaine/weekly-review--create-file-for-sunday coming-sunday)))
|
||||
(find-file file-path)
|
||||
(message "%s review for Sunday %s."
|
||||
(if (file-exists-p file-path) "Opened existing" "Created")
|
||||
(format-time-string "%d-%m-%Y" coming-sunday))))
|
||||
|
||||
(global-set-key (kbd "C-c w r") #'zaine/weekly-review-catchup)
|
||||
(global-set-key (kbd "C-c w c") #'zaine/weekly-review-create-coming)
|
||||
|
||||
#+end_src
|
||||
|
||||
* Insert org web entries
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
;; Create org web entries for blogs and posts with a consistent template and naming convention.
|
||||
(defun zaine/create-org-web-entry ()
|
||||
"Create a new org web entry in either blogs or posts."
|
||||
(interactive)
|
||||
(let* ((entry-type (completing-read "Choose type (blogs/posts): " '("blogs" "posts") nil t))
|
||||
(slug (read-string "Filename / slug: "))
|
||||
(title (read-string "Title: "))
|
||||
(now (current-time))
|
||||
(date-for-title (format-time-string "[%d-%m-%Y]" now))
|
||||
(date-for-org (format-time-string "<%Y-%m-%d %a %H:%M>" now))
|
||||
(year (format-time-string "%Y" now))
|
||||
(month-num (format-time-string "%m" now))
|
||||
(month-name (downcase (format-time-string "%B" now)))
|
||||
(blog-dir (format "D:/_nextcloud/master-folder/org_files/org_web/blogs/%s/%s-%s"
|
||||
year month-num month-name))
|
||||
(post-dir "D:/_nextcloud/master-folder/org_files/org_web/posts/career")
|
||||
(target-dir (if (string= entry-type "blogs") blog-dir post-dir))
|
||||
(file-path (expand-file-name (concat slug ".org") target-dir))
|
||||
(filetags (if (string= entry-type "blogs") ":life:" ":post:")))
|
||||
|
||||
;; Create directory if it does not exist
|
||||
(unless (file-directory-p target-dir)
|
||||
(make-directory target-dir t))
|
||||
|
||||
;; Open file and insert template if new
|
||||
(find-file file-path)
|
||||
(when (= (buffer-size) 0)
|
||||
(insert (format "#+TITLE: %s\n" title))
|
||||
(insert "#+OPTIONS: num:nil\n")
|
||||
(insert (format "#+DATE: %s\n" date-for-org))
|
||||
(insert (format "#+filetags: %s\n" filetags))
|
||||
(insert "#+COMMENTS: t\n")
|
||||
(insert (format "#+SLUG: %s\n\n" slug))
|
||||
(save-buffer))))
|
||||
|
||||
#+end_src
|
||||
|
||||
* Insert org macros:
|
||||
#+begin_src emacs-lisp
|
||||
(defun org-insert-macro ()
|
||||
"Interactively select and insert an Org macro with prompted values."
|
||||
(interactive)
|
||||
(let* ((macro-defs
|
||||
'(("sidenote" . ("ID" "Content"))
|
||||
("epigraph" . ("Quote" "Footer"))
|
||||
("epigraph_single" . ("Quote"))
|
||||
("epigraph3" . ("Quote" "Author" "Work"))
|
||||
("kbd" . ("Key"))
|
||||
("margimg" . ("Image URL" "Alt text" "Caption (optional)"))
|
||||
("countdown" . ("ISO datetime (e.g. 2025-12-31T00:00:00)" "Label"))))
|
||||
(name (completing-read "Insert macro: " (mapcar #'car macro-defs) nil t))
|
||||
(args (cdr (assoc name macro-defs)))
|
||||
(values (mapcar (lambda (prompt)
|
||||
(read-string (format "%s: " prompt)))
|
||||
args))
|
||||
(macro (format "{{{%s(%s)}}}"
|
||||
name
|
||||
(mapconcat #'identity values ","))))
|
||||
(insert macro)))
|
||||
|
||||
(define-key org-mode-map (kbd "C-c m") #'org-insert-macro)
|
||||
#+end_src
|
||||
|
||||
* Old stuff:
|
||||
** org image inserter:
|
||||
#+begin_src emacs-lisp
|
||||
;; Core group & settings (dedupe: define these only once)
|
||||
;; (defgroup zq/org-assets nil
|
||||
;; "Insert image links from predefined asset profiles."
|
||||
;; :group 'org)
|
||||
|
||||
;; (defcustom zq/org-asset-profiles
|
||||
;; '(("org-roam" . "D:\\_nextcloud\\master-folder\\org_files\\org_roam\\assets")
|
||||
;; ("org-web" . "D:\\_nextcloud\\master-folder\\org_files\\org_web\\assets"))
|
||||
;; "Alist of (PROFILE-NAME . ASSETS-DIR)."
|
||||
;; :type '(alist :key-type string :value-type directory)
|
||||
;; :group 'zq/org-assets)
|
||||
|
||||
;; (defcustom zq/org-image-extensions '("png" "jpg" "jpeg" "gif" "svg" "webp" "mov" "mp4" "mp3")
|
||||
;; "Image file extensions considered when offering candidates."
|
||||
;; :type '(repeat string)
|
||||
;; :group 'zq/org-assets)
|
||||
|
||||
;; (defun zq/org--image-regexp ()
|
||||
;; "Build a regexp that matches configured image extensions."
|
||||
;; (concat "\\." (regexp-opt zq/org-image-extensions t) "\\'"))
|
||||
|
||||
;; (defun zq/org--image-files (dir)
|
||||
;; "Return a list of image files under DIR (recursively)."
|
||||
;; (when (and dir (file-directory-p dir))
|
||||
;; (directory-files-recursively dir (zq/org--image-regexp))))
|
||||
|
||||
;; (defun zq/org-insert-asset-image (&optional profile use-file-prefix)
|
||||
;; "Insert an Org link to an image chosen from a PROFILE’s assets dir.
|
||||
|
||||
;; Interactively:
|
||||
;; - Prompts for PROFILE (from `zq/org-asset-profiles`).
|
||||
;; - Presents only image files under that profile’s assets directory.
|
||||
;; - Inserts a relative link like [[path/to/image.png]].
|
||||
;; - With prefix arg (C-u), uses [[file:path/to/image.png]].
|
||||
|
||||
;; Non-interactively:
|
||||
;; PROFILE is the profile name string, USE-FILE-PREFIX non-nil forces file: prefix."
|
||||
;; (interactive
|
||||
;; (list (completing-read "Profile: "
|
||||
;; (mapcar #'car zq/org-asset-profiles)
|
||||
;; nil t nil nil "org-roam")
|
||||
;; current-prefix-arg))
|
||||
;; (let* ((base-dir (cdr (assoc profile zq/org-asset-profiles)))
|
||||
;; (_ (unless (and base-dir (file-directory-p base-dir))
|
||||
;; (user-error "Profile '%s' has no valid directory: %s" profile base-dir)))
|
||||
;; (candidates (or (zq/org--image-files base-dir)
|
||||
;; (user-error "No images found in %s" base-dir)))
|
||||
;; (abs-path (expand-file-name (completing-read "Image: " candidates nil t)))
|
||||
;; (here (or (buffer-file-name) default-directory))
|
||||
;; (rel (file-relative-name abs-path (file-name-directory here)))
|
||||
;; (link (format "[[%s%s]]"
|
||||
;; (if use-file-prefix "file:" "")
|
||||
;; rel)))
|
||||
;; (insert link)
|
||||
;; (when (derived-mode-p 'org-mode)
|
||||
;; (org-display-inline-images t t))))
|
||||
|
||||
|
||||
;;; --- Robust paste-into-profile without org-download--clipboard-image-p ---
|
||||
|
||||
;; (require 'url) ;; for url-copy-file
|
||||
;; (require 'org-download) ;; for org-download-clipboard
|
||||
|
||||
;; (defun zq/org--ensure-dir (dir)
|
||||
;; "Ensure DIR exists and is a directory; error if invalid."
|
||||
;; (unless (and dir (file-directory-p dir))
|
||||
;; (user-error "Profile directory invalid or not found: %s" dir))
|
||||
;; (make-directory dir t)
|
||||
;; dir)
|
||||
|
||||
;; (defun zq/org--unique-image-filename (&optional ext)
|
||||
;; "Return a unique filename like 20260227_160404.png. Default EXT is png."
|
||||
;; (concat (format-time-string "%Y%m%d_%H%M%S") "." (or ext "png")))
|
||||
|
||||
;; (defun zq/org--rel-link-to (abs-path)
|
||||
;; "Return a relative Org link for ABS-PATH from current buffer."
|
||||
;; (let* ((here (or (buffer-file-name) default-directory))
|
||||
;; (rel (file-relative-name abs-path (file-name-directory here))))
|
||||
;; (format "[[%s]]" rel)))
|
||||
|
||||
;; (defun zq/org--clipboard-text ()
|
||||
;; "Return current clipboard text (or nil) without altering kill ring."
|
||||
;; (let ((str (current-kill 0 t)))
|
||||
;; (when (and str (stringp str) (> (length str) 0)) str)))
|
||||
|
||||
;; (defun zq/org--grab-clipboard-image-to-temp ()
|
||||
;; "Try to capture clipboard image into a temporary PNG file.
|
||||
;; Return the temp filename on success; nil otherwise. Temp file is caller-managed."
|
||||
;; (let ((tmp (make-temp-file "zq-clip-" nil ".png"))
|
||||
;; (ok nil))
|
||||
;; (unwind-protect
|
||||
;; (progn
|
||||
;; ;; Try to save clipboard to TMP; some systems signal error when no image
|
||||
;; (condition-case _err
|
||||
;; (org-download-clipboard tmp)
|
||||
;; (error
|
||||
;; ;; ignore; ok remains nil
|
||||
;; ))
|
||||
;; ;; Validate: file exists, non-empty, and Emacs recognizes image type
|
||||
;; (when (and (file-exists-p tmp)
|
||||
;; (> (nth 7 (file-attributes tmp)) 0)
|
||||
;; (ignore-errors (image-type-from-file tmp)))
|
||||
;; (setq ok t)))
|
||||
;; (unless ok
|
||||
;; (when (file-exists-p tmp) (delete-file tmp))))
|
||||
;; (when ok tmp)))
|
||||
|
||||
;; (defun zq/org--save-data-uri (data-uri target)
|
||||
;; "Save a data:image/...;base64,... DATA-URI to TARGET."
|
||||
;; (unless (string-match "\\`data:image/\\([a-zA-Z0-9+.-]+\\);base64,\\(.*\\)\\'" data-uri)
|
||||
;; (user-error "Clipboard looks like a data URI but not an image/base64"))
|
||||
;; (let ((bytes (base64-decode-string (match-string 2 data-uri))))
|
||||
;; (with-temp-file target
|
||||
;; (set-buffer-multibyte nil)
|
||||
;; (insert bytes))))
|
||||
|
||||
;; (defun zq/org--download-url-to-file (url target)
|
||||
;; "Download image from URL to TARGET (synchronously)."
|
||||
;; (url-copy-file url target t))
|
||||
|
||||
;; (defun zq/org-paste-image-into-profile (&optional profile)
|
||||
;; "Paste image (clipboard/URL/data-URI) into PROFILE assets and insert a relative Org link."
|
||||
;; (interactive
|
||||
;; (list (completing-read "Profile: "
|
||||
;; (mapcar #'car zq/org-asset-profiles)
|
||||
;; nil t nil nil "org-roam")))
|
||||
;; (let* ((assets-dir (cdr (assoc profile zq/org-asset-profiles))))
|
||||
;; (zq/org--ensure-dir assets-dir)
|
||||
|
||||
;; (let* ((filename (zq/org--unique-image-filename "png"))
|
||||
;; (target (expand-file-name filename assets-dir))
|
||||
;; (handled nil))
|
||||
|
||||
;; ;; 1) Preferred: raw image from clipboard via org-download backends
|
||||
;; (let ((tmp (zq/org--grab-clipboard-image-to-temp)))
|
||||
;; (when tmp
|
||||
;; (rename-file tmp target t) ;; move tmp → target
|
||||
;; (setq handled t)))
|
||||
|
||||
;; ;; 2) If no raw image, try clipboard text: data URI or URL
|
||||
;; (unless handled
|
||||
;; (let ((clip (zq/org--clipboard-text)))
|
||||
;; (cond
|
||||
;; ;; data URI
|
||||
;; ((and clip (string-prefix-p "data:image/" clip))
|
||||
;; (zq/org--save-data-uri clip target)
|
||||
;; (setq handled t))
|
||||
;; ;; http(s) URL
|
||||
;; ((and clip (string-match-p "\\`https?://" clip))
|
||||
;; (zq/org--download-url-to-file clip target)
|
||||
;; (setq handled t)))))
|
||||
|
||||
;; (unless handled
|
||||
;; (user-error
|
||||
;; (concat
|
||||
;; "No image found in clipboard.\n\n"
|
||||
;; "Tips:\n"
|
||||
;; "• In your browser, use **Copy image** (not “Copy image address”)\n"
|
||||
;; "• Or copy from an image viewer / screenshot tool (to clipboard)\n"
|
||||
;; "• On Wayland: install `wl-clipboard` (provides `wl-paste`)\n"
|
||||
;; "• On X11: install `xclip` or `xsel`\n"
|
||||
;; "• Alternatively: copy an image URL or a data:image/...;base64,... and retry")))
|
||||
|
||||
;; ;; Insert the relative link and display inline
|
||||
;; (insert (zq/org--rel-link-to target))
|
||||
;; (when (derived-mode-p 'org-mode)
|
||||
;; (org-display-inline-images t t))
|
||||
;; (message "Saved image → %s" target))))
|
||||
|
||||
|
||||
#+end_src
|
||||
Reference in New Issue
Block a user