Emacs 中显示世界各地时间

最近开始使用 helm, 实验其各种功能, 偶然发现 M-x helm-world-time 可以显示世界各时区的时间.

比如, 想要显示北京时间和墨尔本时间, 只要把 display-time-world-list 这个变量修改一下即可:

(setq display-time-world-list '(("Asia/Shanghai" "天朝")
                                ("Australia/Melbourne" "猫本")))

  • 第一项”Asia/Shanghai”是时区的名字, 第二项”天朝”是用于显示的标签, 可以自己随意填.
  • 其它时区的名字, 可以参照这个列表.
  • 除了 helm-world-time 之外, 也可以用 Emacs 自带的 display-time-world, 它会开一个名为 *wclock* 的 buffer 来显示世界各地时间, 而且里边的内容会不停更新.

1 hack 过程

下面是我解决问题时, 对自己思路的记录. 对 elisp 感兴趣的不妨看看, 另外也望高手指点一二

helm-world-time, 可以显示世界各地的时间. 但是默认情况下, 没有我需要的北京时间(东八区)和墨尔本时间(东十区).

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.

点进 helm-misc.el 中看看:

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

在 helm-misc.el 搜索一下, 找到了 helm-source-time-world 的定义:

(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)))

是个 variable, 其中 init 对应的是个 lambda, 主要内容是: (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.

这就是那一列被显示的时间, 我需要把这个 list 改成北京时间和墨尔本时间, 格式在 zoneinfo-style-world-list 中有说明, 点进去看看:

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.

格式是 (TIMEZONE LABEL), 其中 TIMEZONE 是 AREA/LOCATION, 比如 “Asia/Tokyo”, 而 LABEL 就是个标签, 随便写什么.

但是没说都有哪些 TIMEZONE, 自己试试:

(setq display-time-world-list '(("Asia/Beijing" "天朝")
                                ("Australia/Melbourne" "猫本")))

再次 M-x helm-world-time, 发现墨尔本的时间显示对了, 但是天朝的时间不对, 正好比墨尔本时间提前10个小时, 墨尔本是东10区, 也就是说 Asia/Beijing 被当成了 UTC0.

试着把 Asia/Beijing 换成 China/Beijing, 也还是不行. 还是看看 display-time-world-display 这个函数是怎么回事儿把, 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.

点进 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)))

setenv 设置时区, 然后通过 (format-time-string display-time-world-time-format) 显示时间, 看一下 setenv 的说明, 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.

最后一段说, 如果参数是 TZ, 那么就会调用 set-time-zone-rule 这个函数, 点进去看一下:

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.

这函数是 C 语言写的, 我的 emacs 是自己编译的, 正好有源文件:

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;
}

这就是传说中的 GNU 风格? 晕了, 我对 emacs 的 C 源代码完全不熟悉, 自己那点儿 C 知识早还给虎哥和振宇哥了. 强看一下, 之前猜测 Asia/Beijing 被当作 UTC0, 那么应该是下面这句被执行了:

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

也就是说 EQ(tz, Qt) 成立, 可是 Qt 是什么啊? 还是生成个 tags 继续读吧, 在 emacs/src 下创建一个 .projectile 文件, 然后 M-x projectile-regenerate-tags 生成 tags, 在 Qt 那儿按 M-. 跳到其定义处:

Lisp_Object Qnil, Qt, Qquote, Qlambda, Qunbound;

只是个声明…算了, 还是让我保持对 Emacs C 代码的神秘感吧.

Google 搜索一下 “setenv TZ beijing”, 第一个结果 (google.com.au) 就是维基百科的 List of tz database time zones, 点进去, 搜 beijing, 发现:

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

突然想起每次装 Linux 选时区的时候, 都是有 Shanghai 没 Beijing, 魔都为什么这么吊…

试一下:

(setq display-time-world-list '(("Asia/Shanghai" "天朝")
                                ("Australia/Melbourne" "猫本")))

M-x helm-world-time , 成了!

另外一个发现是, 在看 display-time-world-list 的文档的时候, 发现它也被 display-time-world 调用. M-x display-time-world, 会开一个名为 *wclock* 的非临时 buffer 来显示世界各地时间, 而且里边的内容会不停更新.