Emacs 읽기 전용 버퍼에서 모드 키 조합 없이 이동하기
Emacs를 쓰게 되면서 한 가지 안 좋아진 점은, 왼쪽 새끼 손가락 쪽 인대의 피로도가 예전에 비해 꽤나 많이 높아 졌다는 것이다. 보통 이런 증상을 Repetitive strain injury(RSI), 혹은 좀 더 specific하게는 Emacs pinky라고 하던데, Emacs 쓰기 이전 vi나 source insight를 사용하던 때는 싸구려 멤브레인 키보드로도 그닥 장시간 작업 해도 피로감이 없었으나, Emacs를 쓰고 난 이후부터 증상이 슬슬 나타나기 시작했다. 나름대로는 이 증상을 완화시켜보겠다고 Ctrl 키를 Caps Lock 위치로 바꾼다거나 비싼 키보드를 지른다거나 했지만 (물론 이런 노력을 하지 않던 예전에 비해 확실히 덜하긴 하지만) 여전히 퇴근할 때 쯤에는 왼팔꿈치 근처나 새끼 손가락쪽 인대가 묵직하게 저려 오는 느낌이 있다.
그래서 최대한 왼쪽 새끼손가락으로 모드 키, 즉 Ctrl Alt Shift를 누르는 회수를 줄이려는 노력을 계속 하고 있는데, 이것도 그 중의 한 가지 일환이다. 아래의 소스에서 보듯 my:read-only-mode 마이너 모드는, buffer-read-only가 non-nil일 때, 즉 버퍼가 읽기 전용 모드일 때 모드 키 없이 일반 문자로 커서 이동을 할 수 있게 해 준다. 마치 read-only 상태를 vi에서의 modal 상태처럼 다룬다고 보면 될 것 같다.
그러나 특정 메이저/마이너 모드에서는 특정 버퍼를 읽기 전용으로 띄우고 모드 키 없는 바인딩을 자체적으로 추가하기도 하는데, 보통 이 경우는 단순히 커서 이동 뿐만 아니라 이동 된 위치의 아이템에 대한 상세 정보를 다른 윈도우에 띄우기도 한다. 가령 magit에서 n이나 p 키는 라인이 아니라 섹션 단위로 이동하는 역할을 하고 때에 따라선 옮겨진 위치의 정보를 다른 윈도우에 표시하는 등 부가적인 기능을 수행하기도 하는데, 이와 같이 해당 키에 기존에 정의된 바인딩이 있다면 해당 내용을 단순한 커서 이동으로 덮어 쓰지 않도록 해 줄 필요가 있다.
(defconst my:read-only-keybind-alist '(
("p" . previous-line)
("n" . next-line)
("f" . forward-char)
("b" . backward-char)
("a" . beginning-of-line)
("e" . end-of-line)
;; vi-like
("h" . (lambda ()
(interactive)
(if (bolp) (error "Beginning of line")
(backward-char))))
("l" . (lambda ()
(interactive)
(if (eolp) (error "End of line")
(forward-char))))
("j" . next-line)
("k" . previous-line)
;; page scroll
("SPC" . my:scroll-up-command)
("u" . my:scroll-down-command)
) "key binding for read-only buffer")
(defvar my:read-only-overridden-keys nil)
(make-variable-buffer-local 'my:read-only-overridden-keys)
(defface my:read-only-face
'((t :foreground "yellow" :background "red"))
"Used for readonly lighter"
:group 'basic-faces)
(defvar my:read-only-mode-lighter
(list " " (propertize "Readonly" 'face `my:read-only-face)))
(put 'my:read-only-mode-lighter 'risky-local-variable t)
(define-minor-mode my:read-only-mode
"Control-key-less keybind for read-only buffer."
:lighter my:read-only-mode-lighter
:variable buffer-read-only
(let ((overwritten-keys my:read-only-overridden-keys))
(if buffer-read-only
(progn (dolist (keybind my:read-only-keybind-alist)
(unless (and (current-local-map)
(lookup-key (current-local-map) (kbd (car keybind))))
(add-to-list 'overwritten-keys (car keybind))
(buffer-local-set-key (kbd (car keybind)) (cdr keybind))))
(setq my:read-only-overridden-keys overwritten-keys))
(dolist (key overwritten-keys)
(buffer-local-set-key (kbd key) nil))
(setq my:read-only-overridden-keys nil)))
(read-only-mode (if buffer-read-only 1 -1)))
buffer-local-set-key 함수는 EmacsWiki에서 참고한 것으로, Emacs 자체(혹은 최소한 GNU Emacs)에서 버퍼별로 keymap을 할당할 수 있는 방법이 없기 때문에 해당 버퍼 전용 minor mode를 만들어서 거기에다 키를 바인딩한다.
;; https://www.emacswiki.org/emacs/BufferLocalKeys
(defun buffer-local-set-key (key func)
(interactive "KSet key on this buffer: \naCommand: ")
(let ((name (format "%s-magic" (buffer-name))))
(eval
`(define-minor-mode ,(intern name)
"Automagically built minor mode to define buffer-local keys."))
(let* ((mapname (format "%s-map" name))
(map (intern mapname)))
(unless (boundp (intern mapname))
(set map (make-sparse-keymap)))
(eval
`(define-key ,map ,key func)))
(funcall (intern name) t)))
위와 같이 정의했다면 my:read-only-mode을 원래 read-only-mode의 키인 C-x C-q에 지정하자. 내 경우는 왼쪽 모드키를 덜 누르는 게 지상과제이니 만큼 <f12>에도 바인딩해 주었다.
(global-set-key (kbd "C-x C-q") 'my:read-only-mode) ; override default key (global-set-key (kbd "<f12>") 'my:read-only-mode)
이제 C-x C-q이나 <f12>을 눌러 버퍼를 읽기 전용으로 세팅하면 모드 키를 누르지 않고 이동을 할 수 있게 되지만, 기본적으로 읽기 전용 모드인 버퍼, 특히 *Help* 버퍼처럼 모드 키 없이 커서 이동이 가능한 바인딩이 되어 있지 않은 버퍼들에 대해서도 읽다보면 라인이나 페이지 이동을 자주 쓸 일이 있으므로 위 키 바인딩을 적용할 필요가 있다.
;; add my:read-only-mode when the buffer-read-only is set
(add-hook 'buffer-list-update-hook
(lambda ()
(with-current-buffer (current-buffer)
(when (and buffer-read-only
(not (member 'my:read-only-mode minor-mode-list)))
(my:read-only-mode 1)))))
buffer-list-update-hook에 hook을 추가하여 버퍼가 읽기 전용일 때, 즉 buffer-read-only 버퍼 로컬 변수가 non-nil일 때, my:read-only-mode가 적용이 되어 있지 않다면 적용하도록 해 주었다. buffer-list-update-hook은 GNU Emacs Lisp Manual에서 아래와 같이 설명하듯,
This is a normal hook run whenever the buffer list changes. Functions (implicitly) running this hook are버퍼 리스트의 순서나 element가 추가/삭제 될 경우 호출 되므로 이 시점에 my:read-only-mode를 적용하도록 하는 것이 가장 이상적으로 보였다.get-buffer-create(see Creating Buffers),rename-buffer(see Buffer Names),kill-buffer(see Killing Buffers),bury-buffer(see above) andselect-window(see Selecting Windows).
그러나 이렇게 적용하고 난 후 magit-popup-mode에서는 키가 바인딩 되는 시점의 문제인지 오히려 my:read-only-mode 의 키 바인딩 때문에 기존 magit popup의 키가 제대로 적용되지 않는 문제가 생겼다. 대충 봐선 타이밍 이슈로 보이긴 하는 데 더 이상 깊게 들여다보고 싶지도 않고, 사실 magit popup에서는 커서 이동이 거의 필요가 없으므로, (차후에 또 어떤 메이저 모드에서 말썽일지 모르므로 준비 차원에서) my:read-only-mode를 적용하지 않을 blacklist를 작성하는 식으로 처리했다. 아래는 위의 hook을 수정한 결과이다.
(defconst my:read-only-mode-blacklist '(
magit-popup-mode
) "The blacklist which can not run well with my:read-only-mode.")
;; add my:read-only-mode when the buffer-read-only is set
(add-hook 'buffer-list-update-hook
(lambda ()
(with-current-buffer (current-buffer)
(when (and buffer-read-only
(not (member major-mode my:read-only-mode-blacklist))
(not (member 'my:read-only-mode minor-mode-list)))
(my:read-only-mode 1)))))
코드를 작성할 때야 어쩔 수 없어도, 읽을 때는 읽기 전용 모드 켜 놓고 모드 키 없이 이동하니 한결 손이 편해지는 느낌이다. 사실 일하면서 코드 작성하는 것 보다 읽는 시간이 훨씬 많은 것 같은 데 그 때마다 라인이나 페이지 이동할 때마다 모드 키 입력이 필요하던 걸 이런 식으로 줄여 놓으니 확실히 차이가 보이는 듯. 사실 누가 보면, 이럴거면 viper-mode나 evil-mode 쓰지 왜 그러냐 하겠지만 기본적인 이동 키 외의 vi 키는 고작 1년 안썼다고 익숙치가 않기도 하고 다시 vi 바인딩으로 돌아가는 건 초가 삼간 태우러 가는 일을 하게 될 것 같은 기분이 들기도 해서 그다지 끌리지 않는다.
'Emacs' 카테고리의 다른 글
| ido에서 helm으로 넘어오다 (0) | 2016.05.17 |
|---|---|
| Emacs에서 End of Line 타입 변경을 간편하게! (0) | 2016.05.11 |
| Emacs의 자동 저장 및 복구 기능을 좀 더 잘 활용하자 (0) | 2016.04.04 |
| Emacs의 버퍼 변경 알림 프롬프트에서 변경 내용 표시하기 (0) | 2016.04.01 |
| Emacs spell checker (0) | 2016.03.11 |