To better adhere to the advice in Deep Work, create a simple system to approximate every minute of my schedule.
- Retain only required dependencies from utils files
Simsched is a (sim)ple (sched)uling emacs-lisp program designed to conform to the scheduling philosophy presented by Cal Newport in his book “Deep Work”.
The following excerpt can be found in Part 2: The rules, Rule #4: Drain the Shallows, Schedule Every Minute of Your Day, emphasis mine.
Here’s my suggestion: At the beginning of each workday, turn to a new page of lined paper in a notebook that you dedicate to this purpose. Down the left-hand side of the page, mark every other line with an hour of the day, covering the full set of hours you typically work. Now comes the important part: Divide the hours of your workday into blocks and assign activities to the blocks. For example, you might block off nine a.m. to eleven a.m. for writing a client’s press release. To do so, actually draw a box that covers the lines corresponding to these hours, then write “press release” inside the box. Not every block need be dedicated to a work task. There might be time blocks for lunch or relaxation breaks. To keep things reasonable clean, the minimum length of a block should be thirty minutes (i.e., one line on your page). This means, for example, that instead of having a unique small box for each small task on your plate for the day — respond to boss’s email, submit reimbursement form, ask Carl about report — you can batch similar things into more generic task blocks…
When you’re done scheduling your day, every minute should be part of a block. You have, in effect, given every minute of your workday a job. Now as you go through your day, use this schedule to guide you…
Two things can (and likely will) go wrong with your schedule once the day progresses. The first is that your estimates will prove wrong… The second problem is that you’ll be interupted and new obligations will unexpectedly appear on your plate. These events will also break your schedule.
This is okay. If your schedule is disrupted, you should, at the next available moment, take a few minutes to create a revised schedule for the time that remains in the day… Cross out the blocks for the remainder of the day and create new blocks to the right of the old ones on the page (I draw my boxes skinny so I have room for several revisions). On some days, you might rewrite your schedule half a dozen times. Don’t despair if this happens. Your goal is not to stick to a given schedule at all costs; it’s instead to maintain, at all times, a thoughtful say in what you’re doing with your time going forward — even if these decisions are reworked again and again as the day unfolds.
I’ve emphasized the key pieces of procedure and philosophy which underlie this scheduling program. I find this way of scheduling to be much more effective than some other, more fine-grained approaches, characteristic of more complex and flexible software (such as the agenda shipped with org-mode). I want to simply be aware of the macro-level objectives I have throughout the day, and track their evolution. I find that the more detail I put into a schedule, the more time I spend curating it, and the less it actually reflects my daily activities. The same philosophy is echoed in David Allen’s Getting Things Done.
The rest of this document is the entirety of the simsched program, written in a literate style.
To use this program, you can clone this repository somewhere on your emacs load-path
and load
it into your emacs config.
For example, if your config file is ~/.emacs
, and you have the path ~/.emacs.d/packages
on your load-path
:
- Clone this repository into
~/.emacs.d/packages/simsched
- load simsched in
.emacs
(load "./.emacs.d/packages/simsched/simsched.el")
If you would like to modify the software, you can edit this org file and tangle (with org-babel-tangle
) it yourself.
Entries will be a newline separated list of entries, where an Entry is defined as
Entry | TimeRange Objective TimeRange | Time-Time Time | [0-9] | 1[0|1|2] | [0-9]:[0|3]0 | 1[0|1|2]:[0|3]0 Objective | .*
for example
9-9:30 objective 11:30-2 This objective has a longer (single-line) description
These entries can be used in plaintext files, source code, markdown, org-mode headers, items, sub-items, etc. As long as there is only a single new-line separating them, the format holds.
(defun simsched/parse-time-range-entry ()
"Return the start time, end time, and subsequent text listed on a given line, if they exist."
(let* (
(time-regex "\\([[:digit:]][[:digit:]]?:[[:digit:]][[:digit:]]\\|[[:digit:]][[:digit:]]?\\)")
(range-regex (concat time-regex "-" time-regex))
(whole-range nil)
(start-time nil)
(end-time nil))
(condition-case nil
(progn
(beginning-of-line)
(re-search-forward range-regex (point-at-eol))
(setq whole-range (split-string (match-string 0) "-"))
(setq start-time (first whole-range))
(setq end-time (second whole-range))
;; reset the point to be at the end of the time range to get the event name
(beginning-of-line)
(re-search-forward "-" (point-at-eol))
(re-search-forward " " (point-at-eol))
(list start-time end-time (buffer-substring-no-properties (match-end 0) (point-at-eol))))
(error nil))))
(defun simsched/parse-schedule ()
"Expand region above and below current line to discover
all lines parseable as time ranges"
(let ((begin (point-at-bol))
(ranges-here-to-end '())
(ranges-beginning-to-here '())
(current-range (simsched/parse-time-range-entry)))
(if (not current-range)
(message "Point not inside a valid simple schedule.")
(save-excursion
(while current-range
(push current-range ranges-here-to-end)
(forward-line 1)
(setq current-range (simsched/parse-time-range-entry)))
(goto-char begin)
(forward-line -1)
(setq current-range (simsched/parse-time-range-entry))
(while current-range
(push current-range ranges-beginning-to-here)
(forward-line -1)
(setq current-range (simsched/parse-time-range-entry)))
(append ranges-beginning-to-here (reverse ranges-here-to-end))))))
try running (simsched/parse-schedule)
on the example below
9-9:30 meeting 9:30-10:30 meeting 2 10:00-11 unexpected thing 10:30-11:30 unexpected thing 2 12:00-1 unexpected thing 3
To pretty-print each time range entry, we can use unicode-enbox (and its dependencies)
(require 'cl) ; for setf, reduce
(autoload 'unicode-enbox "unicode-enbox" "Draw boxes around lines using unicode box-drawing characters")
Try running the below code to see the effect
(insert (unicode-enbox "Test"))
┌────┐ │Test│ └────┘
Because our format is flexible - allowing for both simple times (like 1
) and long-form times (like 1:00
),
we must normalize all times for a consistent representation within our program.
;; Assign synonymous times (1 and 1:00) with the same values, 1 representing A.M., the other P.M.
(setq simsched/normalize-time
#s(hash-table
size 24
test equal
data (
"1:00" (1 25)
"1" (1 25)
"1:30" (2 26)
"2:00" (3 27)
"2" (3 27)
"2:30" (4 28)
"3:00" (5 29)
"3" (5 29)
"3:30" (6 30)
"4:00" (7 31)
"4" (7 31)
"4:30" (8 32)
"5:00" (9 33)
"5" (9 33)
"5:30" (10 34)
"6:00" (11 35)
"6" (11 35)
"6:30" (12 36)
"7:00" (13 37)
"7" (13 37)
"7:30" (14 38)
"8:00" (15 39)
"8" (15 39)
"8:30" (16 40)
"9:00" (17 41)
"9" (17 41)
"9:30" (18 42)
"10:00" (19 43)
"10" (19 43)
"10:30" (20 44)
"11:00" (21 45)
"11" (21 45)
"11:30" (22 46)
"12:00" (23 47)
"12" (23 47)
"12:30" (24 48))))
;; When denormalizing times, always use the long form (1:00)
(setq simsched/denormalize-time
#s(hash-table
size 48
test equal
data (
1 "1:00"
25 "1:00"
2 "1:30"
26 "1:30"
3 "2:00"
27 "2:00"
4 "2:30"
28 "2:30"
5 "3:00"
29 "3:00"
6 "3:30"
30 "3:30"
7 "4:00"
31 "4:00"
8 "4:30"
32 "4:30"
9 "5:00"
33 "5:00"
10 "5:30"
34 "5:30"
11 "6:00"
35 "6:00"
12 "6:30"
36 "6:30"
13 "7:00"
37 "7:00"
14 "7:30"
38 "7:30"
15 "8:00"
39 "8:00"
16 "8:30"
40 "8:30"
17 "9:00"
41 "9:00"
18 "9:30"
42 "9:30"
19 "10:00"
43 "10:00"
20 "10:30"
44 "10:30"
21 "11:00"
45 "11:00"
22 "11:30"
46 "11:30"
23 "12:00"
47 "12:00"
24 "12:30"
48 "12:30")))
(defun simsched/normalize-schedule (schedule)
(mapcar (lambda (time-range-entry)
(list
(gethash (first time-range-entry) simsched/normalize-time)
(gethash (second time-range-entry) simsched/normalize-time)
(third time-range-entry)))
schedule))
But how will our program know whether the user means 1:00 A.M.
or 1:00 P.M.
if we only require them to enter 1
?
We will have to truncate the schedule to 12 hours only.
Consider the following example:
9:00-10:00 first thing 10:00-8:00 super super long thing 8:00-10:00 supposed to be after the last thing
If we allow 24 hours, our schedule will not be able to figure out if the third entry conflicts with the first, or if it comes after the second. We could require the user to enter a more descriptive format, and provide utility functions for doing so, but I am not willing to compromise on the simplicity of the format. I just want to be able to super quickly jot down entries and render the schedule.
So instead, we will define our schedule to have a maximum of 12 hours, starting with an hour configurable by the user. This way, we will disambiguate the case above - it will conflict with the first entry.
;;Begin by defining a group for all our customizations
(defgroup simsched nil
"Surround a string with box-drawing characters."
:version "0.0.1"
:link '(url-link :tag "GitHub" "http://github.com/mjdiloreto/simsched")
:prefix "simsched/"
:group 'extensions)
(defcustom simsched/start-time "7:00"
"First hour of the 12-hour schedule"
:group 'simsched
:type 'string
:options '("1:00" "2:00" "3:00" "4:00" "5:00" "6:00" "7:00" "8:00" "9:00" "10:00" "11:00" "12:00"))
With this custom variable in place, we can disambiguate cases like the above (The last entry will conflict with the first now).
(defun simsched/normalize-schedule-to-start-time (schedule)
"Normalize all time ranges and return the proper times given the user's customized start time."
(let ((custom-start (first (gethash simsched/start-time simsched/normalize-time)))
(result (list))
(am? t))
(dolist (range schedule (reverse result))
(let* ((normalized-start (gethash (first range) simsched/normalize-time))
(normalized-end (gethash (second range) simsched/normalize-time))
(am-start (first normalized-start))
(pm-start (second normalized-start))
(am-end (first normalized-end))
(pm-end (second normalized-end))
(should-start-am? (<= custom-start am-start))
(should-still-be-am? (<= (if should-start-am? am-start pm-start) am-end))
(normalized-range (list (third range))))
(progn
(if should-still-be-am? (push am-end normalized-range) (push pm-end normalized-range))
(if should-start-am? (push am-start normalized-range) (push pm-start normalized-range))
(push normalized-range result))))))
Once we have a parsed schedule, we need to decide how to place each time-range entry onto the formatted page.
Let’s begin by creating a grid representing (hour, task)
;; Decide our schedule should have max 8 columns.
;; Any more, and why do you have so many conflicts during the day?
(defvar simsched/MAX-COLUMNS 8)
(defun simsched/create-schedule-grid (schedule)
(let ((grid (let ((grid_ (make-vector 48 nil)))
(dotimes (i 48) (aset grid_ i (make-vector simsched/MAX-COLUMNS nil)))
grid_)))
(-each (simsched/normalize-schedule-to-start-time schedule) (lambda (range) (simsched/add-time-range-to-grid range grid)))
grid))
(defun simsched/add-time-range-to-grid (range grid)
"put range into the grid, inserting something to represent it"
(let* ((start-time (first range))
(end-time (second range))
(column (let ((look-at start-time)
(max-column 0))
(while (< look-at end-time)
(when (aref (aref grid look-at) max-column)
(progn (setq look-at start-time) (cl-incf max-column)))
(cl-incf look-at))
max-column)))
(while (< start-time end-time)
(aset (aref grid start-time) column (third range))
(cl-incf start-time))))
(setq simsched/test-schedule '(("9" "9:30" "first")("10:00" "11:00" "second")("10:30" "11:30" "conflict")("11:30" "1" "another thing until afternoon")("2:00" "4" "one more in afternoon")))
(setq simsched/test-schedule2 '(("7" "9:30" "first")("1:30" "8" "another thing until afternoon")("6:00" "8" "one more in afternoon")))
We want to render the grid consistently in the same buffer, and reuse it whenever possible. This is a similar strategy to other emacs programs.
We will allow the user to customize the buffer name and function used to switch to that buffer (in case the user wants the buffer in a certain window, new-frame, etc.)
(defcustom simsched/schedule-buffer-name "*simsched schedule*"
"The default name of the buffer where the rendered schedule is displayed"
:group 'simsched
:type 'string)
(defcustom simsched/switch-buffer-function #'pop-to-buffer
"Function called to display the schedule buffer."
:group 'simsched
:type 'function)
(defun simsched/get-create-schedule-buffer ()
(get-buffer-create simsched/schedule-buffer-name))
(defun simsched/switch-to-schedule-buffer ()
(interactive)
(funcall simsched/switch-buffer-function (simsched/get-create-schedule-buffer)))
Ultimately, we want the schedule to look like this
11:00┌────────┐ │ │ 11:30│some │ │task │ 12:00│here │ │ │ 12:30└────────┘ 1:00┌────────┐ │ │ 1:30│ │ │ Some │ 2:00│ other │ │ task │ 2:30│ here │ │ │ 3:00└────────┘
Breaking it down step-wise, we get the following procedures:
(defun simsched/render-time-labels ()
"Place time labels in the simsched buffer.
Assumes the buffer is empty."
(with-current-buffer (simsched/get-create-schedule-buffer)
(dotimes (n 48)
(let* ((time-string (gethash (1+ n) simsched/denormalize-time))
;; hacky, better to align w/ regexp but I can't find appropriate elisp function
(aligned-time-string (if (< (length time-string) 5) (concat " " time-string) time-string)))
(insert aligned-time-string)
(insert "\n")
(insert "\n")))))
(defun simsched/range-span (schedule-grid row column)
"Return the span (number of matching rows) that the item at row,column occupies.
e.g. An item that lasts 30 minutes will have a span of 1
An item that lasts 90 minutes will have a span of 3"
(let ((task-at-time (aref (aref schedule-grid row) column))
(bottom-row row)
(top-row row))
(while (and task-at-time (equal (aref (aref schedule-grid bottom-row) column) task-at-time))
(setq bottom-row (1+ bottom-row)))
(while (and task-at-time (equal (aref (aref schedule-grid top-row) column) task-at-time))
(setq top-row (1- top-row)))
(if (not task-at-time) 1
(+ (- row top-row 1)
(- bottom-row row)))))
(defun simsched/justify-entry (text max-width span)
"Try to split long entries so that they take the minimum horizontal space required.
max-width is a suggestion that `fill-region` will try to match without breaking words.
span is the total number of lines that the entry should fill after justification.
e.g. if max-width is 7 but the word is antidisestablishmentarianism, fill-region will not
break that word to 7 character chunks."
(with-temp-buffer
(insert text)
(setq fill-column max-width)
(fill-region (point-min) (point-max))
(let* ((lines (count-lines (point-min) (point-max)))
(newlines-above (floor (/ (- span lines) 2.0)))
(newlines-below (ceiling (/ (- span lines) 2.0))))
(goto-char (point-min))
(dotimes (_ newlines-above) (insert " \n"))
(goto-char (point-max))
(dotimes (_ newlines-below) (insert "\n "))
(buffer-string))))
(defun simsched/insert-time-at-junction (string-to-insert)
"When two time ranges share end/start time, a new special character must be used
to concatenate the two schedule boxes."
(let ((line-replacement (replace-regexp-in-string "┘" "┤"
(replace-regexp-in-string "└" "├"
(or (thing-at-point 'line t) "")))))
(beginning-of-line)
(unless (eobp) (kill-line))
(insert line-replacement)
(beginning-of-line)
(insert-rectangle (cdr string-to-insert))))
(defun simsched/render-schedule (schedule-grid)
"Display the given (normalized) schedule grid in the simsched schedule buffer"
(let ((column 0))
(with-current-buffer (simsched/get-create-schedule-buffer)
(erase-buffer)
(simsched/render-time-labels)
(while (< column simsched/MAX-COLUMNS)
(let* ((max-width (cl-reduce #'max
(mapcar (lambda (n)
(let* ((task-at-time (aref (aref schedule-grid n) column))
(length-of-entry (length task-at-time))
(span (simsched/range-span schedule-grid n column)))
(/ length-of-entry (- (1+ (* span 2)) 2))))
(number-sequence 0 47))))
(enboxed-tasks (with-temp-buffer
(let ((i 0)
(special-delimiter "s%!r!")) ; need a short special delimiter to not mess up the boxing width
(while (< i 48)
(let* ((span (simsched/range-span schedule-grid i column))
(task-at-time (aref (aref schedule-grid i) column))
(string-to-insert (if task-at-time (simsched/justify-entry task-at-time max-width (- (1+ (* span 2)) 2)) "\n")))
(insert string-to-insert)
(insert "\n")
(insert special-delimiter)
(insert "\n")
(matt/pad-lines-to-max-length)
(setq i (1+ i))))
(mapcar (lambda (entry) (if (string-match "[^\s\n]" entry) (unicode-enbox entry nil 'append 'append)))
(split-string (buffer-string) (concat "\n" special-delimiter "\s*" "\n"))))))
(rendered-column (with-temp-buffer
(let ((i 0))
(dotimes (i 48) (insert "\n"))
(while (< i 48)
(let* ((span (simsched/range-span schedule-grid i column))
(task-at-time (aref (aref schedule-grid i) column))
(string-to-insert (if task-at-time (split-string (nth i enboxed-tasks) "\n") (list ""))))
(goto-line (1- (* i 2)))
(end-of-line)
(if (and task-at-time (char-equal (or (char-before) ?a) ?┘))
(simsched/insert-time-at-junction string-to-insert)
(insert-rectangle string-to-insert))
(setq i (+ i span))))
(split-string (buffer-string) "\n")))))
(goto-char (point-min))
(end-of-line)
(insert-rectangle rendered-column)
(matt/pad-lines-to-max-length)
(setq column (+ column 1))))
(simsched/trim-schedule)))
(call-interactively #'simsched/switch-to-schedule-buffer))
(setq simsched/test-grid (simsched/create-schedule-grid simsched/test-schedule))
(defun simsched/trim-schedule ()
"Remove schedule lines that don't have any items rendered."
(let ((beginning-found nil)
(end-found nil))
(goto-char (point-min))
(while (not beginning-found)
(if (not (string-match "\u250c" (or (thing-at-point 'line t) ""))) ; top-left corner char
(kill-region (point-at-bol) (1+ (point-at-eol)))
(setq beginning-found t)))
(goto-char (point-max))
(while (not end-found)
(if (not (string-match "\u2514" (or (thing-at-point 'line t) ""))) ; bottom-left corner char
(kill-region (1- (point-at-bol)) (point-at-eol))
(setq end-found t)))))
(defun simsched-for-region-around-point ()
"Parse and render a simple schedule view for the schedule the point is within.
The schedule region is a series of newline-separated Entries, where an Entry conforms to:
Entry | TimeRange Objective
TimeRange | Time-Time
Time | [0-9]
| 1[0|1|2]
| [0-9]:[0|3]0
| 1[0|1|2]:[0|3]0
Objective | .*
"
(interactive)
(simsched/render-schedule (simsched/create-schedule-grid (simsched/parse-schedule))))
8-9 Emacs lisp coding 8:30-12 A conflict came up that I added later 9-10:30 Slack 11:00-12:30 Misc. task block 12-1:30 Another conflict came up 1-3 I planned on doing this already
8:00┌───────┐ │Emacs │ 8:30│lisp │┌────────┐ │coding ││ │ 9:00├───────┤│ │ │ ││A │ 9:30│ ││conflict│ │Slack ││came │ 10:00│ ││up │ │ ││that │ 10:30└───────┘│I │ │added │ 11:00┌───────┐│later │ │ ││ │ 11:30│Misc. ││ │ │task ││ │ 12:00│block │├────────┤ │ ││Another │ 12:30└───────┘│conflict│ │came │ 1:00┌───────┐│up │ │I ││ │ 1:30│planned│└────────┘ │on │ 2:00│doing │ │this │ 2:30│already│ │ │ 3:00└───────┘
Depending on the font used (GitHub’s is pretty ugly), the schedule will look more or less like this:
As you can see, I like to place conflicts immediately after the events they conflict with instead of placing them at the end of the schedule. This just helps keep things more or less chronological, but won’t affect the output of the program.
8-9 Emacs lisp coding 9-10:30 Slack 11:00-12:30 Misc. task block 1-3 I planned on doing this already 8:30-12 A conflict came up that I added later 12-1:30 Another conflict came up
results in the exact same rendered schedule as above.