이 노트에 대하여

터미널에서도 GUI를 포기하지 않아도 되는 구성이 실제로 완성되었다는 선언이다. 한글 입력 5레이어 체인, OSC 52 클립보드, tmux truecolor, 독립 인스턴스와 세션 통신까지 삽질의 결론만 남겼다. 터미널 Emacs가 에이전트 협업의 보편 인터페이스가 되는 순간이다.

히스토리

  • [2026-05-07 Thu 10:39] @junghan — ghostel 합류 — Emacs 안의 Ghostty (2026-05-07)
  • [2026-04-23 Thu 18:26] @junghan — WezTerm TTY minibuffer 공백 어긋남 원인을 한글 입력이 아니라 Consult prompt/path truncation의 hardcoded Unicode ellipsis(…)로 확인. tty-config.el에서 TTY 전용 advice로 ASCII … 치환.
  • [2026-04-19 Sun 19:38] @junghan — 성능 최적화 완료
  • [2026-04-17 Fri 17:30] @opus — 컬러 이모지 제거 작업 기록. 3레이어(NixOS fontconfig + WezTerm 내장 우회 + Emacs 방어층) 통합. VS-16 폭 미스매치는 별도 이슈로 남김.
  • [2026-04-17 Fri 16:11] @junghan#터미널 #이맥스: ¤Wezterm ¤Ghostty ¤kitty 이거 기억하자.
  • [2026-04-14 Tue 22:50] @gpt — 하네스 프론트엔드 관점으로 How to Read와 관련노트 보강. 이 글이 Emacs 일반 소개가 아니라, 거대한 라이브 그릇이 터미널 창 안에 들어와 에이전트 협업 인터페이스가 된 사건이라는 점을 더 분명히 남김.
  • [2026-04-13 Mon 14:34] @pi(분신) — 리뷰 완료. 퍼블리시 허가! 🫡 How to Read 섹션 추가, clipetty→xterm 전환 배경 보강
  • [2026-04-13 Mon 14:06] @pi — clipetty→xterm 내장 OSC 52 전환, WezTerm SSH OSC 52 제한사항 문서화, DECSCUSR 커서 추가
  • [2026-04-13 Mon 11:52] @junghan — 완성. 이전 로그 문서는 삭제한다.
  • [2026-04-13 Mon] @pi — 생성. 20251224T144906(llmlog)과 20260410T203723(llmlog)의 핵심을 합침. 2주간 삽질의 결론: 터미널 Emacs가 에이전트 하네스의 프론트엔드로 완성됨.

How to Read

이 글은 Emacs 입문서가 아니다. 거대한 라이브 그릇이 터미널 창 안에 들어왔고, 그 결과 인간과 에이전트가 같은 작업면에서 만날 수 있게 되었다는 기록이다.

  • 누구를 위한 글인가: 에이전트 협업의 프론트엔드를 GUI 앱이 아니라 터미널이라는 보편 인터페이스 위에 올리고 싶은 사람.
  • 무엇을 다루는가: 한글 입력 5레이어 체인, OSC 52 클립보드, 24bit truecolor, SSH 원격, 독립 인스턴스, 세션 간 통신.
  • 무엇을 일부러 다루지 않는가: Emacs의 수천 개 패키지, 키바인딩 문화, Org-mode 일반론. 여기서 중요한 것은 세부 기능 목록이 아니라, 그 거대한 그릇이 터미널 안에 담겼다 는 사실이다.
  • 어떻게 읽는가: 먼저 “이 문서의 존재이유”와 “해결된 것” 표를 본 뒤, 관심 있는 레이어만 상세 섹션으로 내려가라.
  • 왜 중요한가: 로컬이든 SSH 원격이든, 이 터미널 창 하나가 인간과 에이전트가 함께 만지고 스스로 eval하며 라이브로 개선할 수 있는 협업 인터페이스가 되기 때문이다.
  • 비유를 허용한다면: 냉장고에 코끼리를 넣은 것 같고, 보아뱀이 코끼리를 삼킨 그림을 터미널 안에 접어 넣은 것 같다. 핵심은 “작은 창”이 아니라, 그 안에 들어온 거대한 생태계 다.
  • 전제 조건: WezTerm + tmux + Doom Emacs + NixOS. 다른 조합이라면 세부 구현은 달라도, 각 레이어의 역할과 접합 순서를 읽어야 한다.

이 문서의 존재이유

터미널 이맥스 하네스 프론트엔드 완성.

모든 삽질은 이걸 끌어내려고 했다. 터미널 Emacs에서 GUI와 동등한 환경 — 한글 입력, 클립보드, truecolor, SSH 원격, 독립 인스턴스 — 이 모두 동작하여 에이전트 협업의 보편 인터페이스가 된 시점의 기록.

해결된 것 (2026-04-12/13)

문제해결파일
한글 입력 (터미널)term-keys + wezterm + fcitx5 기본그룹tty-config.el, korean-input-config.el
클립보드 (로컬)clipetty→xterm 내장 OSC 52 (send-string-to-terminal)tty-config.el
24bit truecolor (tmux)e() wrapper TERM=tmux-directnixos shell.nix
독립 인스턴스 (emacs -nw)init.el 가드 daemon-only로 변경init.el
pi —session-controlextra-args :init setq (defer 타이밍)ai-pi-agent.el
세션 매니저 void 에러—last-usage → —state :stats :contextUsageai-pi-agent.el
컬러 이모지 (터미널 fixed-pitch 깨짐)NixOS fontconfig 흑백 기본 + WezTerm 내장 우회 + Emacs doom-emoji-font 핀shared.nix, wezterm.lua, +user-info.el, ai-bot-config.el, korean-input-config.el

한글 입력: 5레이어 체인

물리 Right Alt

① xkb (kr104): Alt_R keysym (fcitx5 기본그룹에서 통과)

② wezterm: RightAlt → term-keys 바이트 시퀀스 \x1b\x1f\x50\x60\x1f

③ SSH / tmux: 바이트 그대로 통과 (중간자가 가로챌 이유 없음)

④ Emacs term-keys: input-decode-map → <Hangul>

⑤ toggle-input-method → korean-hangul (Emacs 내장 입력기)

핵심: OS 입력기를 안 쓴다. Emacs 내장 입력기만 쓴다. NFD 문제 자체가 발생하지 않는다.

왜 이렇게 삽질했나

kime가 X11 레벨에서 Hangul/S-Space를 Consume → KKP 도달 불가. fcitx5 복원 → TriggerKeys가 전역 Consume → 또 막힘. fcitx5 기본그룹(English)으로 고정 → Alt_R가 통과 → term-keys가 잡음. wezterm이 RightAlt를 인식 → 시퀀스 전송 → Emacs가 <Hangul>로 디코딩.

패턴: “왜 안 되는가”를 xev → 터미널 디버그 → C-h k 순으로 한 레이어씩 추적. 각 레이어의 “소비자”를 찾아서 제거.

undo-fu 충돌

term-keys 프리픽스 \x1b\x1f = ESC + C-_ = C-M-_. undo-fu-mode-map이 C-M-_ 바인딩 → 프리픽스 가로챔. 해결: undo-fu-mode-hook 에서 매번 unbind.

클립보드: OSC 52 전구간

Emacs kill → clipetty → OSC 52 → tmux allow-passthrough → SSH → WezTerm → 시스템 클립보드
시스템 클립보드 → Ctrl-Shift-V → WezTerm 터미널 페이스트 → Emacs insert
  • WezTerm: OSC 52 기본 지원 (설정 불필요)
  • tmux: set -g allow-passthrough on 이 MVP — 없으면 중첩 터미널에서 차단
  • Doom: (tty) + (set-terminal-parameter nil 'xterm--set-selection t) 한 줄
  • clipetty 제거 이유: clipetty는 process-connection-typenil 로 설정한 뒤 write-region 으로 임시 파일을 거쳐 OSC 52를 보낸다. 이 과정에서 terminal-name/dev/tty 일 때 시퀀스가 깨지는 문제가 있었다. Emacs 29+ 내장 xterm.el은 send-string-to-terminal 로 직접 전송하므로 이 문제가 원천 차단된다. 외부 패키지 의존 하나를 줄이고, 빌트인으로 통일.
  • xclip은 유지 (paste 방향 커버 — OSC 52는 copy 방향만 담당)
  • ref: [emacs terminal-name /dev/tty 문제 조사]

WezTerm SSH OSC 52 제한사항

WezTerm은 SSH를 통해 들어오는 OSC 52를 처리하지 못함 (known issue: wezterm/wezterm#764, #1790). Ghostty에서는 동일 경로가 정상 동작.

시나리오결과
로컬 → OSC 52
SSH Oracle → printf OSC 52

WezTerm TTY minibuffer 공백 어긋남 — Consult ellipsis

증상은 +default/search-cwd / consult-ripgrep 프롬프트에서 앞쪽에 입력하거나 백스페이스를 칠 때 공백이 어긋나 보이는 것이었다. 처음엔 한글 입력 문제처럼 보였지만, 실제 원인은 입력기가 아니라 Consult가 길어진 경로를 줄이면서 넣는 hardcoded Unicode ellipsis (U+2026) 였다.

예: Search (…/agent-config/pi-extensions)!

여기서 뒤의 ! / : 는 consult async indicator 상태 갱신이고, WezTerm TTY에서 앞의 가 폭 드리프트를 일으키면 indicator overlay가 갱신될 때 공백이 남거나 커서 위치가 틀어진 것처럼 보인다. 즉 한글 입력이 문제를 만든 것이 아니라, TTY 문자폭 드리프트를 consult redraw가 드러낸 것 이다.

핵심 포인트:

  • truncate-string-ellipsis 만 바꿔서는 해결되지 않음
  • consult 내부 함수가 를 직접 하드코딩하고 있었음
  • 문제 경로: consult--left-truncate-file, consult--directory-prompt
  • 해결: tty-config.el 에서 TTY일 때만 advice로 ... 치환

이 경로는 WezTerm + terminal Emacs + built-in Korean input 이 결합된 커스텀 경로다. 나중에 minibuffer/search prompt spacing이 다시 깨지면, 한글 입력기부터 의심하지 말고 먼저 Consult prompt/path truncation의 ellipsis와 TTY width drift 를 보라.

SSH Oracle → tmux copy-mode yank
SSH Oracle → tmux → WezTerm copy mode
Ghostty → SSH Oracle → tmux yank

대응: SSH+tmux에서는 WezTerm 자체 copy mode 사용.

truecolor: TERM=*-direct

tmux → TERM=tmux-256color → Emacs init_tty C레벨 → 256색 고정 → 테마 깨짐
해결: e() wrapper → tput colors < 257 → TERM=tmux-direct → colors#16777216 → 24bit OK

Emacs 터미널 색상은 C 레벨 init_tty 에서 결정. Lisp로 사후 변경 불가. emacsclient 호출 시점에 TERM을 바꾸는 것이 유일한 방법.

독립 인스턴스: daemon-only 가드

init.el:
  (when (and (daemonp) (server-running-p))  ; ← 비-daemon은 통과
    (kill-emacs))

doom run (GUI) → non-daemon → 가드 스킵 → 독립 실행. emacs -nw → non-daemon → 가드 스킵 → 서버 없는 독립 인스턴스.

메모리: TTY Emacs + pi RPC = ~530MB. 5개 = ~3.5GB. 27GB 시스템에서 여유.

에이전트 통합

Emacs에서 pi 시작 시 --session-control 플래그 포함 → send_to_session, list_sessions 사용 가능. :custom 은 defer 상태에서 적용 안 됨 → :initsetq 로 해결.

스크롤: pi-coding-agent--with-scroll-preservation 매크로가 자동 팔로잉. point가 버퍼 끝이면 따라가고, 위로 스크롤했으면 유지.

이모지: 흑백 통일 (2026-04-17)

터미널 fixed-pitch에 컬러 이모지가 섞이면 줄이 깨진다. Emacs에서 doom-emoji-fontNoto Emoji 로 핀했는데도 커서 hover에서 Noto Color Emoji 로 표시되는 현상. 원인이 한 곳이 아니라 3레이어에 동시 존재 해서, 한 군데만 막아서는 못 끈다.

3레이어 진단

  1. NixOS: fonts.packagesnoto-fonts-color-emoji 가 포함되고, fontconfig.defaultFonts.emoji 가 컬러로 잡혀 있었다. 시스템 차원에서 이미 컬러.
  2. WezTerm 내장: WezTerm이 Noto Color Emoji번들 폰트 로 들고 있다. 시스템에서 지워도 WezTerm 자체가 fallback으로 쏜다.
  3. Emacs 내부: Doom 기본 doom-emoji-fallback-font-families 는 컬러가 먼저. cl-find-if 가 컬러를 먼저 잡아 emoji fontset에 박히면, 뒤에 prepend로 흑백을 얹어도 VS-16 결합 이모지(✔️, ❤️)는 컬러로 폴백된다. telega의 telega-emoji-font-family defcustom 도 초기화 시 (font-family-list) 로 컬러를 적극 선택해 시스템 fontconfig를 캐싱시킨다.

3레이어 해결

레이어해결파일
NixOSnoto-fonts-color-emoji 제거, enableDefaultPackages = false, defaultFonts.emoji = [ "Noto Emoji" ]machines/shared.nix
WezTerm내장 폰트 fallback에 assume_emoji_presentation = true + Symbola 최종 fallbackusers/junghan/configs/wezterm.lua
Emacsdoom-emoji-fontNoto Emoji 로 pre-bind, telega emoji 변수 :init 에서 pre-bind, GUI에서 emoji fontset 전부 clear 후 흑백만 재등록+user-info.el, ai-bot-config.el, korean-input-config.el

핵심 교훈: 정말로 해결하려면 “제일 바깥에서 안 쓰게 만든다”. 시스템에서 빼고, 터미널에서 우회하고, 그래도 남은 가능성을 Emacs에서 방어. 한 층만 막으면 새어나온다.

남은 이슈: VS-16 폭 미스매치

wezterm ls-fonts --text "✔️"
  U+2714 U+FE0F  cells=0   # WezTerm: 결합해서 0셀로 처리
Emacs char-width-table       : 2   # Emacs: 2셀로 계산

✔️ (U+2714 + U+FE0F) 같은 VS-16 결합 이모지에서 WezTerm과 Emacs의 셀 폭 계산이 어긋난다. telega chat, org-mode 줄이 깨지는 원인.

후보 해결: WezTerm unicode_version = 14 (기본 9). 검증 필요. ref: https://wezterm.org/config/fonts.html#font-related-options

서버 소켓 정리

소켓인스턴스용도
userEmacs 30.2 GUI힣의 메인 에디터 (doom run)
serverEmacs 30.2 headless에이전트 데몬 (run.sh agent start)
doom-igcEmacs 31 IGCMPS GC 실험 (bin/emacs-igc.sh)
(없음)emacs -nw 독립터미널 pi 세션들

관련 커밋

리포SHA내용
doomemacs-configede202cterm-keys 항상 로드, NFC타이머 제거, undo-fu hook
doomemacs-config604a243tty-config 통합 (term-keys, kitty-graphics, clipboard)
doomemacs-config03fed55clipetty→xterm OSC 52 + DECSCUSR 커서 + keybindings
doomemacs-configce48767문서 재작성 + 터미널 독립 실행 + IGC TTY
doomemacs-config0cabb89pi —session-control :init 타이밍 수정
term-keys5ea6bfawezterm col: Hangul→RightAlt
nixos-confige8eb520emacsclient 24bit truecolor wrapper
nixos-config30acff6시스템 이모지 기본값 흑백 전환
nixos-config5a69c3d컬러 이모지 시스템/WezTerm에서 완전 배제
doomemacs-config8208441doom-emoji-font Noto Emoji 핀 (컬러 폴백 차단)
doomemacs-config2e03f4etelega emoji pre-bind + GUI fontset clear

관련노트

관련메타

BIBLIOGRAPHY

성능 경량화 — 타이머/훅 정리 (2026-04-19)

배경 — 프로파일이 말한 것

gg 로 대용량 저널 맨 위 이동 시 체감 지연. M-x profiler-report:

  • 57% Automatic GC
  • 13% timer-event-handler
  • 6% xterm-set-window-title
  • 1시간짜리 반복 타이머: org-persist--refresh-gc-lock, url-cookie-write-file, celestial-mode-line--update-handler, doom-modeline—github-fetch-notifications(30m)

TTY 에이전트 프론트엔드에 불필요하거나 비용 대비 이득 없는 것들을 걷어냈다.

제거한 것들

smooth-scroll 모듈

Doom ui/smooth-scroll +interpolateultra-scroll 은 GUI pixel-scroll 전용(pixel-scroll-precision-mode 전제), good-scrollscroll-up/down 에 advice + good-scroll--render 타이머. TTY 라인 렌더에서 픽셀 보간은 의미 없음. 통째 제거. GUI 에서 ultra-scroll 이 절실해지면 그때 다시.

kitty-graphics

kitty-graphics-mode 가 활성화되는 순간:

  • post-command-hook, window-scroll-functions, window-size-change-functions, window-buffer-change-functions 에 훅 추가
  • org-display-inline-images / org-link-preview / image-mode / doc-view-* / shr-put-image / markdown-overlays--fontify-image 등 11개 advice 설치

이미지가 한 번도 안 뜬 버퍼에서도 매 커맨드마다 호출이 돈다. gg 처럼 명령이 빠르게 반복되는 상황에서 누적된다. 에이전트 프론트엔드는 텍스트 중심이라 비용 대비 이득 없음 → tty-config 에서 제거. 필요 시 M-x kitty-graphics-mode 토글.

xterm-set-window-title

Emacs → 터미널 OSC 0/2 송출로 프레임 타이틀 동기화. tmux/WezTerm 이 이미 탭/윈도우 타이틀을 관리하므로 중복. (setq-default xterm-set-window-title nil) 로 6% 회수.

doom-modeline-github

30분 주기 GitHub 알림 fetch 타이머 + 네트워크 IO. 알림은 magit/ghcli 로 본다. (setq doom-modeline-github nil).

diff-hl-flydiff

after-change-functions 에 1초 debounced 타이머. diff-hl 자체는 유지하고 flydiff 만 off — save / vc-refresh 시 gutter 갱신은 여전히 동작.

auto-revert-check-vc-info

auto-revert-buffers 가 revert 시마다 git status 호출해 modeline VC 상태 갱신. 프로파일 auto-revert-buffers 6s 누적의 주원인. magit/diff-hl 이 대신하므로 off.

hook 타이밍 재정렬 — bonus

기존 tty-config.el 은 top-level 에서 (unless (display-graphic-p) ...) 여러 블록:

  • daemon 로드 시점엔 frame 이 없어 display-graphic-p 가 항상 t → 조건 통과 실패
  • non-daemon 에서도 Doom 이 tty-setup-hook / doom-first-buffer-hook 에서 xterm-mouse-mode, show-paren-mode나중에 켜서 우리가 끈 걸 덮어씀

해결: 모든 TTY 설정을 +tty-setup 단일 함수로 모아 Doom 의 같은 hook 에 :append 로 붙여 뒤에 돈다. OSC 52, display-table vertical-border, VS-16 FE0F 숨김도 같은 경로로 옮겨 daemon + GUI/TTY 혼합 frame 에서 정상.

구조 정리 — config.el 은 loader

config.el 에 섞여있던 better-default 뭉치(completion-at-point-functions, inhibit-compacting-font-caches, vc-follow-symlinks, auto-revert, kill-ring-max, create-lockfiles, xref-search-program) 를 lisp/defaults-config.el 로 추출. config.el 은 module loader 지향으로 정돈.

커밋

  • c52ad7b refactor: tty-config — Doom hook 이후로 TTY 설정 지연
  • f976d7d perf: TTY-first 경량화 + better defaults 모듈 분리

다음 라운드 후보 (today 는 여기까지)

  • celestial-mode-line — 달/해 표시 실사용 여부 확인 후 제거
  • display-time-event-handler — 모드라인 시계 주기 (현재 1m)
  • org-persist--refresh-gc-lock — org-persist 의존 기능 쓰는지 확인
  • url-cookie-write-file — eww 미사용이면 타이머 취소
  • IGC (MPS) 전환 — GC 57% 의 근본 처방

lockfiles 질문에 대한 결론

외부 에이전트가 Emacs 의 .#filename 규약을 따르지 않으므로 create-lockfiles 는 인간↔에이전트 충돌 보호에 무력. 진짜 안전장치는 이미 Emacs 내장:

  • basic-save-bufferverify-visited-file-modtime 체크 (y-or-n-p 경고)
  • inotify + global-auto-revert-mode 로 외부 write 시 즉시 revert (unsaved 아니면)

따라서 create-lockfiles nil 유지.

ghostel 합류 — Emacs 안의 Ghostty (2026-05-07)

vterm 옆에 ghostel 을 실험적 옵션으로 붙였다. ghostel은 libghostty 기반 — 즉 Mitchell Hashimoto의 Ghostty 터미널 코어를 Emacs 안으로 임베딩한 것이다. src/Zig 로 작성됐고, Kitty graphics, OSC 8 hyperlink, OSC 133 prompt navigation 같은 modern terminal 기능을 기본 으로 들고 있다.

vterm은 안정적이지만 정체된 libvterm에 묶여 있다. ghostel은 터미널 에뮬레이터의 미래(Ghostty) 가 Emacs 안에서 돌아가는 첫 순간 이다. 에이전트 하네스 프론트엔드 입장에서 vterm의 정착지가 될 수 있는 후보이므로, 지금부터 길들인다. Doom 기본은 vterm을 baseline으로 두고, ghostel은 M-x ghostel 로 호출.

한글 입력 — 우리가 직접 고쳤다

ghostel 키맵은 [remap self-insert-command] 로 영문 입력을 잡는데, hangul-input-method(self-insert-command 1)함수로 직접 호출 해서 이 remap을 우회한다. 한글이 buffer에는 들어가지만 PTY로는 안 가서, 다음 redraw에 글자가 사라진다 — RET, SPC, Backspace에서 모두.

vterm/eat 등 PTY-mirror 계열 터미널 패키지 공통의 사각지대 였다. vterm은 input-method-function 이 이벤트 리스트를 반환한다는 전제로 돌아가는데, hangul은 buffer를 직접 만지고 nil을 반환한다.

해결: input-method-function 을 ghostel buffer 한정으로 wrap. 원래 IME 호출 전후(point) 위치를 비교해, IME가 buffer에 commit한 텍스트를 잡아 buffer에서 지우고 PTY로 UTF-8 전송. shell echo가 정상 redraw 경로로 다시 채운다. 이벤트를 반환하는 일반 quail 패키지는 point가 안 움직이므로 그대로 통과. +72 lines, 기존 코드 무수정.

junghan0611/ghostel fix/korean-ime-commit 에 푸시. packages.el 은 그동안 fork branch를 가리킴. 2주 실사용 후 upstream PR 예정 — 단순 코드 제안이 아니라 검증 히스토리가 붙은 fix로 가져가려는 것.

상세 분석/디자인은 별도 llmlog: [ghostel 한글 IME commit 경로 fix 디자인]

pi 데몬 개방

config.el 의 터미널 모듈 게이트를 풀었다.

BeforeAfter
(equal server-name "user")(member server-name '("user" "pi"))

agent-server/server 같은 가벼운 보조 데몬은 그대로 제외. pi(분신)와 user GUI에서는 ghostel/vterm 모두 사용 가능.

관련 커밋

리포SHA내용
junghan0611/ghostela3a0511Forward IME-committed text to PTY for Emacs input methods
doomemacs-configad39610feat(term): add ghostel with Korean IME fix, allow user/pi daemons