diff --git a/README.org b/README.org index 887a161..652580c 100644 --- a/README.org +++ b/README.org @@ -1,27 +1,262 @@ * Org Zettelkasten +:PROPERTIES: +:ID: ae7f78fc-39cc-4eaa-bf0e-3cfa73172dbc +:END: +** Inspiration +:PROPERTIES: +:ID: 27e5faf6-62c0-46d5-a61c-611ee4062c94 +:END: +*** David Allen - Getting Things Done :book: +:PROPERTIES: +:ID: 872a3f20-e7b3-4caa-bd60-8b7b747c326f +:END: +*** Lion Kimbro - [[https://users.speakeasy.net/~lion/nb/html/][How to Make a Complete Map of Every Thought You Think]] :website: +:PROPERTIES: +:ID: 0692d591-de30-4f72-b8e6-92473487e2b8 +:END: +*** Niklas Luhman - Zettelkasten +:PROPERTIES: +:ID: b3463c09-00ad-40f6-affe-77427da6fccf +:END: +** Stability +:PROPERTIES: +:ID: 787a0e30-0456-4535-963c-aa5661df11c4 +:END: +This project should be considered *unstable* at the moment. +** Dependencies +:PROPERTIES: +:ID: 7a024900-509e-4e01-a2c9-ece290b3e218 +:END: +- [[https://github.com/abo-abo/swiper][abo-abo/swiper]] + For searching / selecting files +- [[https://github.com/abo-abo/ace-link][abo-abo/ace-link]] + For following links +- [[https://github.com/abo-abo/hydra][abo-abo/hydra]] +** Full-Text Search +:PROPERTIES: +:ID: 119938a8-7308-43d5-b0de-9a805fa7b7cd +:END: +While a simple =grep= or =awk= would be sufficient for most searches, this +package contains a small C++ wrapper for the [[https://xapian.org/][Xapain]] search engine +library. + +This makes it possible to search for files using queries with boolean +operators and matching of synonyms. + +See [[https://xapian.org/docs/queryparser.html][Xapian Docs - Query Parser]] for details on this. +*** NEXT Switch to Rust search engine +:PROPERTIES: +:ID: 28483fc4-510c-4991-99d7-33ab4fb544ab +:END: +*** TODO Document dependencies needed +:PROPERTIES: +:ID: b1ad8941-7779-4052-81bc-361fb7968544 +:END: +*** TODO Document installation +:PROPERTIES: +:ID: f9693279-3d96-4a2c-b5c7-a91907a358df +:END: +*** TODO Document executable paths +:PROPERTIES: +:ID: 88a6fa63-efbf-4702-94c1-d315b6d62051 +:END: +** Components +:PROPERTIES: +:ID: c38d79aa-1e0e-4a05-acef-07939e9575dc +:END: +*** ~org-zk-keywords~ +:PROPERTIES: +:ID: 3b26e04b-2bfe-4ee1-80c3-50b2cc9f36f9 +:END: +File-level attributes are stored as =#+KEY: value= org mode keywords. +These should be placed at the start of the file, not containing any +newlines before or between keywords. + +- ~(org-zk-keywords-set-or-add (key value))~ +- ~(org-zk-keywords-add (key value))~ +- ~(org-zk-keywords-delete (key))~ + +The macro ~org-zk-def-keyword~ can be used to create commands to set +keywords to one of a list of predefined values using ~ivy-read~. +When generating the functions name, the keyword is converted to +lowercase and "_" are replaced by "-". + +**** Example +:PROPERTIES: +:ID: a8aa3b24-16a8-44ab-9001-576cbdbde9a8 +:END: +#+begin_src emacs-lisp +(org-zk-def-keyword + "GTD_STATE" + '("active" + "someday" + "planning" + "cancelled" + "done")) +#+end_src + +Generated function: ~org-zk-set-gtd-state~ +*** ~org-zk-cache~ +:PROPERTIES: +:ID: 1e63a714-eb51-4592-880d-bf8640984991 +:END: + +By default, emacs is not fast enough to efficiently search large collections (>1k +files) for TODO keywords, tags, dates etc. + +To get around this limitation, a cache is introduced. + +This cache works by running ~org-element-process-buffer~ each time a +file is saved or the buffer moves out of focus (e.g. when switching to +another window or buffer). + +Sub-modules can register on element types to compute data on a file or +headline level that can then be used to implement fast views on all +the data in the zettelkasten, e.g. for listing open projects, tasks or +calendar entries, generating clocking reports and statistics or for +use with the integrated spaced repetition system. + +A query language is implemented on this cache for building custom +views on the data. +*** ~org-zk-links~ +:PROPERTIES: +:ID: 19346837-601f-49f7-901e-f421224915b1 +:END: + +There are three types of links, *parent*, *child* and *friend*. + +When adding a link to some target, a link pointing back to the +current file is added to the link list of the target file, +using the inverse (parent <-> child, friend <-> friend) link type. + +Links to other files that are not part of the main text are placed in +a list at the beginning of the file, right after the keywords. + +When adding links, the current search term can be used to create a +new note using ~ivy-immediate-done~, by default bound to ~C-M-j~. + +I've rebound it to ~C-d~. + +#+begin_src emacs-lisp +(define-key ivy-minibuffer-map (kbd "C-d") 'ivy-immediate-done) +#+end_src + +The ~org-zk-hydra~ can be used to quickly add links to the current file. +If *follow-mode* is active, after adding a link the target file is +opened. This is useful for quickly creating "mind-maps" without +entering text or other links to each file. +*** ~org-zk-titlecase~ +:PROPERTIES: +:ID: ad77d3a6-d33c-435d-a71b-7ea0e9ca8ae8 +:END: +When creating a note using the ~org-zk~ commands, +the title that is entered is automatically converted to title-case. + +This only works for English text and not all rules are implemented. +Multi-word conjunctions are not supported yet. +** Testing +:PROPERTIES: +:ID: e39756a6-02ef-454c-a0a7-77b495b0d52d +:END: +Integration testing is done by providing an input file, +a set of operations and an file with the expected output. + +The output is written to a third =_got= file, which can be diffed with +the expected output or used to replace the =_expected= file if the +output generated was valid. +** ~awk~ +:PROPERTIES: +:ID: 5eef70f5-d5be-40ce-b851-e4cf818438fb +:END: +~awk~ is used to extract timestamps, TODO headlines and flashcard +spacing information from the collection of files. + +This has two important implications: -#+begin_quote -Now I am become time-management, the waster of projects. -#+end_quote +Each headline needs an ~:ID:~ property, +this is enforced via a ~before-save~ hook. +IDs should be generated so that a heading can be identified *uniquely* +identified given the filename and an ID. + +Files should be saved as often as possible, +this is saving buffers when they are switched away from. +** ~git~ +:PROPERTIES: +:ID: c1ee3b46-3862-4a56-a574-0f69dfdfee73 +:END: +I'm willing to sacrifice a bit of memory and meaningful commit +messages to make sure no information / history is lost. + +Files are committed to version control every hour. +** Ivy +:PROPERTIES: +:ID: 223f4a88-dc18-47a8-842a-40b55270b531 +:END: +[[https://github.com/abo-abo/swiper]] + +** Hydra +:PROPERTIES: +:ID: 033a1ef3-5301-4ede-86c0-4e331236f7f6 +:END: +Used to implement a self-documenting modal editing layer. ** Design Goals +:PROPERTIES: +:ID: a4b632d0-8011-4a26-8c56-5e8e9a082526 +:END: *** Optimize for performance +:PROPERTIES: +:ID: 88664eac-08ad-42de-a52e-46243cd1fc0b +:END: *** Full-text search +:PROPERTIES: +:ID: 11b35a45-60a2-4aab-94a7-81d321bbba14 +:END: *** Prefer specific custom code to extending / configuring existing code +:PROPERTIES: +:ID: 7372c2de-6908-43b5-b720-92fbc68b76c1 +:END: *** Prefer small files to large ones split into sections +:PROPERTIES: +:ID: 9f414b6a-d2cc-43aa-891b-dfa28c082b23 +:END: *** Designed for use with the GTD method +:PROPERTIES: +:ID: 8083b921-041b-482f-b5c4-eb3ac7c86987 +:END: *** Modular building blocks for flexible systems +:PROPERTIES: +:ID: 77a7e15b-6a0b-44be-9070-3944c67b029e +:END: *** Reuse of existing packages +:PROPERTIES: +:ID: 87be7df6-d4ff-4507-a10f-8b6955e7c684 +:END: - org-ql - org-superagenda ** Long Term Goals +:PROPERTIES: +:ID: c5301646-855c-48ca-b239-4adb6d9d91a4 +:END: *** Back by graph database +:PROPERTIES: +:ID: d38f738c-a1b1-493f-be37-be6f40e2b10e +:END: E.g. [[https://github.com/indradb/indradb][indradb]] *** Integrate with [[https://github.com/remacs/remacs][remacs]] +:PROPERTIES: +:ID: 1a3f602e-ddea-47a7-93fc-0a892a628b02 +:END: For direct bindings from rust to emacs-lisp *** Replace Xapian with something written in rust +:PROPERTIES: +:ID: 3b9099cb-e4ff-462d-a0dd-a75e002b9828 +:END: [[https://github.com/toshi-search/Toshi]] ** Performance +:PROPERTIES: +:ID: a1767a17-a3cd-4cf0-814f-fb98d7c5db52 +:END: Searching a collection of 915 files (160k words) for =NEXT= tasks in active projects takes around 5ms. @@ -30,6 +265,9 @@ Running a full-text search against the same collection of files takes Benchmark were run on a Thinkpad L470 (SSD, i5-2700U 2.5GHz). ** Cache +:PROPERTIES: +:ID: 34343c55-dc76-4a5d-800b-521a7fb06a6c +:END: There are a few other packages that provide more advanced / performant queries on org-mode files: @@ -48,20 +286,10 @@ files takes around 5s. Cache entries are updated when a =.org= file is saved, created, moved or deleted. -** Xapian -While a simple =grep= or =awk= would be sufficient for most searches, this -package contains a small C++ wrapper for the [[https://xapian.org/][Xapain]] search engine -library. - -This makes it possible to search for files using queries with boolean -operators and matching of synonyms. - -See [[https://xapian.org/docs/queryparser.html][Xapian Docs - Query Parser]] for details on this. - -*** TODO Document dependencies needed -*** TODO Document installation -*** TODO Document executable paths ** Project View +:PROPERTIES: +:ID: f78a7024-9492-4c62-b0b9-6530505fc4b4 +:END: Files can be tagged as GTD projects using the =#+GTD_STATE= keyword. Based on this keyword, a view of all (active) projects can be created @@ -69,6 +297,9 @@ and it's easy to mark a whole project as =on_hold= or =someday= to remove it's tasks from the task view. *** TODO Project Priorities +:PROPERTIES: +:ID: 892fb4fa-00a1-4540-b0a3-7714accffc70 +:END: Similar to tasks, files can be assigned different priorities and states in order to represent GTD projects. @@ -80,27 +311,73 @@ that is then factored in when sorting =NEXT= tasks by priority. - Nested - Multiplicative or additive ** Task View +:PROPERTIES: +:ID: 2a5ae864-8fff-475e-9928-5d9bf763e513 +:END: This package implements its own simple version of the =org-agenda= task list. based on =tabulated-list-mode=. I mainly use this to get a view of all =NEXT= tasks, sorted by priority, to see what I should work on next. ** Calendar View +:PROPERTIES: +:ID: 7337a11c-8f38-4891-bd18-25b68bdfe531 +:END: Derived from the task view, filtered by tasks due in some span of time. *** TODO Allow custom views based on filter rules +:PROPERTIES: +:ID: eec63ece-1393-46ae-b145-93e226df819c +:END: *** TODO Fix handling of links in headings +:PROPERTIES: +:ID: ef8cdfc5-85a9-458e-a1d4-b3f81fb55630 +:END: *** WAITING Wrapper around org agenda CLOSED: [2019-12-12 Thu 10:23] +:PROPERTIES: +:ID: 9ef53e02-e02f-4c61-b993-658c3fd90df8 +:END: :LOGBOOK: - State "WAITING" from "NEXT" [2019-12-12 Thu 10:23] :END: Set source files on function call, use projects containing timestamps for this ** Edges and Links Between Files -Files can contain lists of labeled links (edges) to other files, -wrapped in a special block. +:PROPERTIES: +:ID: 24b89cba-6365-4e31-96be-0d5ff02b754a +:END: +There are two ways of linking files together, +using the *edges* list at the beginning of the file +or using *inline-links* in the main text. + +Both use the default org-mode link syntax. + +*Edges* should only be manipulated using the functions in +~org-zk-links~ to ensure the edge list of the target file is updated +to. + +- ~org-zk-remove-edge~ :: Select & remove an edge from both files +- ~org-zk-add-edge~ :: Add an edge to both files +- ~org-zk-add-yank-link~ :: If the kill-ring / clipboard contains a + valid URL, insert it in the file's link list. + +*** TODO Add block +:PROPERTIES: +:ID: 6b520525-cf80-4c1a-a182-4d15186334ce +:END: +Edges are wrapped in a =ZK_EDGES= block so that the list can be +collapsed when using the graph viewer. + +No such restriction applies to *inline-links*. + *** TODO Outline Sidebar, Visualization +:PROPERTIES: +:ID: f83337c7-82b3-4939-a3d3-331e17439390 +:END: *** TODO Graph Queries / Graph Database +:PROPERTIES: +:ID: 823405c8-4f14-43c3-90f5-feba66cdb643 +:END: I'd like to ask the system questions like “Which concepts are introduced in books by $author”. diff --git a/org-zettelkasten.el b/org-zettelkasten.el index abbc9e5..536c7db 100644 --- a/org-zettelkasten.el +++ b/org-zettelkasten.el @@ -1,6 +1,7 @@ (use-package ts) (require 'org-zk-utils) +(require 'org-zk-macros) (require 'org-zk-categories) (require 'org-zk-titlecase) (require 'org-zk-cache) @@ -75,6 +76,24 @@ name-fn to generate a filename." (funcall setup-fn title) (save-buffer))))) +(defun org-zk-create-file (title category) + "Create a new org zettelkasten file given its category and title. +Returns the name of the new file" + (let* ((link-fn (org-zk-category-link-fn category)) + (name-fn (org-zk-category-name-fn category)) + (setup-fn (org-zk-category-setup-fn category)) + (name (funcall name-fn title)) + (file (expand-file-name + (concat name ".org") + (org-zk-category-path category)))) + (if (file-exists-p file) + (error "Aborting, file already exists: %s" file)) + (with-current-buffer (find-file-noselect file) + (funcall setup-fn title) + (save-buffer) + (kill-buffer)) + file)) + (defun org-zk-new-file-and-link () "Create a new org zettelkasten file and insert a link to it at point." @@ -86,22 +105,6 @@ point." (interactive) (kill-new (org-zk-make-link (buffer-file-name)))) -(defun org-zk-edit-keywords () - "Edit the value of the KEYWORDS keyword of the current buffer." - (interactive) - (let* ((options (org-option-parse)) - (keywords (assoc "KEYWORDS" options))) - (if keywords - (org-option-apply - (rest keywords) - (lambda (kw) (string-trim (read-string "keywords: " kw)))) - (org-option-add - "KEYWORDS" - (string-trim (read-string "keywords: ")) - (if (null options) - (point-min) - (fourth (first options))))))) - (defvar org-zk-quick-query-history nil) (defun org-zk--ivy-query (query action) @@ -139,4 +142,59 @@ point." (when (and (equal (file-name-extension filename) "org") category) (org-zk-xapian-refresh-file (org-zk-category-name category) filename)))) +(defun org-zk-add-ids-to-headlines () + "Make sure all headlines in the current file have an ID property" + (interactive) + ;; Can't use `org-map-entries' as it opens a prompt when run inside + ;; a buffer that hasn't been saved yet (Non-existing file / org + ;; agenda) + (save-excursion + (goto-char (point-max)) + (while (outline-previous-heading) + (org-id-get-create)))) + +(add-hook 'org-mode-hook + (lambda () + (add-hook 'before-save-hook 'org-zk-add-ids-to-headlines nil 'local))) + +;; Index current buffer on focus change +(defadvice switch-to-buffer (before save-buffer-now activate) + (when buffer-file-name (org-zk-cache-process-buffer))) + +(defadvice other-window (before other-window-now activate) + (when buffer-file-name (org-zk-cache-process-buffer))) + +(defadvice other-frame (before other-frame-now activate) + (when buffer-file-name (org-zk-cache-process-buffer))) + +(if (fboundp 'ace-window) + (defadvice ace-window (before other-frame-now activate) + (when buffer-file-name (org-zk-cache-process-buffer)))) + +(defun org-zk-file-title (file) + "If FILE is in the org cache, return its title, +if not, return its filename." + (let ((cache-file (org-zk-cache-get (expand-file-name file)))) + (if cache-file + (or (org-zk-cache-get-keyword cache-file "TITLE") + file) + file))) + +(defun org-zk-files-linking-here () + "Generate a list of files linking to the current buffer." + (let ((path (buffer-file-name))) + (org-zk-cache-filter + (lambda (source file) + (--any (string= (oref it path) path) + (oref file links)))))) + +(defun org-zk-rename (title) + "Rename the current zettel, prompting for a new TITLE." + (interactive (list (org-zk-read-title))) + (org-zk-keywords-set-or-add "TITLE" title) + (let ((target (buffer-file-name))) + (dolist (file (org-zk-files-linking-here)) + (org-zk-in-file (car file) + (org-zk-update-link target nil title))))) + (provide 'org-zettelkasten) diff --git a/org-zk-cache.el b/org-zk-cache.el index 4b15e1b..e59f93d 100644 --- a/org-zk-cache.el +++ b/org-zk-cache.el @@ -181,15 +181,14 @@ (buffer-hash (buffer-hash)) (cat (org-zk-category-for-file path))) (if (and cat (not (org-zk-category-ignore-p cat))) - (when (or (null entry) - (not (string= buffer-hash (oref entry hash)))) - (message "processing buffer %s" path) - (let ((element (org-element-parse-buffer)) - (object (make-org-zk-cache-file path buffer-hash))) - (puthash - path - (org-zk-cache-process-element element object) - org-zk-cache--table)))))) + (when (or (null entry) + (not (string= buffer-hash (oref entry hash)))) + (let ((element (org-element-parse-buffer)) + (object (make-org-zk-cache-file path buffer-hash))) + (puthash + path + (org-zk-cache-process-element element object) + org-zk-cache--table)))))) ;; Because we would have to enable org-mode anyway, ;; using `with-temp-buffer` and `insert-file-contents` diff --git a/org-zk-calendar.el b/org-zk-calendar.el index 4e296ce..72be7e8 100644 --- a/org-zk-calendar.el +++ b/org-zk-calendar.el @@ -27,6 +27,8 @@ Returns a list of elements (headline . org-zk-cache-timestamp)" org-zk-cache--table) entries)) +(length (org-zk-calendar--time-entries)) + (defvar org-zk-calendar-n-days 14) (defface org-zk-calendar-today-face diff --git a/org-zk-graphviz.el b/org-zk-graphviz.el index 35f620d..437cfb7 100644 --- a/org-zk-graphviz.el +++ b/org-zk-graphviz.el @@ -13,7 +13,7 @@ (defun org-zk-graph-collect (path depth graph) "Collect connected nodes up to DEPTH" - (message (format "%s" depth)) + (message (format "%s @ %s" path depth)) (let ((cache-file (org-zk-cache-get path))) (when cache-file (add-node @@ -23,9 +23,10 @@ :title (org-zk-cache-get-keyword cache-file "TITLE") :id (oref cache-file path))) (when (plusp depth) - (dolist (link (oref cache-file :links)) - (let ((link-path (oref link path))) - (when link-path + (dolist (link (oref cache-file links)) + (let ((link-path (oref link path)) + (link-type (oref link type))) + (when (and link-path (string-prefix-p "zk_" link-type)) (add-edge graph (oref cache-file path) @@ -37,13 +38,11 @@ ;; 3. draw edges ;; path as id (defun org-zk-graph (path) + (interactive (list (buffer-file-name))) (let* ((cache-file (org-zk-cache-get path)) - (links (oref cache-file :links)) (graph (make-instance 'org-zk-graphviz-graph :name "Graph"))) (org-zk-graph-collect path 2 graph) - (save-image graph "/home/leon/graph.png"))) - -(org-zk-graph "/home/leon/org/inbox.org") + (save-image graph "/home/leon/graph.svg"))) (defun org-zk-graphviz-escape (id) "Escape filenames for use as graphviz node IDs" @@ -54,11 +53,10 @@ (interactive) (let* ((path (buffer-file-name)) (cache-file (org-zk-cache-get path)) - (links (oref cache-file :links)) (graph (make-instance 'org-zk-graphviz-graph :name "Graph"))) (org-zk-graph-collect path 2 graph) - (save-image graph "/home/leon/graph.png") - (find-file "/home/leon/graph.png") + (save-image graph "/home/leon/graph.svg") + (find-file "/home/leon/graph.svg") (image-mode))) (defmethod save-image ((graph org-zk-graphviz-graph) outfile) @@ -71,16 +69,28 @@ (insert "graph ") ;; (insert name " ") (insert " {\n") + (insert " ratio=\"fill\";\n") + (insert " size=\"10,10!\";\n") + (insert " resolution=128;\n") + (insert " overlap=false;\n") + (insert " splines=true;\n") + (insert " node[fontsize=20];\n") + (dolist (node nodes) (with-slots (id title) node (insert (format "%s [ label=\"%s\" ];\n" (org-zk-graphviz-escape id) title)))) (dolist (edge edges) - (insert (format "%s--%s;\n" - (org-zk-graphviz-escape (oref edge from)) - (org-zk-graphviz-escape (oref edge to))))) + (if (< 0.5 (cl-random 1.0)) + (insert (format "%s--%s;\n" + (org-zk-graphviz-escape (oref edge from)) + (org-zk-graphviz-escape (oref edge to)))) + (insert (format "%s--%s;\n" + (org-zk-graphviz-escape (oref edge to)) + (org-zk-graphviz-escape (oref edge from))))) + ) (insert "}\n"))) - (shell-command (format "dot %s -Tpng -o %s" infile outfile))))) + (shell-command (format "dot %s -Tsvg -Kfdp -o %s" infile outfile))))) (defmethod add-edge ((graph org-zk-graphviz-graph) from to) (with-slots (edges nodes) graph @@ -90,14 +100,14 @@ (with-slots (nodes) graph (setq nodes (remove-duplicates (cons node nodes))))) -(let ((graph (make-instance 'org-zk-graphviz-graph :name "test"))) - (add-edge graph 1 4) - (add-edge graph 1 2) - (add-edge graph 1 7) - (add-edge graph 7 8) - (add-edge graph 8 9) - (add-edge graph 8 1) - (save-image graph "~/test.png")) +;; (let ((graph (make-instance 'org-zk-graphviz-graph :name "test"))) +;; (add-edge graph 1 4) +;; (add-edge graph 1 2) +;; (add-edge graph 1 7) +;; (add-edge graph 7 8) +;; (add-edge graph 8 9) +;; (add-edge graph 8 1) +;; (save-image graph "~/test.png")) ;; (defun ge-find-node-id-str (id-str) diff --git a/org-zk-hydra.el b/org-zk-hydra.el index 60c0ad7..8c5702a 100644 --- a/org-zk-hydra.el +++ b/org-zk-hydra.el @@ -1,22 +1,44 @@ (require 'hydra) +(require 'org-zk-keywords) +(require 'org-zk-links) + +(defun org-zk--hydra-follow () + (if org-zk-links-follow + "[X]" + "[ ]")) (defhydra org-zk-hydra (:hint none) " -^GTD^ ^Add Edge^ ^Create Edge^ -^^^^^---------------------------------------------- -_g_: Set State _p_: Parent _P_: Parent -^ ^ _c_: Child _C_: Child -^ ^ _f_: Friend _F_: Friend -^ ^ _o_: Other _O_: Other +^Actions^ ^Keywords^ ^Edges^ ^Options^ +^^^^^---------------------------------------------------- +_l_: Open Link _g_: State _p_: + Parent _C-f_: %s(org-zk--hydra-follow) follow +_o_: Open File _t_: Type _c_: + Child +_n_: New File _s_: Stabil. _f_: + Friend +_h_: Jump Hdng ^ ^ _y_: + Yank URL +_L_: Link File ^ ^ _r_: Remove +_q_: New Query +_R_: Rename " - ("g" org-zk-projects-set-gtd-state "Set GTD State" :exit t) + ("g" org-zk-set-gtd-state "Set GTD State") + ("t" org-zk-set-category "Set Category / Type") + ("s" org-zk-set-stability "Stability") + ("l" ace-link-org "Open Link") + ("L" org-zk-link-file "Link File") + ("C-l" org-zk-link-file "Link File") + ("h" avy-org-goto-heading-timer "Jump Heading") + ("o" org-zk-open-file "Open File") + ("n" org-zk-new-file "New File") + ("q" org-zk-xapian-query-new-query "Xapian Query") + ("R" org-zk-rename "Rename") + ;; I'm using C-b as a hotkey for the hydra + ;; double-tapping b opens a file + ("b" org-zk-open-file "Open File") + ("C-b" org-zk-open-file "Open File") ("p" org-zk-add-parent "Add parent") ("c" org-zk-add-child "Add child") ("f" org-zk-add-friend "Add friend") - ("o" org-zk-add-edge "Add other edge") - ("P" org-zk-create-parent "Create parent") - ("C" org-zk-create-child "Create child") - ("F" org-zk-create-friend "Create friend") - ("O" org-zk-create-edge "Create other edge")) + ("r" org-zk-remove-edge "Remove edge") + ("y" org-zk-add-yank-link "Add yank link") + ("C-f" org-zk-links-toggle-follow "Toggle follow")) (provide 'org-zk-hydra) diff --git a/org-zk-links.el b/org-zk-links.el index 648a838..3be8174 100644 --- a/org-zk-links.el +++ b/org-zk-links.el @@ -1,30 +1,253 @@ -(defun my-test () - (interactive) - (if (looking-at-p "^$") - (forward-line) - (insert "nn\n"))) +(defvar org-zk-links-follow nil + "Switch to target file after adding a link.") + +(defun org-zk-links-toggle-follow () + "Toggle link-following on creation." + (interactive) + (if org-zk-links-follow + (progn + (setq org-zk-links-follow nil) + (message "org-zk link follow disabled")) + (progn + (setq org-zk-links-follow t) + (message "org-zk link follow enabled")))) (defun org-zk-add-link (type target) - "Add a link of TYPE to TARGET to the current files link list." + "Add a link of TYPE to TARGET to the current buffer's link list." + ;; Skip keywords + (save-excursion + (goto-char (point-min)) + ;; skip keywords + (while (looking-at-p "^#+") + (forward-line)) + (let (p) + (setq p (point)) + (if (looking-at-p "^$") + (forward-line)) + ;; forward-line did not work / file empty besides keywords + (if (= p (point)) + (insert "\n"))) + ;; TODO: Skip lines of "lesser / sorted prev" type + (insert (format + "- [[zk_%s:%s][%s]]\n" + type + target + (org-zk-file-title target))) + ;; For the first link, insert a newline between it an the main text + (forward-line) + (if (looking-at-p (rx alpha)) + (insert "\n")))) + +(defun org-zk-add-plain-link (target title) + "Add a 'plain' link to the current buffer's link list." ;; Skip keywords (save-excursion (goto-char (point-min)) ;; skip keywords (while (looking-at-p "^#+") (forward-line)) - (let (p) - (setq p (point)) - (if (looking-at-p "^$") - (forward-line)) - ;; forward-line did not work / file empty besides keywords - (if (= p (point)) - (insert "\n"))) - ;; TODO: Skip lines of "lesser / sorted prev" type - (insert (format - "- [[zk_%s:%s][%s]]\n" - type - target - (org-zk-file-title target))))) + (let (p) + (setq p (point)) + (if (looking-at-p "^$") + (forward-line)) + ;; forward-line did not work / file empty besides keywords + (if (= p (point)) + (insert "\n"))) + ;; TODO: Skip lines of "lesser / sorted prev" type + (insert (format "- [[%s][%s]]\n" target title)) + ;; For the first link, insert a newline between it an the main text + (forward-line) + (if (looking-at-p (rx alpha)) + (insert "\n")))) + +;; TODO: Allow custom link prefix +;; TODO: Allow custom link types +(defun org-zk-change-link-type (new-type target) + "Change the type of all links to TARGET in the current buffer +to NEW-TYPE. This changes inline-links, too." + (replace-regexp + (concat + (rx + "[[zk_" (or "parent" "child" "friend") ":") + (regexp-quote target) + (rx "]")) + (format "[[zk_%s:%s]" new-type target) + nil (point-min) (point-max))) + +(defvar org-zk-link-re + (rx + "[[zk_" + (group-n 1 (or "parent" "child" "friend")) + ":" + (group-n 2 (* (not (any "]")))) + "][" + (group-n 3 (* (not (any "]")))) + "]]") + "Regex matching zettelkasten links. +Groups: +1. type +2. target +3. text / title") + +(defvar org-zk-list-link-re + (rx + "-" + (+ (any " " "\t")) + "[[zk_" + (group-n 1 (or "parent" "child" "friend")) + ":" + (group-n 2 (* (not (any "]")))) + "][" + (group-n 3 (* (not (any "]")))) + "]]") + "Regex matching zettelkasten link list entries. +Groups: +1. type +2. target +3. text / title") + +(defvar org-zk-deleted-link-text "(deleted)") + +;; TODO: Jump to file for manual fixes +;; +;; NOTE: It's easier to parse the buffer again instead of adding link bounds +;; to the org cache and dealing with cache-incoherence problems. +(defun org-zk-remove-inline-link (target) + "Replace links to TARGET with their description. +If a link has no description, replace it with + `org-zk-deleted-link-text'. +Link targets are compared using their *absolute* path." + (let (links + (dir (file-name-directory (buffer-file-name)))) + (org-element-map + (org-element-parse-buffer) + 'link + (lambda (link) + (if (string= + (expand-file-name (org-element-property :path link)) + (expand-file-name target)) + (push link links)))) + ;; LINKS is already in reverse order so its save to delete links + ;; by their bounds + (dolist (link links) + (if (org-element-contents link) + (progn + (goto-char (org-element-property :begin link)) + (delete-region + (org-element-property :begin link) + (org-element-property :end link)) + (insert + (org-element-interpret-data + (org-element-contents link))) + ;; NOTE: There is some weird issue with :end being set incorrectly, + ;; if the link doesn't end at the end of a line + (unless (eolp) (insert " "))) + (progn + (goto-char (org-element-property :begin link)) + (delete-region + (org-element-property :begin link) + (org-element-property :end link)) + ;; NOTE: There is some weird issue with :end being set incorrectly + ;; if the link doesn't end at the end of a line + (insert + org-zk-deleted-link-text) + (unless (eolp) (insert " "))))) + links)) + +(defun org-zk-update-link (target target-new desc-new) + "Update links to TARGET to point to TARGET-NEW and change their description to DESC-NEW. DESC-NEW is set for links without a description, too. +Link targets are compared using their *absolute* path. +If either TARGET-NEW or DESC-NEW is nil, that part of the link is left unchanged." + (let (links + (dir (file-name-directory (buffer-file-name)))) + (org-element-map + (org-element-parse-buffer) + 'link + (lambda (link) + (if (string= + (expand-file-name (org-element-property :path link)) + (expand-file-name target)) + (push link links)))) + ;; LINKS is already in reverse order so its save to delete links + ;; by their bounds + (dolist (link links) + (if desc-new + (org-element-set-contents link desc-new)) + (if target-new + (org-element-put-property link :path target-new)) + (goto-char (org-element-property :begin link)) + (delete-region + (org-element-property :begin link) + ;; Because of the way `org-element-interpret-data' work, there + ;; is no need to use a -1 here + (org-element-property :end link)) + (insert (org-element-interpret-data link))) + links)) + +(defun org-zk-remove-link (target) + "Remove the link to TARGET from the link list in the current buffer." + (save-excursion + (goto-char (point-min)) + ;; skip keywords + (while (looking-at-p "^#+") + (forward-line)) + ;; skip whitespace + (while (looking-at-p "^\w*$") + (forward-line)) + ;; loop over link list + (while (looking-at org-zk-list-link-re) + (if (string= (match-string 2) target) + (kill-line) + (forward-line))))) + +(defun org-zk-buffer-edges () + "Return a list (type target title) for all edges in the current buffer." + (let (res) + (save-excursion + (goto-char (point-min)) + ;; skip keywords + (while (looking-at-p "^#+") + (forward-line)) + ;; skip whitespace + (while (looking-at-p "^\w*$") + (forward-line)) + ;; loop over link list + (while (looking-at org-zk-list-link-re) + (push (list (match-string-no-properties 1) + (match-string-no-properties 2) + (match-string-no-properties 3)) + res) + (forward-line))) + res)) + + +(defun org-zk-select-edge (action) + "Call FN with the target of an edge in the current buffer." + (ivy-read + "Edge: " + (mapcar + (lambda (e) + (cons + (format "%s (%s)" (third e) (first e)) + (second e))) + (org-zk-buffer-edges)) + :action action)) + +(defun org-zk--remove-edge (target) + (let ((source (buffer-file-name))) + (org-zk-remove-link target) + (save-buffer) + (if (file-exists-p target) + (org-zk-in-file target + (org-zk-remove-link source) + (save-buffer))))) + +(defun org-zk-remove-edge () + "Select and remove an edge of the current buffer." + (interactive) + (org-zk-select-edge + (lambda (selection) + (org-zk--remove-edge (cdr selection))))) (defun org-zk-edge-inverse (type) (pcase type @@ -35,10 +258,14 @@ (defun org-zk--add-edge (type target) (let ((inverse (org-zk-edge-inverse type)) (source (buffer-file-name))) - (org-zk-add-link type target) + (org-zk-add-link type target) + (save-buffer) (when inverse - (with-current-buffer (find-file-noselect target) - (org-zk-add-link inverse source))))) + (if org-zk-links-follow + (with-current-buffer (find-file target) + (org-zk-add-link inverse source)) + (org-zk-in-file target + (org-zk-add-link inverse source)))))) (defun org-zk-add-edge (type) "Add a new edge of TYPE. Prompts for an existing file. If `ivy-immediate-return' is used, @@ -89,12 +316,22 @@ creates a file with that title in category of the current file." (or title (org-zk-file-title file))) (error "File %s is not part of any zettelkasten category" file)))) -(defun org-zk-files-linking-here () - "Generate a list of files linking to the current buffer." - (let ((path (buffer-file-name))) - (org-zk-cache-filter - (lambda (source file) - (--any (string= (oref it path) path) - (oref file links)))))) +(defun org-zk--try-yank-url () + "If the result of `yank' is a url, +return it, if not, return NIL." + ) + +;; TODO: Get title from url +(defun org-zk-add-yank-link () + "Check if the element in the clipboard / kill-ring is an URL, +if so, insert a link to it in the edge list, prompting for a description." + (interactive) + (let ((res + (with-temp-buffer + (yank) + (thing-at-point 'url (point))))) + (if res + (org-zk-add-plain-link res (read-string "Description: ")) + (message "Kill-ring contents are not a URL")))) (provide 'org-zk-links) diff --git a/org-zk-projects.el b/org-zk-projects.el index a495fe6..d9d4377 100644 --- a/org-zk-projects.el +++ b/org-zk-projects.el @@ -1,23 +1,8 @@ (require 'org-zk-cache) -(defvar org-zk-projects-gtd-states - '("active" - "on_hold" ; was active or is ready to go, now is paused - "someday" ; incubator / someday / maybe - "blocked" ; blocked by some other project - "planning" ; planning / brainstorming - "cancelled" ; planning / brainstorming - "done")) - -(defun org-zk-projects-set-gtd-state (state) - (interactive - (list (ivy-completing-read "State: " org-zk-projects-gtd-states))) - (let ((options (org-buffer-options))) - (set-org-option options "GTD_STATE" state))) - (defun org-zk-projects-all () (org-zk-cache-file-query - `(or ,@(--map `(gtd-state ,it) org-zk-projects-gtd-states)))) + `(or ,@(--map `(gtd-state ,it) org-zk-gtd-states)))) (define-org-zk-cache-file-query-macro gtd-state (state) `(keyword "GTD_STATE" ,state)) @@ -27,18 +12,18 @@ (defun org-zk-projects-active () (org-zk-cache-file-query '(gtd-state "active"))) -(defun org-zk-projects-on-hold () - (org-zk-cache-file-query '(gtd-state "on_hold"))) - (defun org-zk-projects-someday () (org-zk-cache-file-query '(gtd-state "someday"))) -(defun org-zk-projects-blocked () - (org-zk-cache-file-query '(gtd-state "blocked"))) - (defun org-zk-projects-planning () (org-zk-cache-file-query '(gtd-state "planning"))) +(defun org-zk-projects-cancelled () + (org-zk-cache-file-query '(gtd-state "cancelled"))) + +(defun org-zk-projects-done () + (org-zk-cache-file-query '(gtd-state "done"))) + (defun org-zk-projects-buffer () (get-buffer-create "*Org Projects*"))