• Stars
    star
    386
  • Rank 108,070 (Top 3 %)
  • Language
    Emacs Lisp
  • License
    GNU General Publi...
  • Created almost 5 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A Gantt Chart (Calendar) for Org Mode

El Gantt – A Gantt chart/calendar for Orgmode

El Gantt creates a Gantt calendar from your orgmode files. It provides a flexible customization system with the goal of being adaptable to ?multiple purposes. You can move dates, scroll forward and backward, jump to the underlying org file, and customize the display.

This package is under ongoing development. Feature requests and bug reports are welcome. Help is welcome.

screenshots/output-2020-07-20-14:25:27.gif

Installation

Install the dependencies:

Clone this repository into your lisp directory:

cd ~/.emacs.d/lisp
git clone https://github.com/legalnonsense/elgantt.git

Then:

(add-to-list 'load-path (concat user-emacs-directory "lisp/elgantt/")) ;; Or wherever it is located
(require 'elgantt)

Disclaimer

This package is in-progress. I don’t know if that means it is in alpha testing, or beta, or whatever. Everything described below should work, and it is stable enough for my every day use. If this package is useful for you, please help me polish it so we can put in on MELPA.

First use

Run (elgantt-open). By default, Elgantt will use your org-agenda files.

Before running this on your org-agenda files, you may want to experiment with the test file.

Using the test file

Set elgantt-agenda-files to the location wherever you installed elgantt. If you cloned to the lisp subdirectory, then:

(setq elgantt-agenda-files (concat user-emacs-directory "lisp/elgantt/test.org"))

And run (elgantt-open). You’ll see this rather bland looking chart: screenshots/Screenshot_2020-07-20_20-20-20.png

Customization

All variables can get set with setq and there is no need to use the customization interface, or use-package’s :custom keyword.

Setting the header type

The headers of the calendar are defined by elgantt-header-type. There are four default values and an option to define a custom function which will be run at the first point of each org heading:

ValueBehavior
categoryGroup entries by their CATEGORY property, or the filename if no CATEGORY property is set.
hashtagGroup entries by tags which are prefixed by a hashtag.
outlineUse the outline structure
rootGroup by the root heading
functionRun the given function at point, grouping entries by the return value of the function

Setting the timestamps to display

Set the variable elgantt-timestamps-to-display to control what types of timestamps are displayed. This variable is a list which can contain any of:

  • deadline
  • timestamp
  • timestamp-ia
  • scheduled
  • timestamp-range
  • timestamp-range-ia

The order of the list matters determined precedence. Only the first type of entry found in a heading will be displayed with a character. I generally use (setq elgantt-timestamps-to-display '(deadline timestamp scheduled timestamp-range))

Display options

ValueBehavior
elgantt-startup-foldedIf non-nil, display all entries, grouped by header-type, on a single line; otherwise, entries are grouped under a header with one entry per line
elgantt-show-header-depthIf elgantt-header-type is set to ‘outline, then show the outline depth by inserting elgantt-level-prefix-char. If elgantt-header-type is not ‘outline, then this has no effect
elgantt-level-prefix-charThe character used to prefix nested entries.
elgantt-even-numbered-line-changeThis controls how much the percent the even numbered lines are offset from the background color; set to 0 if you don’t want any distinction
elgantt-scroll-to-current-month-at-startupScroll to the current month at startup, or keep the calendar at the first timestamp
elgantt-insert-blank-line-between-top-level-headerJust what it says.
elgantt-draw-overarching-headersDraw a line bracketing the start and end dates for the children of and top-level headers, assuming there is no date already associated with the header.
elgantt-header-column-offsetThe width of the header column.
elgantt-header-line-formatSee the section below detailing how to use this variable.
elgantt-exclusionsThis is a list of strings. Do not display any headers that appear in this list.
elgantt-insert-header-even-if-no-timestampInsert the header even if there is no timestamp associated with it.
elgantt-hide-number-lineHides the number line that appears at the top of the calendar

Other custom variables

VariableDefault
elgantt-deadline-character
elgantt-active-timestamp-character
elgantt-inactive-timestamp-character
elgantt-scheduled-character
elgantt-multiple-entry-character
elgantt-timestamp-range-start-character
elgantt-timestamp-range-end-character
elgantt-cal-timestamp-range-ia-start-character
elgantt-timestamp-range-ia-end-character
elgantt-agenda-files(org-agenda-files)
elgantt-skip-archivest
elgantt-start-date(concat (format-time-string “%Y-%m”) “-01”) (i.e., the current month)
elgantt-header-column-offset20

elgantt-start-date is probably the most important one here. This sets the cut-off date for when to ignore old entries.

Navigation commands

Key bindingCommand
fMove forward to next entry on the line
nMove backward to previous entry
nMove to the closest entry on the next line
pMove to the closest entry on the previous line
FScroll forward by one month
BScroll backward by one month
M-fShift date at point forward one day
M-bShift date at point backward one day
cMove calendar to current date
spaceNavigate to org heading at point
ReturnShow agenda for date at point

Note about cells with multiple entries: If a calendar cell has multiple entries, a special character will be displayed (“☰” by default). If you try to perform a function on one of these cells (e.g., navigating to the org file, shifting a date, etc.), you will be prompted to select the entry you want to perform the operation on.

Examples

These exampes all use the test.org file: (setq elgantt-agenda-files "~/.emacs.d/lisp/elgantt/test.org") (or wherever your elgantt direcctory is located).

A note about colorizing the outline

The examples that follow draw a gradient between the scheduled time of an entry and the deadline of the entry. (The scheduled date is not actually shown in the calendar.) This is not included in the package and you need to use a custom macro (shown below) to do it. I took this idea from the org-gantt package. It is not included by default because it only works if you use deadlines and scheduling in a particular way. I do not use colorize my calendars this way, but it makes for a good demonstration. The code necessary to do this, and an alternative way to use colors, are discussed below when explaining the elgantt-create-display-rule macro. If you want these colors to appear, evaluate this code and reload (i.e., C-r) the calendar:

(setq elgantt-user-set-color-priority-counter 0)
(elgantt-create-display-rule draw-scheduled-to-deadline
  :parser ((elgantt-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
                               (s-split " " colors)))))
  :args (elgantt-scheduled elgantt-color elgantt-org-id)
  :body ((when elgantt-scheduled
           (let ((point1 (point))
                 (point2 (save-excursion
                           (elgantt--goto-date elgantt-scheduled)
                           (point)))
                 (color1 (or (car elgantt-color)
                             "black"))
                 (color2 (or (cadr elgantt-color)
                             "red")))
             (when (/= point1 point2)
               (elgantt--draw-gradient 
                color1
                color2
                (if (< point1 point2) point1 point2) ;; Since cells are not necessarily linked in 
                (if (< point1 point2) point2 point1) ;; chronological order, make sure they are sorted
                nil
                `(priority ,(setq elgantt-user-set-color-priority-counter
                                  (1- elgantt-user-set-color-priority-counter))
                           ;; Decrease the priority so that earlier entries take
                           ;; precedence over later ones (note: it doesn’t matter if the number is negative)
                           :elgantt-user-overlay ,elgantt-org-id)))))))

Use outline structure, unfolded, with space between headers, and overarching header lines

(setq elgantt-header-type 'outline
      elgantt-insert-blank-line-between-top-level-header t
      elgantt-startup-folded nil
      elgantt-show-header-depth t
      elgantt-draw-overarching-headers t)

screenshots/unfolded-outline-with-space-betwee-headers.png

Same as above, but folded

(setq elgantt-header-type 'outline
      elgantt-insert-blank-line-between-top-level-header nil
      elgantt-startup-folded t
      elgantt-show-header-depth t
      elgantt-draw-overarching-headers)

screenshots/folded-outline.png Note: When two colored gradients overlap, the average of the two gradients will be used for the display. This way, you can still see both spans of time. (Though the result is not always pretty.)

Use hashtags, folded, with no spaces

(setq elgantt-header-type 'hashtag
      elgantt-insert-blank-line-between-top-level-header nil
      elgantt-startup-folded t)

screenshots/folded-hashtag-no-space.png

What does it look like unfolded?

screenshots/Screenshot_2020-07-20_20-39-11.png

A custom header

Here’s a silly example that will group headers by the first letter ofo the headline

  (setq elgantt-header-type (lambda () (substring (org-entry-get (point) "ITEM") 0 1)))
;; You’ll also want to set `elgantt-insert-header-even-if-no-timestamp' to nil, otherwise you’ll see single letter headers that are assocated with headlines without dates

screenshots/Screenshot_2020-07-20_20-48-32.png

Header line format

The variable elgantt-custom-header-line controls the format of the header line. It can use any of the properties that are in a cell. You can reference these properties with the :prop keyword, with or without the :elgantt- prefix. (For example, you can access the headline of a cell’s entry with :elgantt-headline or headline. There is also a unique property date-at-point which will display the date at point and that is not dependent on the properties stored in the given cell. If there are multiple entries in the cell, then the data will be separated with a pipe (i.e, |). You can align text in the headerline to the left, center, or right side of the header. If there is an overlap, the latter properties will take precedence over the former. If the property doesn’t return a string, it will be formatted into a string with (format "%s").

The :prop keyword can also be a function that is run at the cell at point.

The header line is disabled by default while I finish sorting out the variable and function that handles it. You can enable it with (setq header-line-format '(:eval (elgantt-header-line-function))). This will use the default value for elgantt-custom-header-line, which is:

(setq elgantt-custom-header-line '((:left ((:prop date-at-point
                                                  :padding 25)
                                           (:prop headline
                                                  :padding 25)))))

Here is another example:

(setq elgantt-custom-header-line '((:left ((:prop date-at-point
                                                  ;; you could also use, for example, 'elgantt-get-date-at-point
                                                  ;; or (lambda () (elgantt-get-date-at-point))
                                                  :padding 25)
                                           (:prop todo 
                                                  :padding 30)))
                                   (:center ((:prop headline)))
                                   (:right ((:prop hashtag
                                                   :padding 40
                                                   :text-props (face (:background "red")))))))

The header line is work-in-progress and this was my first attempt at a solution. Here is a list of all the properties:

KeywordDescription
:propAny property stored in a cell that is retrievable with elgantt-get-prop-at-point, or a function that is run at the cell at point
:paddingInteger which indicates the padding before the next entry (defaults to no padding)
:after-padIf the length of the string exceeds the value of :padding, still separate this entry from the following by this number of padding characters
:padding-charThe character used for padding. Can be any single character. Defaults to a space
:text-propsAny text properties associated with the text. For example, you can set a custom face with (face '(:background "red"))

Macro/configuration examples and explanations

Elgantt aims to provide a flexible way to customize calendar displays. Whether it hits its target is not my concern.

The elgantt-create-display-rule macro

This macro is used to customize the display of the calendar. It defines functions that are run at each cell after the calendar is generated. If a cell contains multiple entries, it will be run for each entry in the cell.

Accessing and adding properties

Before proceeding, here is a list of the properties that are included for each entry in the calendar:

The following properties are included in each cell by default:

PropertyValue
:elgantt-headlineText of the org headline (no text properties)
:elgantt-deadlineDeadline as a string YYYY-MM-DD, or nil
:elgantt-scheduledScheduled timestamp, or nil
:elgantt-timestampFirst active timestamp (date only) or nil
:elgantt-timestamp-iaFirst inactive timestamp (date only) or nil
:elgantt-timestamp-rangeActive timestamp range, as a list of two strings ‘(“YYYY-MM-DD” “YYYY-MM-DD”) or nil
:elgantt-timestamp-range-iaSame, but inactive timestamp range
:elgantt-categoryCategory property of the heading, or the filename if no category property is supplied
:elgantt-todoTODO type, no properties, or nil
:elgantt-markerMarker pointing to the location of the heading in the org buffer
:elgantt-fileFilename of the underlying org file
:elgantt-org-bufferBuffer for the underlying org heading
:elgantt-alltagsA list of all tags, including inherited tags, associated with the heading
:elgantt-headerHeader used for insertion into the calendar buffer. Depends on the value of elgantt-header-type
:elgantt-dateDate used for insertion into the calendar. Uses the first date found in elgantt-timestamps-to-display
:elgantt-hashtagAny hashtag (inherited) associated with the headline

All properties returned by (org-entry-properties) are also included in an entry’s property list.

Here are some basic examples of how to use the display customization macro.

Changing the color of certain cells

Suppose we want to change the background color of any cell with a “TODO” state to red:

(elgantt-create-display-rule turn-todo-red
  :args (elgantt-todo) ;; Any argument in this list is available in the body
  :body ((when (string= "TODO" elgantt-todo)
           ;; `elgantt--create-overlay' is generally the easiest way to create an overlay
           ;; since `ov' is not a dependency.
           (elgantt--create-overlay (point) (1+ (point))
                                '(face (:background "red"))))))

Some caveats: If there is already an overlay on the cell, you have to manage the overlay priorities for them to display properly. The manual is serious when it warns “you should not make assumptions about which overlay will prevail” when two overlays share the same priority (or do not have a priority).

For example, here we will choose an arbitrarily large priority to make sure this overlay is displayed over any others:

(elgantt-create-display-rule turn-todo-red
  :args (elgantt-todo) ;; Any argument listed here is available in the body
  :body ((when (string= "TODO" elgantt-todo)
           ;; `elgantt--create-overlay' is generally the easiest way to create an overlay
           (elgantt--create-overlay (point) (1+ (point))
                                '(face (:background "red")
                                       priority 99999)))))

If you want to make a dynamic display (i.e., one that updates every time you move), the post-command-hook keyword will add the function as a post-command-hook and run it each time the cursor moves. For example, suppose you want to make each cell red that matches the TODO state of the cell at point. We’ll use the the macro elgantt--iterate-over-cells to run the expression for each cell.

If you want to use this kind of display, then you’ll probably want to give the overlay a unique ID, and clear those overlay each time the cursor moves.

(elgantt-create-display-rule turn-matching-todos-red
  :args (elgantt-todo)
  :post-command-hook t ;; This will recalculate every time the point moves
  :body ((remove-overlays (point-min) (point-max) :turn-it-red t)
         ;; Since this will run each time the cursor moves, we need to clear
         ;; the previous overlays first
         (when elgantt-todo ;; make sure there is a todo state
             (elgantt--iterate-over-cells 
              (when (member elgantt-todo (elgantt-get-prop-at-point :elgantt-todo))
                (elgantt--create-overlay (point) (1+ (point))
                                     '(face (:background "red")
                                       priority 9999
                                       ;; arbitrary identifier
                                       ;; so we know what overlays to clear
                                       :turn-it-red t)))))))

Using the test.org file (where only a few of the headlines have TODO states), you’ll see this will turn the background of any entry that also has a TODO state when the point is on a cell with the same state: screenshots/output-2020-07-21-12:39:52.gif

If, during your experimentation, you want to disable a display rule, add :disable t and it will be removed from the function stack (or the post-command hook, if appropriate). In the alternative, call elgantt--clear-all-customizations which will delete any functions created by the customization macros.

Adding new properties from org files

Suppose you want to change the color of a cell based on a property that is not present by default. For example, you want to change the color if the cell has a certain priority, but that property is not included by default. In that case, use the :parser keyword to add a property. The expression is run at the first point of each org heading, and will be automatically added to the parsing function. The syntax is:

:parser ((property-name1 . ((expression)))
         (property-name2 . ((expression))))

So, to add the property to get the priority of an org heading:

(elgantt-create-display-rule priority-display
  :parser ((elgantt-priority . ((org-entry-get (point) "PRIORITY"))))
  :body (())) ;; insert code here, which can use elgantt-priority variable

You must reload the calendar after evaluating the macro so the calendar can repopulate and :elgantt-priority and its value will be added to each entry’s text properties.

Examples

Other ways to colorize time blocks

Here is how I colorize blocks of time. It depends on two org properties: ELGANTT-COLOR and ELGANTT-LINKED-TO. ELGANTT-COLOR is an org property that contains two color names, which will represent the start and end of a gradient. ELGANTT-LINKED-TO contains the ID of an org heading. This is different than the colorizing macro used for other examples, which colors a block starting with the scheduled date and ending with a deadline.

(setq elgantt-user-set-color-priority-counter 0) ;; There must be a counter to ensure that overlapping overlays are handled properly
(elgantt-create-display-rule user-set-color
  :parser ((elgantt-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
                           (s-split " " colors))))
           (elgantt-linked-to . ((org-entry-get (point) "ELGANTT-LINKED-TO"))))
  :args (elgantt-org-id)
  :body ((when elgantt-linked-to
           (save-excursion
             (when-let ((point1 (point))
                        (point2 (let (date) 
                        ;; Cells can be linked even if they are not 
                        ;; in the same header in the calendar. Therefore, 
                        ;; we have to get the date of the linked cell, and then
                        ;; move to that date in the current header
                                  (save-excursion (elgantt--goto-id elgantt-linked-to)
                                                  (setq date (elgantt-get-date-at-point)))
                                  (elgantt--goto-date date)
                                  (point)))
                        (color1 (car elgantt-color))
                        (color2 (cadr elgantt-color)))
               (when (/= point1 point2)
                 (elgantt--draw-gradient 
                  color1
                  color2
                  (if (< point1 point2) point1 point2) ;; Since cells are not necessarily linked in 
                  (if (< point1 point2) point2 point1) ;; chronological order, make sure they are sorted
                  nil
                  `(priority ,(setq elgantt-user-set-color-priority-counter
                                    (1- elgantt-user-set-color-priority-counter))
                              ;; Decrease the priority so that earlier entries take
                              ;; precedence over later ones
                    :elgantt-user-overlay ,elgantt-org-id))))))))

Linking cells with elgantt--connect-cells

Some samples here use the following macro to draw a line through cells which share the same hashtag. This code also adds a shortcut to move to the next matching hashtag:

(elgantt-create-display-rule show-hashtag-links
  :args (elgantt-hashtag)
  :post-command-hook t ;; update each time the point is moved
  :body ((elgantt--clear-juxtapositions nil nil 'hashtag-link) ;; Need to clear the last display
         (when elgantt-hashtag ;; only do it if there is a hashtag property at the cell
           (elgantt--connect-cells :elgantt-alltags elgantt-hashtag 'hashtag-link '(:foreground "red")))))

(elgantt-create-action follow-hashtag-link-forward
  :args (elgantt-alltags)
  :binding "C-M-f"
  :body ((when-let* ((hashtag (--first (s-starts-with-p "#" it)
                                       elgantt-alltags))
                     (point (car (elgantt--next-match :elgantt-alltags hashtag))))
           (goto-char point))))

(elgantt-create-action follow-hashtag-link-backward
  :args (elgantt-alltags)
  :binding "C-M-b"
  :body ((when-let* ((hashtag (--first (s-starts-with-p "#" it)
                                       elgantt-alltags))
                     (point (car (elgantt--previous-match :elgantt-alltags hashtag))))
           (goto-char point))))

screenshots/output-2020-07-20-14:14:55.gif

Helper functions

The following functions are included to aid customizing the display. See docstrings for more information.

Drawing the display

Create overlays with elgantt--create-overlay.
Draw a gradient with elgantt--draw-gradient.
Draw a progress bar with elgantt--draw-progress-bar.

Here is an example of how to use elgantt--draw-progress-bar Suppose you have the following org file:

* TODO read The Illuminatus! Trilogy 
SCHEDULED: <2020-06-02 Tue> DEADLINE: <2020-07-21 Tue>
:PROPERTIES:
:TOTAL_PAGES: 667
:PAGES_READ: 555
:ID:       99a97ef7-b555-4f98-bdd3-7e44510ac7a4
:END:

The following code:

(elgantt-create-display-rule pages-read-progress
  :parser ((total-pages . ((string-to-number			    
                            (org-entry-get (point) "TOTAL_PAGES"))))
           (pages-read . ((string-to-number
                           (org-entry-get (point) "PAGES_READ")))))
  :args (elgantt-deadline elgantt-scheduled)
  :body ((when (and elgantt-deadline elgantt-scheduled
                    total-pages pages-read)
           (let* ((start (progn (elgantt--goto-date elgantt-scheduled)
                                (point)))
                  (end (progn (elgantt--goto-date elgantt-deadline)
                              (point)))
                  (percent (/ (float pages-read)
                              (float total-pages))))
             (elgantt--draw-progress-bar "red" "blue"
                                         start
                                         end
                                         percent)))))

Will automatically display a progress bar starting at the scheduled date, to the deadline date, displaying a progress bar that represents the percent of pages read: screenshots/Screenshot_2020-07-21_09-37-17.png Note: the above code will generate an error if it is run on an org file that does not have the “TOTAL_PAGES” and “PAGES_READ” properties, because org-entry-get will return nil, which will cause string-to-number to fail. Instead, you should do something like:

:parser ((total-pages . ((--when-let (org-entry-get (point) "TOTAL_PAGES")
                           (string-to-number it))))
         (pages-read . ((--when-let (org-entry-get (point) "PAGES_READ")
                          (string-to-number it)))))

Or some other solution if you don’t like dash.

Draw a line from one cell to another with elgantt--draw-line. See also elgantt--connect-cell.
Juxtapose text on top of a cell with elgantt--insert-juxtaposition and clear them with elgantt--clear-juxtapositions.
Change the character of a cell (while preserving text properties) with elgantt--change-char.

Navigating the buffer

Move to a cell by org-id with elgantt--goto-id.
Move to a date on the current line with elgantt--goto-date.
Iterate over all entries with elgantt--iterate-over-cells

Selecting from multiple entries

Some cells will have multiple entries. To prompt the user to pick which one should be used: elgantt--select-entry.

Getting calendar data

To get the date at point: elgantt-get-date-at-point.
To get the properties of a cell: elgantt-get-prop-at-point.

This will always return a list, and if there are multiple entries in the cell at point it will list all values. Without any arguments, it will return all properties.

Editing the underlying org file

Use the macro elgantt-with-point-at-orig-entry to execute code at the underlying org heading.

Redrawing

You can’t reload a single cell because doing so invites catastrophe. But you can update all cells for the date at point: elgantt-update-this-cell.

The display (i.e., overlays) of a single cell can be redrawn with elgantt--update-display-this-cell or all cells with elgantt--update-display-all-cells.

If all else fails, reload everything with elgantt-open.

A note about org-ql: Org-ql creates a cache of its results and uses that cache until the underlying org file is changed. If you change something about the way the calendar is displayed, odds are that there will be a problem with using the org-ql cache. For this reason, all reloading invalidates the org-ql cache by calling elgantt--reset-org-ql-cache which simply sets org-ql-cache to its initial value. This seems to solve reloading problems.

Creating custom views

You can create custom views of the gantt chart/calendar by defining a function like this. Don’t try to let-bind the variables and then call elgantt-open open inside the closure; things will break. You can use setq and do not need to use the customize interface.

(defun elgantt-outline-folded ()
  (interactive)
  (setq elgantt-start-date nil
        elgantt-scroll-to-current-month-at-startup nil
        elgantt-agenda-files "~/.emacs.d/lisp/elgantt/test.org"
        elgantt-startup-folded nil
        elgantt-insert-header-even-if-no-timestamp t
        elgantt-header-type 'outline
        elgantt-show-header-depth t
        elgantt-header-column-offset 30
        elgantt-even-numbered-line-change 5)
  (elgantt-open))

If you want to use custom display macros, then you should call (elgantt--clear-all-customizations) and then include your custom macros inside the function.

Faces and themes

Elgantt should adjust its colors to work with your theme, regardless of whether it is dark or light.

Interacting with the calendar

There are two ways to interact with the calender: the elgantt-create-action macro and the separate module, elgantt-interaction.

elgantt-create-action

This macro works the same way as elgantt-create-display-rule except that has keywords for binding commands. I don’t use this macro for anything, but you could use it to perform actions on the org-file from the calendar (e.g., marking a TODO as DONE).

elgantt-interaction

To use this, you must (require 'elgantt-interaction).

This module experimental. The code is not cleaned up. It was written in a frenzy of wondering whether I could without considering whether I should. If this inspires ideas for others to use it, I will return to it. Otherwise, unless I have a need, I plan to abandon it.

Here is an example I use to set the :ELGANTT-LINKED-TO and :ELGANTT-COLOR property used in the example above. It is designed to allow the user to select cells and perform actions on them in a certain sequence. Here, it allows the user to make two selections, and when return is pressed, it will prompted the user to enter two colors, and then set the properties of the relevant org heading.

While this example works, the code in elgantt-interaction is generally untested. I do not know whether I will develop it further absent a need to do so. The framework, in theory, provides a robust way to create ways to interact with the calendar and perform actions on multiple org entries.

To invoke the interface, press a to be prompted to select which interface you’d like to execute. After that, a counter should appear which shows the number of cells selected. The message displayed is defined by the :selection-messages keyword. Once the cells are selected (by pressing space), the user presses Return to execute the command. The execution functions will be run in the order listed in :execution-functions. The first number refers to cells in the order in which they were selected. The variable return-val is the return value of the previous function.

So, here, the user selects two cells and presses return. Then, the program moves to the second selected cell, and runs org-id-get-create, and returns the value. The section function moves to the first cell that the user selected, and adds the ID of the second selection (i.e., return-val), and then prompts the user for two colors and sets the properties of that heading appropriatly.

In addition to being able to use numbers to refer to cells by the order in which they were selected, you can use all, rest, all-but-last, and last to refer to the cells and perform operations on them.

  (require 'elgantt-interaction)

  (elgantt--selection-rule
   :name colorize
   :selection-number 2
   :selection-messages ((1 . "Select first cell")
                        (2 . "Select second cell"))
   :execution-functions ((2 . ((elgantt-with-point-at-orig-entry nil
                                   (org-id-get-create))))
                         (1 . ((elgantt-with-point-at-orig-entry nil
                                   (org-set-property "ELGANTT-LINKED-TO" return-val)
                                 (org-set-property "ELGANTT-COLOR" (concat (s-trim (read-color "Select start color:"))
                                                                           " "
                                                                           (s-trim (read-color "Select end color:")))))))))

;; You’ll also need to use this to colorize 
(setq elgantt-user-set-color-priority-counter 0) ;; There must be a counter to ensure that overlapping overlays are handled properly
  (elgantt-create-display-rule user-set-color
    :parser ((elgantt-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
                             (s-split " " colors))))
             (elgantt-linked-to . ((org-entry-get (point) "ELGANTT-LINKED-TO"))))
    :args (elgantt-org-id)
    :body ((when elgantt-linked-to
             (save-excursion
               (when-let ((point1 (point))
                          (point2 (let (date) 
                          ;; Cells can be linked even if they are not 
                          ;; in the same header in the calendar. Therefore, 
                          ;; we have to get the date of the linked cell, and then
                          ;; move to that date in the current header
                                    (save-excursion (elgantt--goto-id elgantt-linked-to)
                                                    (setq date (elgantt-get-date-at-point)))
                                    (elgantt--goto-date date)
                                    (point)))
                          (color1 (car elgantt-color))
                          (color2 (cadr elgantt-color)))
                 (when (/= point1 point2)
                   (elgantt--draw-gradient 
                    color1
                    color2
                    (if (< point1 point2) point1 point2) ;; Since cells are not necessarily linked in 
                    (if (< point1 point2) point2 point1) ;; chronological order, make sure they are sorted
                    nil
                    `(priority ,(setq elgantt-user-set-color-priority-counter
                                      (1- elgantt-user-set-color-priority-counter))
                                ;; Decrease the priority so that earlier entries take
                                ;; precedence over later ones
                      :elgantt-user-overlay ,elgantt-org-id))))))))

screenshots/output-2020-07-21-12:27:23.gif

Here is a second example I played with previously, which provided a more advanced way to link cells/headings together. You can see the use of return-val being passed from one execution function to the next. This is included only for the purposes of illustrating how to use the macro.

(elgantt--selection-rule :name set-anchor
		     :parser ((:elgantt-dependents . ((when-let ((dependents (cdar (org-entry-properties (point)
												     "ELGANTT-DEPENDENTS"))))
						    (s-split " " dependents)))))
		     :execution-functions ((2 . ((elgantt-with-point-at-orig-entry nil
									       (org-id-get-create))))
					   (1 . ((elgantt-with-point-at-orig-entry nil
									       (let ((current-heading-id (org-id-get-create)))
										 (org-set-property "ELGANTT-DEPENDENTS"
												   (format "%s"
													   (substring 
													    (if (member return-val elgantt-dependents)
														elgantt-dependents
													      (push return-val elgantt-dependents))
													    1 -1)))))))
					   (2 . ((elgantt-with-point-at-orig-entry nil
									       (org-set-property "ELGANTT-ANCHOR" return-val)))))
		     :selection-messages ((1 . "Select the anchor.")
					  (rest . "Select the dependents."))
		     :selection-number 0)

This was previously accompanied by code that allowed the user to move the date of dependent cells by moving the anchor cell, and which highlighted all dependent cells when the point was on an anchor. I abandoned this for various reasons. If there is interest in this level of interface I can clean it up and get it working.

FAQ

Your code…

I’ll save you the trouble:

screenshots/code_quality.png

This is a hobby and a continued exercise in learning elisp and programming, and I realized a lot of things along the way. Mostly, I realized that programming is not as much fun as I thought it was, and takes way more time than it should. I don’t have the patience to clean up the code like I should. There are byte-compile warnings. I do not care.

I originally wrote that I hoped publishing this would get it out of my life, but it seems there is interest so I will push this as far as my time and ability will allow. If you can help, please help.

Can you fold and unfold without reloading?

Not without significant changes to the code, or breaking other existing features. You’ll just have to change the value of elgantt-startup-folded and reload.

Why so many gradients?

They are pretty. You can also customize where the midpoint of the gradient appears so it reflects remaining time. If you don’t like gradients, then just use the same start and end color.

Change log

[2020-07-31 Fri]

Added elgantt-custom-header-line and elgantt--header-line-formatter