feat(emacs): first version of review workflow

This commit is contained in:
Pavel Korytov 2021-09-03 13:42:42 +05:00
parent d6d6f40f2c
commit 50979e53c7
2 changed files with 422 additions and 9 deletions

View file

@ -1567,6 +1567,188 @@ then it takes a second \\[keyboard-quit] to abort the minibuffer."
((tags-todo "personal"
((org-agenda-prefix-format " %i %-12:c [%e] ")))))))
(use-package org-ql
:straight (:fetcher github
:repo "alphapapa/org-ql"
:files (:defaults (:exclude "helm-org-ql.el"))))
(setq my/git-diff-status
'(("A" . added)
("C" . copied)
("D" . deleted)
("M" . modified)
("R" . renamed)
("T" . type-changed)
("U" . unmerged)))
(defun my/get-files-status (rev)
(let ((files (shell-command-to-string (concat "git diff --name-status " rev))))
(mapcar
(lambda (file)
(let ((elems (split-string file "\t")))
(cons
(cdr (assoc (car elems) my/git-diff-status))
(nth 1 elems))))
(split-string files "\n" t))))
(defun my/org-changed-files-since-date (date)
(let ((default-directory org-directory))
(my/get-files-status (format "@{%s}" date))))
(defun my/org-review-format-roam (rev)
(let* ((changes (my/org-changed-files-since-date rev))
(new-roam
(seq-filter
(lambda (elem)
(and (eq (car elem) 'added)
(string-match-p (rx bos "roam") (cdr elem))))
changes))
(changed-roam
(seq-filter
(lambda (elem)
(and (eq (car elem) 'modified)
(string-match-p (rx bos "roam") (cdr elem))))
changes)))
(concat
(unless (seq-empty-p new-roam)
(concat "** New Roam entries \n"
(mapconcat
(lambda (entry)
(format "- [[file:%s][%s]]" (cdr entry) (cdr entry)))
new-roam
"\n")
"\n"))
(unless (seq-empty-p changed-roam)
(concat "** Changed Roam entries \n"
(mapconcat
(lambda (entry)
(format "- [[file:%s][%s]]" (cdr entry) (cdr entry)))
changed-roam
"\n"))))))
(defun my/org-journal-entries-since-date (rev-date)
(mapcar
(lambda (date)
(let ((time (encode-time (parse-time-string date))))
`((file . ,(org-journal--get-entry-path time))
(header . ,(format-time-string org-journal-date-format time)))))
(seq-filter
(lambda (date) (string-lessp rev-date date))
(mapcar
(lambda (date)
(format "%04d-%02d-%02dT00:00:00+0300" (nth 2 date) (nth 0 date) (nth 1 date)))
(org-journal--list-dates)))))
(defun my/org-review-format-journal (rev-date)
(mapconcat
(lambda (item)
(format "- [[file:%s::*%s][%s]]"
(cdr (assoc 'file item))
(cdr (assoc 'header item))
(cdr (assoc 'header item))))
(my/org-journal-entries-since-date rev-date)
"\n"))
(setq my/org-ql-review-queries
`(("Waitlist" scheduled scheduled
(and
(done)
(tags-inherited "waitlist")))
("Personal tasks done" closed ,nil
(and
(tags-inherited "personal")
(todo "DONE")))
("Attended meetings" closed scheduled
(and
(tags "meeting")
(todo "PASSED")))
("Done project tasks" closed deadline
(and
(todo "DONE")
(ancestors
(heading "Tasks"))))))
(defun my/org-review-exec-ql (saved rev-date)
(let ((query `(and
(,(nth 1 saved) :from ,rev-date)
,(nth 3 saved))))
(org-ql-query
:select #'element
:from (org-agenda-files)
:where query
:order-by (nth 2 saved))))
(defun my/org-review-format-element (elem)
(concat
(string-pad
(plist-get (cadr elem) :raw-value)
40)
(when-let (scheduled (plist-get (cadr elem) :scheduled))
(concat " [SCHEDULED: " (plist-get (cadr scheduled) :raw-value) "]"))
(when-let (deadline (plist-get (cadr elem) :deadline))
(concat " [DEADLINE: " (plist-get (cadr deadline) :raw-value) "]"))))
(defun my/org-review-format-queries (rev-date)
(mapconcat
(lambda (results)
(concat "** " (car results) "\n"
(string-join
(mapcar (lambda (r) (concat "- " r)) (cdr results))
"\n")
"\n"))
(seq-filter
(lambda (result)
(not (seq-empty-p (cdr result))))
(mapcar
(lambda (saved)
(cons
(car saved)
(mapcar
#'my/org-review-format-element
(my/org-review-exec-ql saved rev-date))))
my/org-ql-review-queries))
"\n"))
(setq my/org-review-directory "review")
(defun my/org-review-get-filename ()
(concat my/org-review-directory "/" (format-time-string "%Y-%m-%d.org" (current-time))))
(defun my/get-last-review-date ()
(substring
(or
(-max-by
'string-greaterp
(-filter
(lambda (f) (not (or (string-equal f ".") (string-equal f ".."))))
(directory-files (f-join org-directory my/org-review-directory))))
(format-time-string
"%Y-%m-%d"
(time-subtract
(current-time)
(seconds-to-time (* 60 60 24 7)))))
0 10))
(setq my/org-review-capture-template
`("r" "Review" plain (file ,(my/org-review-get-filename))
,(string-join
'("#+TITLE: Review %t"
""
"Last review date: %(org-timestamp-translate (org-timestamp-from-string (format \"<%s>\" (my/get-last-review-date))))"
""
"* Roam"
"%(my/org-review-format-roam (my/get-last-review-date))"
"* Journal"
"New journal entries:"
"%(my/org-review-format-journal (my/get-last-review-date))"
"* Agenda"
"%(my/org-review-format-queries (my/get-last-review-date))"
"* Thoughts :crypt:"
"%?")
"\n")))
(add-to-list 'org-capture-templates my/org-review-capture-template t)
(use-package org-journal
:straight t
:after org
@ -2090,8 +2272,7 @@ then it takes a second \\[keyboard-quit] to abort the minibuffer."
:config
(add-hook 'web-mode-hook 'smartparens-mode)
(add-hook 'web-mode-hook 'hs-minor-mode)
(my/set-smartparens-indent 'web-mode)
(add-hook 'web-mode-hook ))
(my/set-smartparens-indent 'web-mode))
(setq my/web-mode-lsp-extensions
`(,(rx ".svelte" eos)
@ -3001,12 +3182,13 @@ then it takes a second \\[keyboard-quit] to abort the minibuffer."
(let ((track (emms-track
'url (my/get-youtube-url (elfeed-entry-link entry)))))
(emms-track-set track 'info-title (elfeed-entry-title entry))
(setq my/test track)
(emms-playlist-insert-track track))))
(defun my/elfeed-add-emms-youtube ()
(interactive)
(emms-add-elfeed elfeed-show-entry))
(emms-add-elfeed elfeed-show-entry)
(elfeed-tag elfeed-show-entry 'watched)
(elfeed-show-refresh))
(with-eval-after-load 'elfeed
(general-define-key

241
Emacs.org
View file

@ -2531,6 +2531,237 @@ Log DONE time
((tags-todo "personal"
((org-agenda-prefix-format " %i %-12:c [%e] ")))))))
#+end_src
*** org-ql
[[https://github.com/alphapapa/org-ql][org-ql]] is a package to query the org files. I'm using it in my review workflow, perhaps later I'll find another usecases.
#+begin_src emacs-lisp
(use-package org-ql
:straight (:fetcher github
:repo "alphapapa/org-ql"
:files (:defaults (:exclude "helm-org-ql.el"))))
#+end_src
*** Review workflow
My take on a review workflow. As a baseline, I want to have a template that lists the important changes since the last review and other basic information. I'm doing reviews regularly, but the time intervals still may vary, hence this flexibility.
**** Data from git & org-roam
First, as I have [[file:Console.org::=autocommit=][autocommit]] set up in my org directory, here is a handy function to get an alist of changed files of a form =(status . path)=. In principle, the =rev= parameter can be a commit, tag, etc but here I'm interested in a form like =@{2021-08-30}=.
#+begin_src emacs-lisp
(setq my/git-diff-status
'(("A" . added)
("C" . copied)
("D" . deleted)
("M" . modified)
("R" . renamed)
("T" . type-changed)
("U" . unmerged)))
(defun my/get-files-status (rev)
(let ((files (shell-command-to-string (concat "git diff --name-status " rev))))
(mapcar
(lambda (file)
(let ((elems (split-string file "\t")))
(cons
(cdr (assoc (car elems) my/git-diff-status))
(nth 1 elems))))
(split-string files "\n" t))))
#+end_src
I'll use it to get a list of added and changed Roam files since the last review. Date should have be in a format =YYYY-MM-DD=.
#+begin_src emacs-lisp
(defun my/org-changed-files-since-date (date)
(let ((default-directory org-directory))
(my/get-files-status (format "@{%s}" date))))
#+end_src
Now we are ready to format this list to insert it into the capture template.
#+begin_src emacs-lisp
(defun my/org-review-format-roam (rev)
(let* ((changes (my/org-changed-files-since-date rev))
(new-roam
(seq-filter
(lambda (elem)
(and (eq (car elem) 'added)
(string-match-p (rx bos "roam") (cdr elem))))
changes))
(changed-roam
(seq-filter
(lambda (elem)
(and (eq (car elem) 'modified)
(string-match-p (rx bos "roam") (cdr elem))))
changes)))
(concat
(unless (seq-empty-p new-roam)
(concat "** New Roam entries \n"
(mapconcat
(lambda (entry)
(format "- [[file:%s][%s]]" (cdr entry) (cdr entry)))
new-roam
"\n")
"\n"))
(unless (seq-empty-p changed-roam)
(concat "** Changed Roam entries \n"
(mapconcat
(lambda (entry)
(format "- [[file:%s][%s]]" (cdr entry) (cdr entry)))
changed-roam
"\n"))))))
#+end_src
**** Data from org-journal
Second, I want to have a list of new jounal entries since the last review.
#+begin_src emacs-lisp
(defun my/org-journal-entries-since-date (rev-date)
(mapcar
(lambda (date)
(let ((time (encode-time (parse-time-string date))))
`((file . ,(org-journal--get-entry-path time))
(header . ,(format-time-string org-journal-date-format time)))))
(seq-filter
(lambda (date) (string-lessp rev-date date))
(mapcar
(lambda (date)
(format "%04d-%02d-%02dT00:00:00+0300" (nth 2 date) (nth 0 date) (nth 1 date)))
(org-journal--list-dates)))))
#+end_src
Format the results:
#+begin_src emacs-lisp
(defun my/org-review-format-journal (rev-date)
(mapconcat
(lambda (item)
(format "- [[file:%s::*%s][%s]]"
(cdr (assoc 'file item))
(cdr (assoc 'header item))
(cdr (assoc 'header item))))
(my/org-journal-entries-since-date rev-date)
"\n"))
#+end_src
**** Data from org-agenda via org-ql
Third, I want to list some changes in my agenda. This section will change depending on what I'm currently working on.
So, here is a list of queries results of which I want to see in the review template. The format is =(name date-field order-by-field query)=.
#+begin_src emacs-lisp
(setq my/org-ql-review-queries
`(("Waitlist" scheduled scheduled
(and
(done)
(tags-inherited "waitlist")))
("Personal tasks done" closed ,nil
(and
(tags-inherited "personal")
(todo "DONE")))
("Attended meetings" closed scheduled
(and
(tags "meeting")
(todo "PASSED")))
("Done project tasks" closed deadline
(and
(todo "DONE")
(ancestors
(heading "Tasks"))))))
#+end_src
The query will be executed like this: =(and (date-field :from rev-date) query)=
#+begin_src emacs-lisp
(defun my/org-review-exec-ql (saved rev-date)
(let ((query `(and
(,(nth 1 saved) :from ,rev-date)
,(nth 3 saved))))
(org-ql-query
:select #'element
:from (org-agenda-files)
:where query
:order-by (nth 2 saved))))
#+end_src
Format one element of query result.
#+begin_src emacs-lisp
(defun my/org-review-format-element (elem)
(concat
(string-pad
(plist-get (cadr elem) :raw-value)
40)
(when-let (scheduled (plist-get (cadr elem) :scheduled))
(concat " [SCHEDULED: " (plist-get (cadr scheduled) :raw-value) "]"))
(when-let (deadline (plist-get (cadr elem) :deadline))
(concat " [DEADLINE: " (plist-get (cadr deadline) :raw-value) "]"))))
#+end_src
Execute all the saved queries and format an Org list for the capture template.
#+begin_src emacs-lisp
(defun my/org-review-format-queries (rev-date)
(mapconcat
(lambda (results)
(concat "** " (car results) "\n"
(string-join
(mapcar (lambda (r) (concat "- " r)) (cdr results))
"\n")
"\n"))
(seq-filter
(lambda (result)
(not (seq-empty-p (cdr result))))
(mapcar
(lambda (saved)
(cons
(car saved)
(mapcar
#'my/org-review-format-element
(my/org-review-exec-ql saved rev-date))))
my/org-ql-review-queries))
"\n"))
#+end_src
**** Capture template
Now, we have to put all this together and define a capture template for the review.
I'll use a separate directory for the review files, just like for org-journal and org-roam. The filename will have a format =YYYY-MM-DD.org=, which will also free me from the effort of storing the last review date somewhere.
If somehow there are no files in the folder, fallback to the current date minus one week.
#+begin_src emacs-lisp
(setq my/org-review-directory "review")
(defun my/org-review-get-filename ()
(concat my/org-review-directory "/" (format-time-string "%Y-%m-%d.org" (current-time))))
(defun my/get-last-review-date ()
(substring
(or
(-max-by
'string-greaterp
(-filter
(lambda (f) (not (or (string-equal f ".") (string-equal f ".."))))
(directory-files (f-join org-directory my/org-review-directory))))
(format-time-string
"%Y-%m-%d"
(time-subtract
(current-time)
(seconds-to-time (* 60 60 24 7)))))
0 10))
#+end_src
A template looks like this:
#+begin_src emacs-lisp
(setq my/org-review-capture-template
`("r" "Review" plain (file ,(my/org-review-get-filename))
,(string-join
'("#+TITLE: Review %t"
""
"Last review date: %(org-timestamp-translate (org-timestamp-from-string (format \"<%s>\" (my/get-last-review-date))))"
""
"* Roam"
"%(my/org-review-format-roam (my/get-last-review-date))"
"* Journal"
"New journal entries:"
"%(my/org-review-format-journal (my/get-last-review-date))"
"* Agenda"
"%(my/org-review-format-queries (my/get-last-review-date))"
"* Thoughts :crypt:"
"%?")
"\n")))
(add-to-list 'org-capture-templates my/org-review-capture-template t)
#+end_src
*** Org Journal
[[https://github.com/bastibe/org-journal][org-journal]] is a plugin for maintaining a journal in org mode. I want to have its entries separate from my knowledge base.
@ -3484,8 +3715,7 @@ Trying this one out instead of vue-mode and svelte-mode, because this one seems
:config
(add-hook 'web-mode-hook 'smartparens-mode)
(add-hook 'web-mode-hook 'hs-minor-mode)
(my/set-smartparens-indent 'web-mode)
(add-hook 'web-mode-hook ))
(my/set-smartparens-indent 'web-mode))
#+end_src
Hooking this up with lsp.
@ -4510,7 +4740,7 @@ Open a URL with eww.
(when link
(eww link))))
#+end_src
**** YouTube
**** YouTube & EMMS
Previously this block was opening MPV with =start-process=, but now I've managed to hook up MPV with EMMS. So there is the EMMS+elfeed "integration".
The following function converts URLs from Invidious and the like to YouTube.
@ -4533,12 +4763,13 @@ Now, a function to add YouTube link with metadata from elfeed to EMMS.
(let ((track (emms-track
'url (my/get-youtube-url (elfeed-entry-link entry)))))
(emms-track-set track 'info-title (elfeed-entry-title entry))
(setq my/test track)
(emms-playlist-insert-track track))))
(defun my/elfeed-add-emms-youtube ()
(interactive)
(emms-add-elfeed elfeed-show-entry))
(emms-add-elfeed elfeed-show-entry)
(elfeed-tag elfeed-show-entry 'watched)
(elfeed-show-refresh))
(with-eval-after-load 'elfeed
(general-define-key