Emacs: Display World Time

Recently, I started to use helm and found that M-x helm-world-time could display local time in cities worldwide.

For example, to display Beijing and Melbourne time, you just need to change the variable display-time-world-list:

(setq display-time-world-list '(("Asia/Shanghai" "China")
                                ("Australia/Melbourne" "Melbourne")))

  • “Asia/Shanghai” is the city name, “China” is just a label, you can set it to any string you like.
  • For other cities, please refer to this list.
  • Besides helm-world-time, you can also use the Emacs bundled display-time-world, which opens a buffer named *wclock* to show refreshing world time.

1 hacking log

This section records my thoughts through solving this problem. You might not be interested in.

helm-world-time can display a list of time in different time zones. However the default list doesn’t include what I need: Beijing (UTC+8) and Melbourne (UTC+10).

C-h f helm-world-time:

helm-world-time is an interactive autoloaded Lisp closure in `helm-misc.el'.

(helm-world-time)

Preconfigured `helm' to show world time.

Click helm-misc.el:

(defun helm-world-time ()
  "Preconfigured `helm' to show world time."
  (interactive)
  (helm-other-buffer 'helm-source-time-world "*helm world time*"))

Search helm-source-time-world in helm-misc.el to find its definition:

(defvar helm-source-time-world
  '((name . "Time World List")
    (init . (lambda ()
              (require 'time)
              (let ((helm-buffer (helm-candidate-buffer 'global)))
                (with-current-buffer helm-buffer
                  (display-time-world-display display-time-world-list)))))
    (candidates-in-buffer)
    (filtered-candidate-transformer . helm-time-zone-transformer)))

It is a variable. The value of init is a lambda, whose main content is (display-time-world-display display-time-world-list)

C-h v display-time-world-list:

display-time-world-list is a variable defined in `time.el'.
Its value is (("America/Los_Angeles" "Seattle")
 ("America/New_York" "New York")
 ("Europe/London" "London")
 ("Europe/Paris" "Paris")
 ("Asia/Calcutta" "Bangalore")
 ("Asia/Tokyo" "Tokyo"))


Documentation:
Alist of time zones and places for `display-time-world' to display.
Each element has the form (TIMEZONE LABEL).
TIMEZONE should be in a format supported by your system.  See the
documentation of `zoneinfo-style-world-list' and
`legacy-style-world-list' for two widely used formats.  LABEL is
a string to display as the label of that TIMEZONE's time.

You can customize this variable.

This variable was introduced, or its default value was changed, in
version 23.1 of Emacs.

This is the list of time being displayed. I need to remove the items in this list and append Beijing and Melbourne to it.

zoneinfo-style-world-list is a variable defined in `time.el'.
Its value is (("America/Los_Angeles" "Seattle")
 ("America/New_York" "New York")
 ("Europe/London" "London")
 ("Europe/Paris" "Paris")
 ("Asia/Calcutta" "Bangalore")
 ("Asia/Tokyo" "Tokyo"))


Documentation:
Alist of zoneinfo-style time zones and places for `display-time-world'.
Each element has the form (TIMEZONE LABEL).
TIMEZONE should be a string of the form AREA/LOCATION, where AREA is
the name of a region -- a continent or ocean, and LOCATION is the name
of a specific location, e.g., a city, within that region.
LABEL is a string to display as the label of that TIMEZONE's time.

You can customize this variable.

This variable was introduced, or its default value was changed, in
version 23.1 of Emacs.

Format is (TIMEZONE LABEL), where TIME is AREA/LOCATION, e.g. “Asia/Tokyo”, and LABEL is just a label and you can fill in whatever you like.

(setq display-time-world-list '(("Asia/Beijing" "China")
                                ("Australia/Melbourne" "Melbourne")))

Run M-x helm-world-time again. Melbourne time is correct, but Beijing time is not. It’s 10 hours earlier than Melbourne time, given that Melbourne is UTC10, I guess Asia/Beijing is regarded as UTC0, weird.

Change Asia/Beijing to China/Beijing. It still doesn’t work. Okay, I think I need to dive deeper, C-h f display-time-world-display :

display-time-world-display is a compiled Lisp function in `time.el'.

(display-time-world-display ALIST)

Replace current buffer text with times in various zones, based on ALIST.

Click time.el:

(defun display-time-world-display (alist)
  "Replace current buffer text with times in various zones, based on ALIST."
  (let ((inhibit-read-only t)
    (buffer-undo-list t)
    (old-tz (getenv "TZ"))
    (max-width 0)
    result fmt)
    (erase-buffer)
    (unwind-protect
    (dolist (zone alist)
      (let* ((label (cadr zone))
         (width (string-width label)))
        (setenv "TZ" (car zone))
        (push (cons label
            (format-time-string display-time-world-time-format))
          result)
        (when (> width max-width)
          (setq max-width width))))
      (setenv "TZ" old-tz))
    (setq fmt (concat "%-" (int-to-string max-width) "s %s\n"))
    (dolist (timedata (nreverse result))
      (insert (format fmt (car timedata) (cdr timedata))))
    (delete-char -1)))

This function first call the setenv sexp to set the timezone, then trigger (format-time-string display-time-world-time-format) to display the time, make sense. I need to know what setenv really does, C-h f setenv :

setenv is an interactive compiled Lisp function in `env.el'.

(setenv VARIABLE &optional VALUE SUBSTITUTE-ENV-VARS)

Set the value of the environment variable named VARIABLE to VALUE.
VARIABLE should be a string.  VALUE is optional; if not provided or
nil, the environment variable VARIABLE will be removed.

Interactively, a prefix argument means to unset the variable, and
otherwise the current value (if any) of the variable appears at
the front of the history list when you type in the new value.
This function always replaces environment variables in the new
value when called interactively.

SUBSTITUTE-ENV-VARS, if non-nil, means to substitute environment
variables in VALUE with `substitute-env-vars', which see.
This is normally used only for interactive calls.

The return value is the new value of VARIABLE, or nil if
it was removed from the environment.

This function works by modifying `process-environment'.

As a special case, setting variable `TZ' calls `set-time-zone-rule' as
a side-effect.

The last paragraph is the key, click set-time-zone-rules:

set-time-zone-rule is a built-in function in `C source code'.

(set-time-zone-rule TZ)

Set the local time zone using TZ, a string specifying a time zone rule.
If TZ is nil, use implementation-defined default time zone information.
If TZ is t, use Universal Time.

Instead of calling this function, you typically want (setenv "TZ" TZ).
That changes both the environment of the Emacs process and the
variable `process-environment', whereas `set-time-zone-rule' affects
only the former.

Wow, it’s C. I compiled Emacs a few days ago and I got all source code.

DEFUN ("set-time-zone-rule", Fset_time_zone_rule, Sset_time_zone_rule, 1, 1, 0,
       doc: /* Set the local time zone using TZ, a string specifying a time zone rule.
If TZ is nil, use implementation-defined default time zone information.
If TZ is t, use Universal Time.

Instead of calling this function, you typically want (setenv "TZ" TZ).
That changes both the environment of the Emacs process and the
variable `process-environment', whereas `set-time-zone-rule' affects
only the former.  */)
  (Lisp_Object tz)
{
  const char *tzstring;

  if (! (NILP (tz) || EQ (tz, Qt)))
    CHECK_STRING (tz);

  if (NILP (tz))
    tzstring = initial_tz;
  else if (EQ (tz, Qt))
    tzstring = "UTC0";
  else
    tzstring = SSDATA (tz);

  block_input ();
  set_time_zone_rule (tzstring);
  unblock_input ();

  return Qnil;
}

Is this the legendary GNU C style? I’m lost, I’m out of my comfort zone. Force myself to read, hmm, if Asia/Beijing is regarded as UTC0, then the following block must be executed:

else if (EQ (tz, Qt))
  tzstring = "UTC0";

Which means EQ(tz, Qt) is true. Hang on, what the hell is Qt? OK, let me generate tags for better code navigation. Create .projectile under emacs/src and M-x projectile-regenerate-tags, then press M-. on “Qt” to jump to its definition:

Lisp_Object Qnil, Qt, Qquote, Qlambda, Qunbound;

This is just a declaration. I give up, let Emacs C keep its mystique.

Google “setenv TZ beijing”, the first result is a Wikipedia page List of tz database time zones. Search “beijing” in it and find:

CN +3114+12128 Asia/Shanghai east China – Beijing, Guangdong, Shanghai, etc. +08:00 +08:00 Covering historic Chungyuan time zone.

Aha, I recall that when Linux installation requires me to choose a time zone, I never find the option “Beijing”. How come? Anyway, just give it a go:

(setq display-time-world-list '(("Asia/Shanghai" "China")
                                ("Australia/Melbourne" "Melbourne")))

M-x helm-world-time, it works!

Another found is, the document of variable display-time-world-list shows that it’s used by function display-time-world. M-x display-time-world, Emacs opens a buffer named *wclock* to display refreshing clocks, nice.

Leave a Reply