how I organize my life in org-mode - using column view

This is how looks my habits or actions calendar. It helps me to monitor, discourage or embrace some actions.

output.gif

Let's try to describe what it is and what parts it is made of. I use Emacs & org-mode to create it. I assume you have a basic knowledge of these tools.

1. reverse date tree format

Calendar is based on simple 'date tree' format. It's just a set of headings arrange in a hierarchical structure:

  • level 1 - year
    • level 2 - week number
      • level 3 - day

I use slightly modify format. For example:

* 2024 (year)
** 18 (week number)
*** [2024-05-05 Sun] (inactive timestamp)
*** [2024-05-04 Sat]
*** [2024-05-03 Fri]
*** [2024-05-02 Thu]
*** [2024-05-01 Wed]
*** [2024-04-30 Tue]
*** [2024-04-29 Mon]
** 17
*** [2024-04-28 Sun]
*** [2024-04-27 Sat]
...

For the designation of days I use inactive timestamps. Because for me they are easier to generate then strings from 'date tree' format e.g. '2022-10-08 Saturday'. Additionally, I can query some data from timestamps using org-ql. E.g. find me days when I did more than 10 push-ups.
Inactive because I don't want them to be display in org-agenda. Reverse because the newest dates are on the top.

The biggest advantage of such a structure is the possibility of folding - the ability to collapse days into week and weeks into year.

1.1. divide time into years, weeks and days, skip months

I recommend to not include months because not all months are equal. A month has 30 or 31 days, and February can have 28 or 29. Most of the time a new month begins in the middle of the week - this completely mess up folding. For example:

* 2024 (year)
** may (month)
*** 18 (week) - (and we are left with 5 days left from previous week)
**** [2024-05-05 Sun]
**** [2024-05-04 Sat]
**** [2024-05-03 Fri]
**** [2024-05-02 Thu]
**** [2024-05-01 Wed]
** april (month)
*** 18 (week) - (this week have only 2 days because - [2024-04-30 Tue] is last day of the month)
**** [2024-04-30 Tue]
**** [2024-04-29 Mon]
*** 17
**** [2024-04-28 Sun]
**** [2024-04-27 Sat]
...

Therefore, I think it is much wiser to rely on the weeks. Week always starts on Monday and ends on Sunday and always consists of the same number of days - 7. So no surprises.

2. column view

So we have a 'reverse date tree' structure that allows us folding. Now we can initialize column view. This creates a special view: a tabel where our days, weeks and years are turns into rows, and in columns we can display selected data (properties) from days (e.g. number of push-ups), which are automatically calculated for weeks, and years.
We just need to add property :COLUMNS: to year heading. You can just copy-paste example below to your org-mode buffer and invoke command `org-columns' (C-c C-x C-c).

* 2024
:PROPERTIES:
:COLUMNS: %37ITEM(time) %WEIGHT(weight){mean;%.1f} %9EXCERCISE(excercise){X/} %S-FOOD-CHECK(s-food-10:00){X/} %POMODORO(pomodoro){X/} %NO-YOUTUBE-TILL-DONE(no-news-till){X/} %8RUN(run){X/} %E-FOOD-CHECK(e-food-19:00){X/} %TIME-TO-SLEEP-CHECK(sleep-23:00){X/} %NOCOFFE(no-coffe){X/} %10CLEAN(no-alcohol){X/} %NORMAL-FOOD(no-sweets){X/}
:END: 
** 18
*** [2024-05-05 Sun]
*** [2024-05-04 Sat]
*** [2024-05-03 Fri]
*** [2024-05-02 Thu]
*** [2024-05-01 Wed]
*** [2024-04-30 Tue]
*** [2024-04-29 Mon]

Most of the columns above are based on checkboxes. While in 'column view,' you can add an unchecked or checked checkbox to a specific row and column by pressing '1' or '2' on the keyboard.

To better understand how the 'column view' works and how to customize columns, I recommend the tutorial available at: Org Column View Tutorial.

3. monitor, discourage or embrace actions

I think actions can be broadly divided into two groups:

  • good actions to take, such as exercising
  • bad actions to avoid, such as eating sweets

I have listed the actions that I am currently using in the table below. I tried to describe them and specify their type. They are listed in chronological order, so the first action of the day is weighing, followed by exercise, and so on.

no. name desc type when? field type
1 weight body weight in kg, first action in the morning action everyday number
2 exercise type and number of reps are avail in 'excercise view' action everyday except Sunday and day after run or training checkbox
3 s-food-10:00 start food eating 10:00, so don't eat before 10:00 prevent-before-time everyday checkbox
4 pomodoro number of [completed / planned] Pomodoros action it depends number
5 no-news-till don't check any news site or YouTube, until you finish pomodoro prevent-before-action if pomodoro present checkbox
6 run running for 1 hour action at least once a week checkbox
7 e-food-19:00 end food eating 19:00, so don't eat after 19:00 prevent-after-time everyday checkbox
8 sleep-23:00 go to sleep before 23:00 action-before-time everyday checkbox
9 no-alcohol don't drink any alcohol beverages prevent-whole-day everyday checkbox
10 no-sweets don't eat any sweets prevent-whole-day everyday checkbox
11 no-coffe don't drink caffeine prevent-whole-day everyday checkbox

In my experience, the 'don't break the chain' method works very well. It is a productivity technique where you commit to performing an action every day and check off a checkbox on a calendar once the action is completed. The idea is to maintain a continuous chain of completed actions, providing visual reinforcement of your progress and helping to build momentum and consistency.

4. exercise view - statistics of exercises performed

Using the column view, I noticed that it would be nice to also collect some basic statistics on exercises performed every day. Below is my simple set of everyday exercises:

no. name number of reps
1 crunch 20
2 plank shoulder 10
3 birdie 20
4 plank 1 minute
5 lying leg raise 10
6 push-up 12
7 pull-up 3

But it's 7 exercises. If we add another 7 columns to our already crowded table, it would decrease readability and overload it with information.

5. column view with multiple views

So I wrote a little hack to be able to switch the column format line on the fly. To try it out copy-paste example below to your org-mode buffer.

* 2024
:PROPERTIES:
:COLUMNS: %37ITEM(time) %WEIGHT(weight){mean;%.1f} %9EXCERCISE(excercise){X/} %S-FOOD-CHECK(s-food-10:00){X/} %POMODORO(pomodoro){X/} %NO-YOUTUBE-TILL-DONE(no-news-till){X/} %8RUN(run){X/} %E-FOOD-CHECK(e-food-19:00){X/} %TIME-TO-SLEEP-CHECK(sleep-23:00){X/} %NOCOFFE(no-coffe){X/} %10CLEAN(no-alcohol){X/} %NORMAL-FOOD(no-sweets){X/}
:COLUMNS: %37ITEM(item) %CRUNCH(crunch){} %PLANK-SHOULDER(plank-shoulder){} %BIRDIE(birdie){} %PLANK(plank){} %LYING-LEG-RAISE(lying leg raise){} %PUSHUP(pushup){} %PULLUP(pullup){}
:END: 
** 18
*** [2024-05-05 Sun]
*** [2024-05-04 Sat]
*** [2024-05-03 Fri]
*** [2024-05-02 Thu]
*** [2024-05-01 Wed]
*** [2024-04-30 Tue]
*** [2024-04-29 Mon]

And evaluate code below or put in your Emacs initialization file.

(defun org-columns-switch-columns ()
  (interactive)
  (save-excursion
    (org-columns-goto-top-level)
    (re-search-forward ":PROPERTIES:")
    (let* ((folded-p (org-fold-folded-p))
           (beg (re-search-forward ":COLUMNS:"))
           (end (re-search-forward ":END:"))
           (num-of-columns (count-matches ":COLUMNS:" beg end)))
      (when folded-p
        (org-fold-hide-drawer-toggle))
      (goto-char beg)
      (dotimes (_ num-of-columns)
        (org-metadown))
      (re-search-backward ":PROPERTIES:")
      (when folded-p
        (org-fold-hide-drawer-toggle))
      (org-columns))))

(with-eval-after-load 'org-colview
  (org-defkey org-columns-map "x" #'org-columns-switch-columns))

(defun my/org-columns-get-format (&optional fmt-string)
  "Return columns format specifications.
When optional argument FMT-STRING is non-nil, use it as the
current specifications.  This function also sets
`org-columns-current-fmt-compiled' and
`org-columns-current-fmt'."
  (interactive)
  (let ((format
         (or fmt-string
             (progn
               (save-excursion (re-search-forward ":COLUMNS:\\s-*.*" nil t)
                               (replace-regexp-in-string ":COLUMNS:\\s-*" ""
                                                         (buffer-substring-no-properties
                                                          (line-beginning-position) (line-end-position)))))
             (org-with-wide-buffer
              (goto-char (point-min))
              (catch :found
                (let ((case-fold-search t))
                  (while (re-search-forward "^[ \t]*#\\+COLUMNS: .+$" nil t)
                    (let ((element (org-element-at-point)))
                      (when (org-element-type-p element 'keyword)
                        (throw :found (org-element-property :value element)))))
                  nil)))
             org-columns-default-format)))
    (setq org-columns-current-fmt format)
    (org-columns-compile-format format)
    format))

(with-eval-after-load 'org
  (advice-add 'org-columns-get-format :override 'my/org-columns-get-format))

I decided to set the default keybinding for invoking the command as 'x'. Initially, I wanted it to be the letter 's', which would align with the name of the command `org-columns-switch-columns' - 's' for switch. However, that keybinding was already in use. So, I opted for 'x', which could suggest 'eXchange'.
So to switch to another 'view' for column view just press 'x'.

To see exactly which lines have changed I'm also attaching a diff below:

diff --git a/lisp/org-colview.el b/lisp/org-colview.el
index e934ae67a..b5ef993e7 100644
--- a/lisp/org-colview.el
+++ b/lisp/org-colview.el
@@ -188,6 +188,7 @@ See `org-columns-summary-types' for details.")
   (org-cycle-overview)
   (org-cycle-content))

+(org-defkey org-columns-map "x"        #'org-columns-switch-columns)
 (org-defkey org-columns-map "c"        #'org-columns-content)
 (org-defkey org-columns-map "o"        #'org-overview)
 (org-defkey org-columns-map "e"        #'org-columns-edit-value)
@@ -830,6 +831,25 @@ around it."
     (org-columns-goto-top-level)
     fmt))

+(defun org-columns-switch-columns ()
+  (interactive)
+  (save-excursion
+    (org-columns-goto-top-level)
+    (re-search-forward ":PROPERTIES:")
+    (let* ((folded-p (org-fold-folded-p))
+          (beg (re-search-forward ":COLUMNS:"))
+          (end (re-search-forward ":END:"))
+          (num-of-columns (count-matches ":COLUMNS:" beg end)))
+      (when folded-p
+       (org-fold-hide-drawer-toggle))
+      (goto-char beg)
+      (dotimes (_ num-of-columns)
+       (org-metadown))
+      (re-search-backward ":PROPERTIES:")
+      (when folded-p
+       (org-fold-hide-drawer-toggle))
+      (org-columns))))
+
 (defun org-columns-get-format (&optional fmt-string)
   "Return columns format specifications.
 When optional argument FMT-STRING is non-nil, use it as the
@@ -839,7 +859,11 @@ current specifications.  This function also sets
   (interactive)
   (let ((format
         (or fmt-string
-            (org-entry-get nil "COLUMNS" t)
+             (progn
+               (save-excursion (re-search-forward ":COLUMNS:\\s-*.*" nil t)
+                               (replace-regexp-in-string ":COLUMNS:\\s-*" ""
+                                                         (buffer-substring-no-properties
+                                                          (line-beginning-position) (line-end-position)))))
             (org-with-wide-buffer
              (goto-char (point-min))
              (catch :found
--

That's all!
I hope that the information presented will be useful to you. If you have any questions, I'll be happy to answer them. I suggest asking them on Reddit.

Date: 2024-05-12 Sun 00:00

Author: Slawomir Grochowski

Email: slawomir.grochowski@gmail.com

Created: 2024-06-17 Mon 08:45