Emacs Hangul Input

Table of Contents

Note: org-mode help links don't work when exported to html; best viewed in Emacs.

Keyboard layout

The variable quail-keyboard-layout-alist by default contains six layouts: all qwerty.

First, we need to define and enable a dvorak layout:

(with-eval-after-load 'quail
  (push
   (cons "dvorak"
         (concat
          "                              "
          "`~1!2@3#4$5%6^7&8*9(0)[{]}    "   ; numbers
          "  '\",<.>pPyYfFgGcCrRlL/?=+\\|  " ; qwerty
          "  aAoOeEuUiIdDhHtTnNsS-_      "   ; asdf
          "  ;:qQjJkKxXbBmMwWvVzZ        "   ; zxcv
          "                              "))
   quail-keyboard-layout-alist)

  (quail-set-keyboard-layout "dvorak"))

However, the Hangul input methods don't respect this variable, so we need to hack about a bit.

Input methods in Emacs

The way Emacs changes the input method is by defining a local variable, input-method-function.

For example, if you change the layout with C-u C-\ korean-hangul RET or (set-input-method 'korean-hangul), the above variable will be set to hangul2-input-method.

Because of this structure, one can simply write a function that takes a single argument (the key sequence) and inserts characters accordingly. Technically, an input method doesn't even need to insert anything; it could just be a bunch of random commands – such is the power of Emacs.

hangul.el

The Hangul library is very qwerty-based. I didn't think this would pose many problems however, as that's what changing the keyboard layout should do, right?

I use the standard "korean-hangul" (AKA Dubeolsik, 두벌식) input method. Others are also defined:

  • Hangul 2-Bulsik input method
  • Hangul 3-Bulsik final input method
  • Hangul 3-Bulsik 390 input method

Side note: Hangul is known for being a very sensible and logical alphabet. The keyboard layout is the same – vowels are on the right, and consonants on the left. Wikipedia

Messing with the internal function

The function hangul2-input-method-internal (or its caller, without -internal) are where the translation seemed to be missing:

;; Defined in lisp/leim/quail/hangul.el
(defun hangul2-input-method-internal (key)
  (let ((char (+ (aref hangul2-keymap (1- (% key 32)))
                 (cond ((or (= key ?O) (= key ?P)) 2)
                       ((or (= key ?E) (= key ?Q) (= key ?R)
                            (= key ?T) (=  key ?W)) 1)
                       (t 0)))))
    (if (< char 31)
        (hangul2-input-method-jaum char)
      (hangul2-input-method-moum char))))

I figured it might work fine if I just lexically rebound KEY to the translation:

(defun hangul2-input-method-internal (key)
  (setq key (quail-keyboard-translate key))
  (let ((char (+ (aref hangul2-keymap (1- (% key 32)))
                 (cond ((or (= key ?O) (= key ?P)) 2)
                       ((or (= key ?E) (= key ?Q) (= key ?R)
                            (= key ?T) (= key ?W)) 1)
                       (t 0)))))
    (if (< char 31)
        (hangul2-input-method-jaum char)
      (hangul2-input-method-moum char))))

Results

Unfortunately, this is janky as hell. There are multiple issues:

  1. Some keys result in "Args out of range: [17 48 26 23 …]".
  2. The translation is only used for some keys.

This is because the key is checked first in hangul2-input-method to determine whether it is alphabetic and should be handed off to the internal version.

hangul2-input-method

So that first one didn't work… What if I used hangul2-input-method instead?

;; Defined in lisp/leim/quail/hangul.el
(defun hangul2-input-method (key)
  "2-Bulsik input method."
  (if (or buffer-read-only (not (alphabetp key)))
      (list key)
    (quail-setup-overlays nil)
    (let ((input-method-function nil)
          (echo-keystrokes 0)
          (help-char nil))
      (setq hangul-queue (make-vector 6 0))
      (hangul2-input-method-internal key)
      (unwind-protect
          (catch 'exit-input-loop
            (while t
              (let* ((seq (read-key-sequence nil))
                     (cmd (lookup-key hangul-im-keymap seq))
                     key)
                (cond ((and (stringp seq)
                            (= 1 (length seq))
                            (setq key (aref seq 0))
                            (alphabetp key))
                       (hangul2-input-method-internal key))
                      ((commandp cmd)
                       (call-interactively cmd))
                      (t
                       (setq unread-command-events
                             (nconc (listify-key-sequence seq)
                                    unread-command-events))
                       (throw 'exit-input-loop nil))))))
        (quail-delete-overlays)))))

I did the same thing as above, just redefining KEY at the beginning with: (setq key (quail-keyboard-translate key))

Yes!! It works!

아이고… There is still a major problem: only the first character is actually translated through quail-input-method.

The problem

You see the second half of that function? A loop that reads input until a complete syllabic block has been entered.

;; From `hangul2-input-method', paraphrased
(let* ((seq (read-key-sequence nil))
       key)
  (cond ((and (stringp seq)
              (= 1 (length seq))
              (setq key (aref seq 0))
              (alphabetp key))
         (hangul2-input-method-internal key))))

Well, I replaced the initial key, but the following keys also need to be translated. This is simple – because it's only passed to the internal function when it's in the latin alphabet, it won't interfere with any keybindings.

Finale

The only changes needed to allow Hangul input with dvorak are:

  • Create and/or enable a dvorak quail-input-method
  • Use quail's key translations in hangul2-input-method
    • For the initial key (argument of the function)
    • For the following keys (in the while loop)

Note: This needs to be redefined after loading quail/hangul, so you might consider wrapping it in with-eval-after-load "quail/hangul" in your init file.

hangul2

(defun hangul2-input-method (key)
  "2-Bulsik input method."
  (setq key (quail-keyboard-translate key))
  (if (or buffer-read-only (not (alphabetp key)))
      (list key)
    (quail-setup-overlays nil)
    (let ((input-method-function nil)
          (echo-keystrokes 0)
          (help-char nil))
      (setq hangul-queue (make-vector 6 0))
      (hangul2-input-method-internal key)
      (unwind-protect
          (catch 'exit-input-loop
            (while t
              (let* ((seq (read-key-sequence nil))
                     (cmd (lookup-key hangul-im-keymap seq))
                     key)
                (cond
                 ((and (stringp seq)
                       (= 1 (length seq))
                       (setq key (quail-keyboard-translate (aref seq 0)))
                       (alphabetp key))
                  (hangul2-input-method-internal key))
                 ((commandp cmd)
                  (call-interactively cmd))
                 (t
                  (setq unread-command-events
                        (nconc (listify-key-sequence seq)
                               unread-command-events))
                  (throw 'exit-input-loop nil))))))
        (quail-delete-overlays)))))

hangul3

(defun hangul3-input-method (key)
  "3-Bulsik final input method."
  (setq key (quail-keyboard-translate key))
  (if (or buffer-read-only (< key 33) (>= key 127))
      (list key)
    (quail-setup-overlays nil)
    (let ((input-method-function nil)
          (echo-keystrokes 0)
          (help-char nil))
      (setq hangul-queue (make-vector 6 0))
      (hangul3-input-method-internal key)
      (unwind-protect
          (catch 'exit-input-loop
            (while t
              (let* ((seq (read-key-sequence nil))
                     (cmd (lookup-key hangul-im-keymap seq))
                     key)
                (cond ((and (stringp seq)
                            (= 1 (length seq))
                            (setq key (quail-keyboard-translate (aref seq 0)))
                            (and (>= key 33) (< key 127)))
                       (hangul3-input-method-internal key))
                      ((commandp cmd)
                       (call-interactively cmd))
                      (t
                       (setq unread-command-events
                             (nconc (listify-key-sequence seq)
                                    unread-command-events))
                       (throw 'exit-input-loop nil))))))
        (quail-delete-overlays)))))

hangul390

(defun hangul390-input-method (key)
  "3-Bulsik 390 input method."
  (setq key (quail-keyboard-translate key))
  (if (or buffer-read-only (< key 33) (>= key 127))
      (list key)
    (quail-setup-overlays nil)
    (let ((input-method-function nil)
          (echo-keystrokes 0)
          (help-char nil))
      (setq hangul-queue (make-vector 6 0))
      (hangul390-input-method-internal key)
      (unwind-protect
          (catch 'exit-input-loop
            (while t
              (let* ((seq (read-key-sequence nil))
                     (cmd (lookup-key hangul-im-keymap seq))
                     key)
                (cond ((and (stringp seq)
                            (= 1 (length seq))
                            (setq key (quail-keyboard-translate (aref seq 0)))
                            (and (>= key 33) (< key 127)))
                       (hangul390-input-method-internal key))
                      ((commandp cmd)
                       (call-interactively cmd))
                      (t
                       (setq unread-command-events
                             (nconc (listify-key-sequence seq)
                                    unread-command-events))
                       (throw 'exit-input-loop nil))))))
        (quail-delete-overlays)))))

Author: Jamie Beardslee

Date: 2020-07-18 (modified 2020-08-25)

Top: The Yeet Log