Skip to content

legalnonsense/reorg

Repository files navigation

Reorg: Re​organize your lifeTHIS IS A WORK IN PROGRESS

Dynamically, recursively, and intelligently reorganize and display any type data in custom outlines

Customize how data displayed and shortcuts to interact with it

Create three dimensional outlines by displaying the same headings in multiple locations

Automatically update outlines when the underlying data is changes

Mix different kinds of data (e.g., org-mode, email, files, etc.) in the same outline

Create modules to expand capabilities to display and interact with any kind of data

Preface

[2024-02-26 Mon] Emacs 29 has brought on some problems…

[2024-02-25 Sun]

Reorg allows you to highly customize the sorting, grouping, displaying, and interacting with any type of data.

For now, Reorg is most useful if you use org-mode. It serves as a potential complete–and far more powerful–relacement for org-agenda (no disrepect to org-agenda).

And as the example modules included here show, it can also be used for file heirarchies (see, e.g., reorg-files.el, .leo files (see, e.g., reorg-leo.el, json data (see, e.g., reorg-json.el), source code (see, e.g., reorg-elisp.el), and limitless other types (so long as someone writes a module).

I am still developing this package. With the exception of reorg-org.el, the modules I’ve supplied are minimal working examples.

The reorg-org.el file is messy. The parsers (i.e., those that call reorg-create-data-type) change often, and contain many overlaps. I am convinced there is no good way to handle every type of timestamp the user might want to access. I am convinced there is no universal way to parse all data from an orgmode heading that serves all purposes.

If anyone hacks on one of the modules, beware that if you add or change a data type, you’ll need to reevaluate the entire buffer, not just that single data type definition. (This is a workaround for a bug in the class and data creation macros.)

Anyone who uses Reorg will have to know some elisp. If, when writing a template, you get an invalid function error with a reference to a .variable always try (quote .variable) or =’.variable=. There are layers of macros and backquoted lambdas in this code and it sometimes gets confusing. The benefit is that the level of abstraction and customization allows you to focus on how exactly how you want data (including orgmode, files, emails, or anything for which a module has been written) to be sorted and displayed.

Module writing is harder when it comes to interacting with the underlying data. See reorg-org.el as an example (and keep in mind that programatically parsing and editing orgmode files is, at best, tricky), which uses the reorg-org--with-source-and-sync macro. There are not similar macros written for other modules yet.

The key is that once you have a piece of parsed data, reorg will delete the old entries and insert new ones without refreshing the entire outline. See the (poorly named) function reorg--insert-new-heading.

Though I would not advise it, because it is so easy to write a parser it would be simple to use Reorg to create your own custom markup language so that you could have your own plain text note taking system that can be parsed into usable outlines.

Example: Org-mode

Reorg was originally designed as a way to view and interact with org-mode files. Think of it as a combination of org-super-agenda, org-sidebar, org-ql, and taxy.el (although it does not rely on any of these packages).

Using your own org-agenda-files

First, (require 'reorg-launch). Then open reorg-test.el and see what you can get to work. You can look at reorg-jeff.el to see some of the ways I am using it for my agenda.

For example, take the following orgmode file which contains a few notes, TODO items, and dates.

* [2023-01-11 Wed 13:16] @Brian told @Alex that @Olivia ate three guitars for breakfast. :aaa:
* TODO [#A] Research the effect of nylon strings on gut health. :aaa:
* [2023-01-11 Wed 14:00] @Olivia told @Brian that she dreams of being a potato. :bbb:
* [2023-01-11 Wed 15:00] @Alex learns that @Olivia wants to be a potato and he begins pacing erratically. :bbb:
* [2023-01-08 Sun 13:19] @Olivia decides that she wants to mess with @Brian and @Alex because she is bored :aaa:
* TODO [#B] Think of a better example file :bbb:
* DONE [#A] Write a terrible example file   

Suppose you want to display this information in the following way:

  1. Group all entries with a timestamp under a single heading and sort them by date
  2. Group all entries that mention an @name under a heading for each @name (regardless of whether those entries were already inserted into the previous heading)
    1. Within each of those headings, group the entries by tag with the tags sorted alphabetically
    2. But don’t display the timestamps
  3. Group all TODO entries together, sorting by tag, but display the headlines in all caps
  4. Show all the entries in the file sorted by org-id, but only display the org-id

This produces:

TEST/initial-example-screenshot.png

One of the key features is that entries can appear in multiple places in the outline.

Clones

Clones are a key feature of Reorg. Clones are entries that appear in multiple places in an outline. See, e.g.,/ Leo Editor’s use of clones. /See also a prototype package I wrote some time ago called org-clones. In my mind, an outline with clones is a three-dimensional outline which means that it can be viewed from different perspectives. More on that later.

In Reorg, a heading can appear in different locations and can have different appearances. Take the original example. Each of the headings in a red box are identical:

TEST/third-example-screenshot.png If you select any of these headings and render the underlying data, you will be taken to the exact same location in the org-mode file. If you edit these headings from the org-mode buffer, they will all be deleted, re-parsed from the new source, and re-inserted into the outline at the appropriate location(s).

Multi-valued data sets

Note that some of the headlines in the above example contained multiple @name references. If Reorg encounters data that is a list, it can be told to clone those entries once for each piece of data in the list. That means that a heading that includes @Olivia and @Brian will appear under both of their headings. (This is explained further in the section on writing templates.) See below discussing the .@ operator (the fact that I used @names as an example and the operator being a .@ prefix is purely a coincidence).

Not just for orgmode

Reorg isn’t just for org-mode, so let’s add some more to the original example. In addition to the previous we want to:

  1. Display all of the variable declarations in reorg.el, but don’t dislpay the reorg- prefix; sort them in reverse alphabetical order. Group them by the type of variable.
  2. Show all of the .el files in the reorg directory, but group them by the number of characters in the filename and sort the groups in descending order. If a filename contains the letter “a”, put a happy face next to it.

Okay. You asked for it. TEST/second-example-screenshot.png

Note that these examples keep the types of data separate. It is just as easily possible to mix data so that files, org-mode entries, etc., all exist within the same headings.

These outlines are defined by Reorg’s template system, which is explained below. For now, let’s talk about the outlines themselves.

Outline buffer

Moving through the outline

The keybindings, usable by any Reorg outline no matter the data type, are:

n or downnext heading
p or upprevious heading
u or leftgoto parent
Unext parent
bprevious sibling
fnext sibling
tabfold/unfold subtree
shift+tabfold/unfold all
RReload
gRefresh heading at point
cjump to next clone
Cjump to previous clone
RETDisplay source buffer

Interacting with data

Reorg itself does not provide a way to interact with the data being displayed in the outline because it has no idea what that data is. All interactions are handled by the various modules. For example, if the header at point is based on an org-mode file, the reorg-org module uses the following keybindings:

SPCreorg-org–open-agenda-day
hedit headline
tedit todo state
aedit tags
dedit deadline
sedit scheduled timestamp
rset properties
iset priority

Note that the keybindings in reorg-org.el are constantly changing. These are only examples.

Refreshing the display

If you run any of these commands and change the underlying org-mode file, Reorg will edit the source buffer, delete the heading (and its clones) in the reorg tree, re-parse the heading, and re-insert the data into the tree. It does this without reloading the entire tree, so the change is immediate.

Note that refreshing the a single element in the outline is tricky. If you encounter a bug, please report it. You can always press R to reload the entire outline if needed.

Viewing the Reorg buffer

A reorg buffer can be viewed in a side window so that it is like a sidebar. Otherwise, it can be viewed in a normal window as you would any buffer.

If you view reorg as a sidebar (the default shortcut to toggle the sidebar is C-; r), it will automatically render the source for the header at point. For example, here it is interacting with the preceding outline:

Reorg will automatically detect if it’s in a side window or not, and adjust its display behavior accordingly. If in a side window, it will try to render the source of the heading at point. If in a main window, it will only try to render the source if you press RET. (Please forgive my haphazard GIF.)

TEST/output-2023-01-11-19:51:30.gif

How to write a template

Templates are used to create custom outlines and have a simple syntax. Be forewarned that the user must be familiar with basic lisp (i.e., enough to navigate a config file) to competently write a template.

Accessing the parsed data: dotted symbols

Templates use variables that are stored when Reorg parses the underlying data. These variables can be accessed using dotted symbols identical those used by the built-in let-alist macro. Templates can use any data that has been parsed by Reorg.

Extra data types can be added using the reorg-create-data-type macro which is explained below.

As the reorg-org module stands now (i.e., hacky, in testing, etc.), if the first heading in the original example is parsed, it returns an alist:

Note: Now, when reorg generates an outline, it scans the template and only parses the data that is needed for that specific outline. This allows uses to define additional data types without worrying about slowing down outlines that don’t use that data type.

Also note: these are for illustration and the actual types defined in reorg-org.el are constantly changing.

((ts-any . "[2023-01-11 Wed 13:16]")
 (ts-ts . #s(ts 13 16 [....] 1673460960.0))
 (timestamp-type)
 (ts-day-name . "Wednesday")
 (ts-day . 11)
 (ts-month-num . 1)
 (ts-month . "January")
 (ts-year . "2023")
 (at-names "Brian" "Alex" "Olivia")
 (root-ts-inactive)
 (root . "[2023-01-11 Wed 13:16] @Brian told @Alex that @Olivia ate three guitars for breakfast.")
 (org-level . 1)
 (order . 1)
 (buffer . #<buffer test.org>)
 (buffer-name . "test.org")
 (filename . "~/.emacs.d/lisp/reorg/TEST/test.org")
 (category . "test")
 (category-inherited)
 (id . "7038a596-f2c3-414d-a68a-fadbc9ef61ad")
 (timestamp-range)
 (timestamp-ia-range)
 (timestamp-ia . "[2023-01-11 Wed 13:16]")
 (link-file-path)
 (link-file-name)
 (link)
 (links)
 (timestamp)
 (todo)
 (tags . ":aaa:")
 (headline . "[2023-01-11 Wed 13:16] @Brian told @Alex that @Olivia ate three guitars for breakfast")
 (scheduled)
 (deadline)
 (body)
 (priority . "B")
 (timestamp-all)
 (ts)
 (ts-pretty)
 (tag-list "aaa")
 (delegatee)
 (class . org))

At all points within an outline template, the user can access any of these variables with dotted notation. In other words, .priority is the same as (alist-get 'priority DATA). (See below for an explanation about DATA).

Help from company-reorg when writing a template

If you use company, you can M-x reorg-enable-completion and you should get auto complete for the dotted prefix

Example

Here is a basic template that will display each heading that has a “TODO” todo state for each file in your org-agenda-files.

(reorg-open-sidebar `( :sources ((org . ,(org-agenda-files)))
                       :group "Example template"
                       :children (( :group (when (equal .todo "TODO" ) "TODO")
                                    :format-results (.stars " " .todo " " .headline)))))

Here is another example that will create a date tree from your agenda files. Note: this requires ts.el.

(reorg-open-sidebar `( :sources ((org . ,(org-agenda-files)))
                       :children (( :group
                                    .ts-year
                                    :sort-groups
                                    string<
                                    :children
                                    (( :group
                                       .ts-month
                                       :sort-groups
                                       (lambda (a b)
                                         (let ((seq '("January"
                                                      "February"
                                                      "March"
                                                      "April"
                                                      "May"
                                                      "June"
                                                      "July"
                                                      "August"
                                                      "September"
                                                      "October"
                                                      "November"
                                                      "December")))
                                           (< (seq-position seq a 'string=)
                                              (seq-position seq b 'string=))))
                                       :sort-results
                                       ((.ts-day . <))
                                       :format-results
                                       (.stars " " .headline " " .tag-string)))))))

Here’s an explanation:

:sources

:sources is an alist where the key is the name of a class and the value is the actual source. The value can also be a list of sources, e.g., in the above code (org-agenda-files) can return multiple org-mode files. You can use multiple sources. For example:

'( :sources ((org . "~/path/to/org/file.org")
             (org . "~/path/to/a/different/org.org")
             (email . "email search terms")
             (files . "/path/to/directory")))

(For a source to be available, a module has to be created using reorg-create-class-type and reorg-create-data-type as described below.)

:group

Group is an elisp form that determines what data to include in the outline, and what data should be available to any subtrees within the outline. The rule is easy: if :group returns nil, then the data is excluded from the outline. If :group returns non-nil, then the data is grouped by return value.

In the previous example, there is only one non-nil return value for group: “TODO”

:group (when (equal .todo "TODO" ) "TODO")

But what if you wanted to create groups for each todo state?

:group .todo ;; Remember: .todo will either be a string (the todo state) or it will be nil (if there is no todo keyword)

What if you wanted to create a group called “GROUP A” if there is a timestamp, and “GROUP B” if there is a “DONE” todo state, but you want the timestamp to take precedence?

:group (cond (.timestamp "GROUP A")
             ((when (and .todo
                         (equal .todo "DONE")))
              "GROUP B"))

The preceding example has a problem: what if a heading has a timestamp, and a DONE todo state? Maybe you want it to appear under both headings. In that case, you have to create sibling groups using the :children keyword:

:children (( :group (when .timestamp "GROUP A"))
           ( :group (when (and .todo
                               (equal .todo "DONE"))
                      "GROUP B")))
Multivalued properties: the .@ operator

The :group keyword allows a second kind of dotted symbol: the .@symbol. If a reorg group contains a symbol prefixed with .@ it is a signal to reorg that: (1) you anticipate the value of that data will be a list; and (2) you want to create clones of the data which are identical except for that one piece of data.

I am not explaining this well, but it is shown in the main example screenshots. This means that if you have, for example, an orgmode entry with multiple dates in it, and you create a date tree, it will appear at each entry (note the use of .@ts-all-flat)

(defun jrf/reorg-diary ()
  (interactive)
  (reorg-org-capture-enable)
  (reorg-open-sidebar
   `( :sources ((org . ,(org-agenda-files))
      :format-results (
		       (propertize
			(reorg--truncate-and-pad 
			 (car (s-split "," .root))
			 12 13)
			'face '((t ( :foreground "black"
				     :background "light gray"
				     :box t))))
		       "     "
		       (propertize 
			(reorg--truncate-and-pad .headline 50 55 "...")
			'face
			'((t ( :foreground "black"
				     :background "light gray"
				     :box t))))
		       .clocked-time)
      :group (when .ts-all-flat
	       (substring .@ts-all-flat 0 4))
      :sort-groups string>
      :children (( :group (reorg-org--format-time-string .ts-all-flat "%B")
		   :bullets "-"
		   :folded-bullets ">"
		   :sort-groups (lambda (a b)
		     (reorg--sort-by-list a b '("January"
						"February"
						"March"
						"April"
						"May"
						"June"
						"July"
						"August"
						"September"
						"October"
						"November"
						"December")))
		   :children (( :group (reorg-org--format-time-string .ts-all-flat "%e %A")
				:sort-groups string<)))))))
The .! operator

I generally call this a “drill.” If you prefix a symbol in a template (which should be an ordered list) with .!, then reorg will assume the list of names of branches in a tree, and create an outline placing each element at the appropriate spot. This means that reorg can easily display, for example, a file system heirarchy.

For example, here is a minimal file class definition with a couple shortcuts. Note that this definition requires the user to supply a shell command to generate a list of files:

(reorg-create-class-type
 :name files
 :getter (cl-loop for each in (s-split "\n" (shell-command-to-string
                                             ;; (concat 
                                             ;;  "find "
                                             SOURCE
                                             ;; " -type f"
                                             )
                                       t)
                  collect (PARSER each))
 :keymap (("e" . (lambda ()
                   (interactive)
                   (let ((file (reorg--get-prop 'fullname)))
                     (reorg--select-main-window)
                     (find-file file))))
          ("o" . (lambda () (interactive)
                   (xdg-open (reorg--get-prop 'path))))))

(reorg-create-data-type
 :name filename
 :class files
 :parse (f-filename data))

(reorg-create-data-type
 :name parent-dirs
 :class files 
 :parse (butlast (s-split "/" data t)))

(provide 'reorg-files)

Note that .parent-dirs will contain an ordered list showing each file’s parent directories. Now, we will create a template that groups the files based on their parent directories. Note that we’ve decided for the user the input will be a directory and the command will be find DIR -type f.

(require 'reorg-files)
(defun reorg-files (&optional dir)
  (interactive "D")
  (reorg-open-sidebar
   `( :sources ((files . ,(concat "find "
                                  dir
                                  " -type f")))
      :format-results (.filename)
      :group .!parent-dirs
      :sort-results ((.filename . string<))
      :sort-groups string<)))

Obviously you could do a great deal of customizing how files are displayed without much trouble.

Mixing different types of groupings

You can mix groups generated by the .! operator with other traditional groups. For example, you can group something first and then drill the results. Here, we create two groups for files with an even number of characters and those with an odd number, then display the full file heirarchy:

(defun reorg-group-then-drill (&optional dir)
    (interactive "D")
    (reorg-open-sidebar
     `( :sources ((files . ,(concat "find "
				    dir
				    " -type f")))
	:format-results (.filename)
	:sort-results ((.filename . string<))
	:sort-groups string<
	:group (if (= (mod (length .filename) 2) 0)
		   "EVEN"
		 "ODD")
	:children  (( :group .!parent-dirs)))))

Or, you can put everything into a heirarchy and then group the items further:

(defun reorg-drill-then-group (&optional dir)
  (interactive "D")
  (reorg-open-sidebar
   `( :sources ((files . ,(concat "find "
				  dir
				  " -type f")))
      :format-results (.filename)
      :group .!parent-dirs
      :sort-results ((.filename . string<))
      :sort-groups string<
      :children (( :group (if (= (mod (length .filename) 2) 0)
			      "EVEN"
			    "ODD"))))))

You can mix different groups of things, for example, you could mix your orgmode entries and your files by sorting them according to the same rules and have the results exist in the same outline.

:children

Let’s return to our template and make it group all of the todo entries in your agenda files and sort them alphabetically:

(reorg-open-sidebar `( :sources ((org . ,(org-agenda-files)))
                       :group "Example template"
                       :children (( :group .todo
                                    :format-results (.stars " " .todo " " .headline)))))

I’ve decided that I do not like having the root heading there. Luckily you do not need a root heading and you can skip the first :group declaration:

(reorg-open-sidebar `( :sources ((org . ,(org-agenda-files)))
                       :children (( :group .todo
                                    :format-results (.stars " " .todo " " .headline)))))

Now, all of the TODO keywords will be root headings instead of part of a subtree.

:sort-groups

Let’s sort the todo keywords. :sort-groups accepts a function that takes two argument and returns t if the first should come before the second. The arguments to the function are the heading strings returned by the :group parameter.

(reorg-open-sidebar `( :sources ((org . ,(org-agenda-files)))
                       :children (( :group .todo
                                    :sort-groups string<
                                    :format-results (.stars " " .todo " " .headline)))))

Unlike some template components, :sort-groups is not inherited. It will only apply to the group in which it is declared.

It may be ideal to pass the function accepted by :sort-groups some metadata about the group instead of only the heading string, but because the outline is still being generated at the time :sort-groups is called, it’s not clear whether it would be useful.

:format-results

Format results tells reorg how to display the data. It is a list that contains either strings or dotted symbols. After the values of the dotted symbols are substituted into the list, the string is concatted together with concat. (Note: it is okay if the dotted symbols evaluate to nil.)

:format-results can transform the results in any way, e.g., adding text properties, overlays, performing calculations, transforming values.

:format-results ((replace-regexp-in-string (rx "reorg-"
                                               (zero-or-one "-"))
                                           ""
                                           .form-name)
                 (propertize " " 'display
                             `(space . (:align-to 70)))
                 (f-filename .file)))))

Again, all that matters is that each form within it returns a string or nil.

:format-results arguments are inherited. For example, in this template, botih groups would be rendered with the same result formatter.

:format-results (.stars " " .todo " " .priority " " .headline)
:children (( :group (when .timestamp "GROUP A"))
           ( :group (when (and .todo
                               (equal .todo "DONE"))
                      "GROUP B")))

But in this tempalte, only the second would use the formatter (the first would have to use one declared higher in the template, or the fallback format reorg-headline-format.

:children (( :group (when .timestamp "GROUP A"))
           ( :group (when (and .todo
                               (equal .todo "DONE"))
                      "GROUP B")
             :format-results (.stars " " .todo " " .priority " " .headline)))

Finally the .stars symbol you have seen refers to org-mode style stars showing the depth in the outline. It can be included or omitted. (Group headings always have leading stars, because Reorg relies on functions from outline-mode to handle folding. This reliance is out of laziness; in reality there is no need for Reorg’s folding or display to be confined to an outline and that code should be written as it would help free Reorg from org-mode styled outline trees.)

:sort-results

Currently, groups can only sorted by a single function. But results can be multi-sorted.

:sort-results accepts an alist in the form ‘((FORM . PREDICATE)) where FORM is code that determines what arguments are passed to the predicate function. For example:

:sort-results (( .todo . string<)
               ((downcase .headline) . string>))

Result sorters are inherited through the subtree. If additional result sorters appear within a subtree, they are added to the previously declared sorters such that the previous declarations have a higher precedence.

:bullet and :folded-bullet

This are strings that are used to replace the leading stars for each heading. For example, youc could use “->” and “–” to show if a heading is folded or not.

hidden options

:overrides and :post-overrides
:action-function

Capturing data into the outline and dynamic updates: org-capture as an example

Try to use reorg-org-capture-enable. It should update the single entry and then then find all of the places the edited entry should appear. No shit. It took me a long time to figure this out. It works for me. This means after you capture a note it will automatically propogate to the outline without reloading the entire outline.

Developing new modules

It’s probably best to look at the examples provided if you’re interested in this.

A class is created using the reorg-create-class macro. Here are two examples of class definitions. First, the class definition for org files:

(reorg-create-class-type
 :name org
 :getter (org-ql-select SOURCE nil :action #'PARSER))

Second, the class definition for files from the file system:

(reorg-create-class-type
 :name files
 :getter (cl-loop for each in (s-split "\n" (shell-command-to-string
                                             SOURCE)
                                       t)
                  collect (PARSER each))

A class definition has two required components: the name of the new class and a “getter.” It also has two optional components: a render function (keyword :render-func) and a keymap (keyword :keymap).

The :name argument is the name of the class that will be used then declaring a source in a template and when creating parsers for the data.

Writing a getter

The job of the “getter” is to fetch the data call tell Reorg where when and how the parser should be used on that data. There are two pre-defined variables that you must use when writing the getter: SOURCE and PARSER.

The SOURCE variable

The SOURCE variable refers to the input from the template that is supplied by the user. For example, in the above org-mode class, SOURCE is simply the path of an org-mode file.

This means that to use org-mode data in an outline, the user would specify the source like this:

'(:sources ((org . "~/.emacs.d/lisp/reorg/TEST/test.org")))

In the files example, SOURCE is a bash command that outputs a list of file paths (e.g., it could be find ~/ -type f). In a template using the files class, the user would specify the source like this:

'(:sources ((files . "find ~/.emacs.d -type f")))

But suppose that you did not want the user to have to type in a shell command to retrieve a list of files; instead, you only want the user to supply a directory. In other words, you want the template to look like this:

'(:sources ((files . "~/.emacs.d")))

Then the class getter would be defined this way:

(reorg-create-class-type
 :name files
 ;; . . . 
 :getter (cl-loop for each in (s-split "\n" (shell-command-to-string
                                             (concat "find "
                                                     SOURCE
                                                     " -type f"))
                                       t)
                  collect (PARSER each))

As you can see, SOURCE simply refers to the anticipated input from the template.

The PARSER function

See below.

Example: a json module

This parses and displays json data. It renders the underlying json file by narrowing it to the relevant region.

(reorg-create-class-type
 :name json
 :getter (with-current-buffer (find-file-noselect SOURCE)
           ;; This is the quickest and dirtiest way
           ;; I found to parse a json file which I have
           ;; no reason to ever do. It will properly
           ;; parse the test file at least. 
           (let ((json-array-type 'list)
                 (json-key-type 'symbol)
                 (json-object-type 'alist)
                 (json-null nil)
                 (json-false nil)
                 (file (buffer-file-name)))
             (save-excursion 
               (goto-char (point-min))
               (cl-loop for each in (json-read)
                        collect (append (list (cons 'file file)) each))))))

Then, to use that class:

(reorg-open-sidebar '( :sources ((json . "~/.emacs.d/lisp/reorg/TEST/y77d-th95.json"))
		       :group (if .year (substring .year 0 4) "Unknown year")
		       :sort-groups string>
		       :format-results (.mass "\t" .name " " .geolocation.type)
		       :sort-results (((if .mass (string-to-number .mass) "") . <))
		       :children (( :group (if .mass
					       (if (> (string-to-number .mass) 1000)
						   "Mass > 1000"
						 "Mass <= 1000")
					     "Mass Unknown")
				    :sort-groups string<))))

Specifying how and when to use the PARSER

Within the class creation macro, PARSER refers to a function that parsers the data. For the purposes of writing a class definition, you do not worry about how you are parsing the data; only worry about when you are parsing it. (We’ll deal with writing the parsers later using a separate macro called reorg-create-data-type, see infra.)

There two primary ways to fetch get data and call the PARSER.

First, for the org-mode class above, all parsing is done by physically moving through the buffer and calling the parsing function at each heading.

(reorg-create-class-type
 :name org
 ;; . . . 
 :getter (with-current-buffer (find-file SOURCE)
           (org-map-entries #'PARSER)))

You could, alternatively, use org-ql which does the same thing:

(reorg-create-class-type
 :name org
 ;; . . . 
 :getter (org-ql-select SOURCE nil :action #'PARSER))

In these examples, PARSER is called with no arguments because it is does not need any data; it gathers the data by examining a buffer.

But sometimes the data is not contained in a buffer. The second way to parse data is to call PARSER, with the data passed to it as an argument. For example, in the files example above which runs a shell command and receives a list of file paths, PARSER is called on each member of that list individually. In that case, PARSER is called with the data to be parsed as an argument:

(reorg-create-class-type
 :name files
 ;; . . . 
 :getter (cl-loop for each in (s-split "\n" (shell-command-to-string
                                             (concat "find "
                                                     SOURCE
                                                     " -type f"))
                                       t)
                  collect (PARSER each))

There are two other parts to writing a class definition: (1) writing a function that, when Reorg is used as a sidebar, displays the data at point in the main buffer; and (2) defining keyboard shortcuts to interact with the data. Because these are both optional, they are addressed below. The next section explains how to define what the PARSER actually does.

Creating data types and defining what the PARSER should parse

In the above examples, we know that PARSER is being called either at an org-mode heading or with the path of a file name. The question is what information we want to store from those sources and, optionally, how we want to display that data. This is accomplished with the reorg-create-data-type marco.

Handling data within a buffer

Suppose that we’ve created the same minimal org-mode class set out above:

(reorg-create-class-type
 :name org
 :getter (org-ql-select SOURCE nil :action #'PARSER))

Suppose we want to store the TODO state of an org entry for use in our outline.

(reorg-create-data-type
 :class org
 :name todo 
 :parse (org-entry-get (point) "TODO"))

This tells Reorg that we are adding data named “todo” to the org class. The macro will automatically create a function definition, add that function to an internal parser list, and call that function each time the PARSER function is invoked by the org class we defined above.

Reorg will store all parsed data in an alist, so the above data type would generate:

'((todo . "TODO")) ;;or "DONE" or whatever

You are not limited to pre-defined parts of the org-mode heading like TODO statets and priorities. For example, here is one that searching the heading for any words prefixed with an @ symbol and that stores a list of those words:

(reorg-create-data-type
 :class org
 :name at-names
 :parse (let ((headline (org-get-heading t t t t)))
          (cl-loop with start = 0
                   while (setq start (and (string-match "@\\([[:word:]]+\\)" headline start)
                                          (match-end 1)))
                   collect (match-string-no-properties 1 headline))))

Note that this will return a list with, potentially, multiple entries. That is not a problem because Reorg can clone these so each @name can appear in multiple places in the outline, as in the first example in this readme. See also deadling with multivalued data types, supra.

There is no limit to the data you can extract and save for later display.

Handling data passed to the parser as an argument

The approach is slightly different when you are dealing with data that is passed to the PARSER as an argument. For example, in the files example, supra, the PARSER was called with each path as an argument.

(reorg-create-class-type
 :name files
 :getter (cl-loop for each in (s-split "\n" (shell-command-to-string
					     SOURCE)
				       t)
		  collect (PARSER each))

To access the information passed to the parser when using the reorg-create-data-type macro, we use the variable data.

For example, suppose you were creating an outline and you wanted to sort a list of files by extension. Then we need to parse the file extension:

(reorg-create-data-type
 :class files
 :name extension
 :parse (f-ext data))

The variable data will always refer to the original data passed to PARSER.

Referencing previously parsed data

For either approach, you can also reference all the data that has has been generated by the other previous parsers within the same class. For example, suppose you parse and store a ts object that represents the deadline of a heading:

(reorg-create-data-type
 :class org
 :name ts-deadline
 :parse (when-let ((deadline (org-entry-get (point) "DEADLINE")))
          (ts-parse-org deadline)))

And suppose you also want to parse and store the name of the day associated with the deadline, so that your outline can include headings that include the name of the day of the week. Instead of parsing another ts object, you can simply refer to the previous data using the dot notation that is used in the template system:

(reorg-create-data-type
 :name ts-day-name
 :class org
 :parse (when .ts-deadline
          (ts-day .ts-deadline))
 :append t)

Alternatively, you can use the variable DATA which is the alist of all previously parsed data. Since Reorg stores all parsed data as an alist, so DATA is just an alist:

(reorg-create-data-type
 :name ts-day-name
 :class org
 :parse (when-let ((ts (alist-get 'ts-deadline DATA)))
          (ts-day ts))
 :append t)

Note the use of :append in both examples. If you want to refer to previously parsed data, you must ensure that parser is run after the parser generating the data you want to use. If the first parser that is run attempts to reference DATA, it will be nil because there will be nothing there to reference.

Writing a display function (optional)

When a user writes a template and tells Reorg how to format and display the results, the user uses dot notation. But sometimes that is not satisfactory. For example, (org-entry-get (point) "PRIORITY") will return “A”, “B”, or “C”. But no one wants to see A, B, or C in their outline because it would be ugly. To change how data is display, use the :display keyword. Like the parser, the display keyword can use dot-notation to refer to any previously parsed data (including the data generated by the current parser):

(reorg-create-data-type
 :class org
 :name priority
 :parse (org-entry-get (point) "PRIORITY")
 :display (pcase .priority 
            ("A" "")
            ("B" "")
            ("C" "")
            (_ " ")))

Now, if the user creates a template that displays the priority of an org heading, the data will be transformed and shown as ⚡, ➙, or ﹍ instead of A, B, or C. But the underlying data remains unchanged.

Note: the :display parameter is not the only way to customize how data is displayed because templates can also alter how data is displayed. The :display parameter is most useful if the data being parsed is not a string. For example, if you create a parser that stores an integer value, you should dislay it as a string.

For example, here is a parser for the files class that stores the depth of the file:

(reorg-create-data-type
 :name depth 
 :class files
 :parse (f-depth data))

If the user attempts to display the depth data in their outline by using .depth in the template, they’ll have to ensure that it’s transformed into a string. For example suppose the user tries to use a tempalte that formats the results like this:

:format-results ("Depth: " .depth " " .filename)

Reorg will err. It cannot concat .depth because it is an integer. Instead, the user’s template will have to convert it to a string:

:format-results (.stars " Depth: " (number-to-string .depth) " " .filename)

This is not best practice. The user should be able to assume that any stored data can be safely displayed.

But also assume (for whatever reason) you want to keep the data stored as an integer (or any other object). You do not want it stored as a string. You can avoid this mess with a definition that uses the :display keyword:

(reorg-create-data-type
 :name depth 
 :class files
 :parse (f-depth data)
 :display (number-to-string .depth))

Writing a render function (optional)

When a Reorg buffer is displayed in a side window, it can automatically render the data at point as the user moves through the outline. For Reorg to know how to display the data, it needs to be told how to do so. If no render function is provided in a class definition, then Reorg will not attempt to render the data. If a render function is provided, and the Reorg buffer is in a side window, then it will render the underlying data each time the user selects a heading in the outline.

Creating keyboard shortcuts

When you define a keyboard shortcut in a class, it will apply any entry in the outline that belongs to that class. See reorg-org.el, reorg-files.el, and other modules to see how this works.

Planned features

Changing views on the fly

For example hoisting headings / inverting the outline / multiple views

TODOs

Fix that sorting results doesn’t work using .! prefix(!?) Fixed [2024-02-25 Sun]

bind “?” to show the commands available at a heading

Write a proper example functions showing using it to: view leo files, view file systems, etc.

Clean up reorg-org data types. (E.g., timestamps are a mess.)

Allow the user to use nested dots referencing the class name, e.g., .org.filename and .files.filename to allow the user (as it’s often desired behavior) to avoid collusions when grouping

Fix the capf functions

Footnotes

[fn:1] For all I know this could be a better back end; it was published as I was completing the core grouping and sorting functions and I was already on my own path.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages