Introduction
So, a while ago I've gone down the rabbit hole - and I don't intend to come up again.
For multiple reasons, one of them just being curiosity, I started using Emacs. And before anyone wants to start waging the holy war of editors1, I'll put myself out there and pronounce that the one and only correct answer is: Emacs with EVIL (GitHub) mode.
I'm currently working on a time tracking tool with an analysis component. Time is tracked in a simple text file and supports tagging. A sample file looks like this:
2024|08|24
8.00 fixed some bugs in time tracker #timetracker
9.00 requirements engineering for TT024 #timetracker #re #tt024
10.00 impl. TT024 #timetracker #tt024
12.00
And while the format is simple enough and Emacs supplies basic auto-completion, there are a few things that would make life even easier.
Inserting the current date and time
It would be nice if I could just press some key combination to insert the current date, and another one to insert the current time. So, I set out to figure out how to do this and started reading up in the Emacs manual. I knew that Emacs has the concept of major and minor modes because of org mode and programming language specific modes. After reading a bit I decided to implement a major mode for my time tracking project. Not sure whether a simple minor mode would've been enough for the shortcut feature, but I'm quite sure I'll need a major mode for the next feature ๐.
Creating a major mode is surprisingly easy. Just create an .el
file and use define-derived-mode
to define a major mode that derives from an existing mode. My mode is named time-tracking-mode
and derives from the basic text-mode
. "ttr"
is the name that is displayed in Emacs when the major mode is active, and the last string is the documentation string.
;; time-tracking-mode.el
(define-derived-mode time-tracking-mode
text-mode
"ttr"
"Major mode for time tracking.")
Next, we can load the mode by evaluating the buffer containing time-tracking-mode.el by M-x: eval-buffer
, or SPC m b e
(Doom Emacs). Alternatively we can run M-x: load-file
and provide the path to the file. After this, we can now select the major mode for our time tracking text file by opening the corresponding file and then M-x: time-tracking-mode
. It doesn't do much yet, except showing ttr
in the lower right corner - small wins.
With our major mode running smoothly, we can now add the shortcuts. For this we define the following variable above the definition of our mode definition.
(defvar-keymap time-tracking-mode-map
:parent text-mode-map
:doc "Keymap for `time-tracking-mode'."
"C-c C-d" #'insert-current-date
"C-c C-t" #'insert-current-time)
The name of the variable matters, it has to have the form *variant*-map
. The variant
is the first argument to the call to define-derived-mode
, in our case time-tracking-mode
. We inherit from text-mode-map
by specifying it as the :parent
. :doc
is simply the documentation string. The next two lines define the actual shortcuts and the corresponding function to call. Control+C Control+d
inserts the current date, and Control+C Control+t
inserts the current time. The methods look as follows:
(defun insert-current-date ()
(interactive)
(insert (format-time-string "%Y|%m|%d")))
(defun insert-current-time ()
(interactive)
(insert (format-time-string "%H.%M")))
Important: Variable definitions like
defvar-keymap
are only executed the first time. Therefore we have to either close and reopen Emacs or create another major mode ๐ to test changes to variables.
Let's try if it works.
Great, on to the next feature.
"Syntax" highlighting
I want the tags to be drawn in a different color. The corresponding feature is called "font-locking" - took me some time to figure this out ๐.
We can define simple regex rules to color text.
(defvar time-tracking-font-lock-keywords
'(("[0-9]\{1,2\}\.[0-9]\{2\}" . 'font-lock-function-name-face)
("[0-9]\{4\}|[0-9]\{2\}|[0-9]\{2\}" . 'font-lock-function-name-face)
;; #[^#\n]+\\b doesn't work, neither does #[^#\\n]+\\b as both are evaluated to #[^#n]+\\b. We need an actual line break in the pattern.
("#[^#
]+\\b" . 'font-lock-constant-face))
"Keyword highlighting specification for `time-tracking-mode'."
)
And then we set the variable in our major mode:
(define-derived-mode time-tracking-mode
text-mode
"ttr"
(setq-local font-lock-defaults '(time-tracking-font-lock-keywords)) ;; <- this line is new
"Major mode for time tracking.")
Entire major mode
Here's the entire major mode in all its glory.
(defvar-keymap time-tracking-mode-map
:parent text-mode-map
:doc "Keymap fr `time-tracking-mode'."
"C-c C-d" #'insert-current-date
"C-c C-t" #'insert-current-time)
(defvar time-tracking-font-lock-keywords
'(("[0-9]\{1,2\}\.[0-9]\{2\}" . 'font-lock-function-name-face)
("[0-9]\{4\}|[0-9]\{2\}|[0-9]\{2\}" . 'font-lock-function-name-face)
;; whatever you do, do NOT remove the line break within the regex pattern. Elisp interprets \n as just an n and the line break has to inserted with C-q C-j - actuall, a simple [RET] linebreak works as well.
("#[^#
]+\\b" . 'font-lock-constant-face))
"Keyword highlighting specification for `time-tracking-mode'.")
(define-derived-mode time-tracking-mode
text-mode
"ttr"
(setq-local font-lock-defaults '(time-tracking-font-lock-keywords))
"Major mode for time tracking.")
(defun insert-current-date ()
(interactive)
(insert (format-time-string "%Y|%m|%d")))
(defun insert-current-time ()
(interactive)
(insert (format-time-string "%H.%M")))
Improvements for another time
Loading and choosing the major mode every time is a bit of a hassle. There are ways to load it automatically when Emacs starts and associate the mode with a file extension so it gets activated every time a file with the extension is opened in a buffer.
Closing and opening Emacs every time changes are made to a variable is annoying, maybe there's a way to unload the major mode?
The Struggles
One of the biggest struggles was to figure out what was necessary and how to implement it. The Emacs manual is - not very beginner friendly? Regular expressions in Emacs Lisp almost drove me mad. It took me a long while to figure out a way to efficiently test a regular expression. As you might have noticed in the corresponding code, I had an issue with line breaks in the expression. The expression worked in regex101 but ignored to ignore line breaks in Emacs. This lead to the next line after a tag being matched as well. My first reaction was to just write an Emacs Lisp function to test arbitrary regular expressions. Just create a small function that returns all matches for a regular expression on a given input text, right? Something like this ought to do:
(defun get-matches (regext text)
(let ((matches nil))
(while (re-search-forward regex text)
(push (match-string 0) matches)))
The problem is the second parameter of re-search-forward
is actually not the text and the function uses the current buffer as the target text to match the expression. There might be a way to make this work, but not today. So, I took to the internet and found that there's a built in tool called re-builder to test regular expressions. M-x re-builder
it was, wrote my regular expression #(^#\n)+
and confusion ensued, it did what I wanted it to do: tags highlighted, stopping at line breaks. WTF! But, hold on. Re-builder has multiple modes which can be changed via C-c TAB
. The default mode is read
, when I switched to string
the regular expression changed from
and this marked the end of my Odyssey. Replacing `\n' with an actual line break was all it took.
In retrospect, I didn't take enough time to really "immerse" myself in the Emacs documentation. The initial win of creating a major mode that did something - even if it just was being selectable and showing "ttr" in the lower right corner - made me want to rush the rest. Especially for the regex stuff I should've done more research before just blindly doing what I'd do in other languages.
Thanks for reading, and keep on struggling.
-
Although, some argue Emacs is so much more than simply an editor, which is fair enough.ย โฉ
Top comments (0)