Org Clock in i3blocks

Lately I have been making use of org-clock to keep track of my time spent on various tasks while freelancing. For billing purposes and cost estimation it has been very valuable.

 * Clock Report

 #+BEGIN: clocktable :scope file :maxlevel 4
 #+CAPTION: Clock summary at  2024-01-31 Wed  22:20 
 | Headline               | Time   |      |      |
 |------------------------+--------+------+------|
 | Total time             | 0:40   |      |      |
 |------------------------+--------+------+------|
 | Clock Report           | 0:40   |      |      |
 | \_  Work Sessions      |        | 0:40 |      |
 | \_    Example Task     |        |      | 0:40 |
 #+END:

 ** Website

 * Example Task
 :LOGBOOK:
 CLOCK:  2024-01-30 Tue  12:52      2024-01-30 Tue  13:32  =>  0:40
 :END:
 - Shipping information
 - Processing time

When clocked in, I need to easily see it, so that I do not forget to clock out.

i3blocks_clock.png

Figure 1: Example

1. New Solution

My new solution is a bit lighter on the CPU. Specifically, it now writes the org modeline to a file so we can easily read it for display in i3blocks. Here is the Emacs configuration:

(defun write-mode-line-to-file ()
  "Write the current mode line string to /tmp/modeline."
  (with-temp-file "/tmp/org-clock"
    (insert (format "%s" (if (boundp 'org-mode-line-string)
                             org-mode-line-string "")))))

(defun delete-mode-line-file ()
  "Delete the /tmp/modeline file."
  (when (file-exists-p "/tmp/org-clock")
    (delete-file "/tmp/org-clock")))

(defvar custom/org-clock-update-timer nil
  "Timer for updating the mode line file while clocked in.")

(defun custom/org-clock-update-mode-line ()
  "Update the mode line file with the current mode line string."
  (when (and (boundp 'org-clock-current-task) org-clock-current-task)
    (write-mode-line-to-file)))

(defun custom/org-clock-in-advice (orig-fun &rest args)
  "Advice to write mode line to file on org-clock-in and start timer."
  (apply orig-fun args)
  (write-mode-line-to-file)
  (when custom/org-clock-update-timer
    (cancel-timer custom/org-clock-update-timer)
    (setq custom/org-clock-update-timer nil))
  (setq custom/org-clock-update-timer
        (run-at-time "1 min" 60 #'custom/org-clock-update-mode-line)))

(defun custom/org-clock-out-advice (orig-fun &rest args)
  "Advice to delete mode line file on org-clock-out and stop timer."
  (delete-mode-line-file)
  (when custom/org-clock-update-timer
    (cancel-timer custom/org-clock-update-timer)
    (setq custom/org-clock-update-timer nil))
  (apply orig-fun args))

(defun custom/org-clock-cancel-advice (orig-fun &rest args)
  "Advice to delete mode line file on org-clock-cancel and stop timer."
  (delete-mode-line-file)
  (when custom/org-clock-update-timer
    (cancel-timer custom/org-clock-update-timer)
    (setq custom/org-clock-update-timer nil))
  (apply orig-fun args))

(advice-add 'org-clock-in :around #'custom/org-clock-in-advice)
(advice-add 'org-clock-out :around #'custom/org-clock-out-advice)
(advice-add 'org-clock-cancel :around #'custom/org-clock-cancel-advice)

1.1. Adding it to i3blocks

You can add a section in the configuration file to call the script.

[org_clock]
command=test -f /tmp/org-clock && echo "⏰" $(cat /tmp/org-clock | sed 's/<//g; s/>//g; s/(//g; s/)//g; s/\[//g; s/\]//g')
interval=1
border=#d12755
border_top=1
border_right=0
border_bottom=0
border_left=0

2. Old Solution

2.1. Getting the org-clock information

I have written a small Python script that extracts the necessary information out of a running Emacs server.

#!/usr/bin/env python3
import time
import sys
import threading
from subprocess import run


def watchdog():
    def f():
        time.sleep(2)
        sys.exit(0)
    threading.Thread(target=f, daemon=True).start()


def main():

    # get current task
    current_task = run([
        "emacsclient",
        "--eval",
        "(if (boundp 'org-clock-current-task) org-clock-current-task \"\")",
    ], capture_output=True, shell=False, text=True).stdout

    # check if not clocked in
    if not current_task or current_task == "nil\n":
        return

    # get mode line string
    mode_line = run([
        "emacsclient",
        "--eval",
        "(if (boundp 'org-mode-line-string) org-mode-line-string \"\")",
    ], capture_output=True, shell=False, text=True).stdout

    # check if output invalid
    if not mode_line or not mode_line.startswith('#("'):
        return

    # success
    print("⏰ ", end="")
    print(mode_line.split('"')[1]
          .replace("<", "")
          .replace(">", "")
          .replace("(", "")
          .replace(")", "")
          .replace("[", "")
          .replace("] ", " - ")
          .strip())


if __name__ == "__main__":
    watchdog()
    try:
        main()
    except:
        pass
    sys.exit(0)

This outputs the following text, or nothing when not clocked in.

$ chmod +x ./org-clock
$ ./org-clock
⏰ 0:01 - Example Task

2.2. Adding it to i3blocks

You can add a section in the configuration file to call the script.

[org_clock]
command=~/.config/i3blocks/org-clock
interval=2
border=#d12755
border_top=1
border_right=0
border_bottom=0
border_left=0