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.
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
- level 2 - week number
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.