-
Notifications
You must be signed in to change notification settings - Fork 0
Development History
이 문서는 pytmux 가 어떻게 만들어졌는지를 시대(Era)별 서사로 정리한 것이다. 간략한 요약은 Project-Overview 에 있고, 여기서는 각 시대에 무엇을 만들었고, 어떤 문제를 풀었으며, 어떤 설계 전환·교훈이 있었는지를 더 자세히 다룬다.
프로젝트는 약 2주 사이에 빠르게 진화했다. 정확한 날짜보다 시대의 순서가 중요하다.
프로젝트는 한 장의 설계 문서에서 출발했다. 사용자 요구사항은 여섯 가지였다.
| # | 요구사항 | 충족 방식 |
|---|---|---|
| R1 | 시작하면 일반 터미널과 동일하게 보일 것 | 시작 시 단일 전체화면 패널에 기본 셸 |
| R2 | 앱·상위 터미널을 닫아도 세션 유지 | 셸 PTY 를 보유하는 백그라운드 데몬 + 렌더 클라이언트 분리 |
| R3 | 하단 상태표시줄 1줄 |
StatusBar 위젯(세션·탭·시각) |
| R4 | 패널 추가/삭제/이동/리사이즈를 TUI 메뉴로 | prefix 키 + 명령 팔레트 + 팝업 메뉴 |
| R5 | 마우스로 리사이즈·삭제·추가 | 분할선 드래그, 닫기 버튼, 우클릭 컨텍스트 메뉴 |
| R6 | 패널별 스크롤백(copy-mode 진입 없이) | 패널마다 항상 활성 독립 스크롤백, 휠/키 즉시 |
전 아키텍처를 결정한 한 줄은 R2였다. Textual 앱은 "현재 터미널"에 붙은 프론트엔드라 창을 닫으면 SIGHUP 으로 죽는다. 셸이 같은 프로세스그룹에 묶이면 같이 죽으므로, tmux 와 동일하게 **셸을 보유하는 데몬을 컨트롤링 터미널에서 분리(setsid + 이중 포크)**한다.
"앱을 닫아도 세션 유지" = 셸 PTY 수명을 클라이언트 수명에서 분리 = 서버(데몬)가 PTY 를 소유
핵심 데이터 트리(현재까지 유지): Server → Session(항상 1개) → Tab → Window → PaneNode(이진 분할 트리). 패널 ID 는 tmux 처럼 %0,%1… 단조 증가. 서버가 레이아웃 권위자이고 각
패널의 절대 좌표(x,y,w,h)를 보내면 클라가 그대로 배치한다(단일 진실 소스 보장). 자세한
배경은 Single-Session-Model 참고.
비목표로 미뤘던 것들(Windows·SSH 원격 attach·검색/복사)이 나중에 전부 들어왔다는 점이 이 프로젝트의 성격을 말해준다.
거의 모든 tmux 기본기가 첫날에 들어왔다. 약 3,500줄 단일 pytmux.py 로 클라/서버 구조와
핵심 기능(줌·윈도우 rename/kill·명령 프롬프트·named session·config·스크롤백·마우스·메뉴)을
가지고 시작한 뒤, 기능 카테고리를 변경 하나당 기능 하나 단위로 돌파했다.
- 패널: display-panes, 레이아웃 프리셋(even-h/v·main-v/h·tiled), swap/rotate, break-pane, join-pane, last-pane, synchronize-panes + SYNC 배지, 패널 제목, border-status.
- 윈도우: last-window, move/swap-window, choose-tree, 포그라운드 프로세스 자동 rename, monitor-activity/bell.
- 세션: kill-session, 복수 클라 미러링 + 공유 최소크기 레이아웃.
-
copy/스크롤백: 검색
/ n N, 마우스 선택, 클립보드(pbcopy/xclip/wl-copy), 페이스트 버퍼, clear-history, vi/emacs mode-keys. - 설정·상태바·자동화: source-file, set/show-options, hooks, format 토큰, clock-mode, display-popup, send-keys, capture-pane, pipe-pane, 외부 CLI 제어, 레이아웃 저장/복원, 중첩-tmux prefix passthrough.
첫 대형 리팩터: 단일 파일 → pytmuxlib 패키지(protocol/keymap/model/server/client/
launcher)로 분해, pytmux.py 는 얇은 진입점으로.
첫날의 두 교훈적 사건:
- 명령 프롬프트 포커스 사가 — Input 위젯이 포커스를 못 잡는 버그를 여러 방식으로 풀고 되돌리길 반복하다 모달 프롬프트 화면으로 해결. "헤드리스 테스트는 타이밍이 달라 포커스 의존 UI 를 헤드리스만으로 판단하면 안 된다" 는 항구적 교훈을 남겼다.
-
대체 화면 버퍼 지원 — VT 파서가
ESC[?1049h/l를 지원하지 않아 풀스크린 TUI 가 깨졌다 → Pane 레벨에서 직접 메인/대체 화면을 분리 처리. 한글 IME 의 비-ASCII Ctrl 크래시도 이때 고쳤다.
- CJK/이모지 와이드문자 렌더 픽스 — 박스 그림이 깨지던 문제.
-
계층 재구성:
Session → Tab → Window → 패널로 재편(탭 도입). 멀티세션을 사용자 표면에서 제거하고 단일 세션 + 최상위 탭으로 단순화. 상단 탭바(가로 스크롤), 색 통일, 탭별 레이아웃 저장. - Claude Code 통합 탄생: 탭 아이콘 + 상태바에 best-effort 토큰/컨텍스트 사용량. busy/idle 상태를 출력 정규식으로 탐지(이후 Claude UI 문자열 변화에 맞춰 반복 강화). 분석용 출력 캡처 기능도 이때 도입했다.
상세한 모바일 중심 UX 요청이 동력이 된 시기다. 우클릭 통합 컨텍스트 메뉴, 비대상 패널 디밍, on/off 표시 토글, ESC 모드 탭바 네비게이션, 종료 확인 분기, 로컬 중첩 pytmux 거부, 테두리 hover 등이 들어왔다. Claude UX 쪽으로는 세션 토큰 누계 영속, 프롬프트 히스토리 팝업, 토큰 사용량 트리 팝업, 백그라운드 Claude 탭 완료 알림이 추가됐다.
WSL/Cygwin 없이 네이티브 Windows 는 fcntl/pty/os.fork/AF_UNIX 에 막혀 부분
재작성이 필요했다. 4개 서브시스템을 추상층 뒤로 숨겼다.
-
PTY: ConPTY(
pywinpty), 후일 직접 소유(owned) 백엔드도 추가. →pty_backend.py/conpty.py -
이벤트 루프(최난관): Proactor 가 ConPTY 파이프에
add_reader불가 → TCP 루프백 + 패널별 reader 스레드(블로킹 read →call_soon_threadsafe). -
프로세스/시그널:
DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW+pythonw.exe(데몬 콘솔창이 떠서 닫으면 서버가 죽던 문제 후 추가). - IPC: TCP 루프백 + 포트파일(Win10 의 AF_UNIX asyncio 미흡 우회).
실 Windows 11 박스 검증 완료: 클린 설치, 헤드리스 전체 통과, 라이브 attach. 여기서
"macOS/Windows 검증 = 로컬 박스가 권위" 라는 항구적 원칙이 섰다. CI 가 즉시 실 Windows-only
버그(cp1252 한글 stdout UnicodeEncodeError)를 잡았다.
개발 중 서버 코드를 갱신하려면 재시작해야 하는데 그러면 모든 패널의 셸/프로그램이 죽는다.
os.execv 인플레이스 re-exec(대안인 SCM_RIGHTS fd 핸드오프는 기각)를 택했다. execv 는
PID 를 보존해 자식이 그대로 서버 자식으로 남고(waitpid/SIGCHLD 유효), 상속한 master fd 가
PTY 를 살린다. CLOEXEC 불변식을 정교하게 다뤘다(넘길 fd 만, execv 직전에만 해제, adopt 직후
재설정). 스크롤백은 평문 스냅샷 + SIGWINCH 리드로로 복원. restart-server/restart-all/
restart-check(드라이런) 명령 도입.
-
클라 분해:
clientutil.py/clientscreens.py(모달들)/clientwidgets.py. 약 46% 감량. -
서버 믹스인 분해(7개):
serverclaude.py/servercapture.py/serverpersist.py/serverpty.py/serverio.py/servertree.py. 코어가 크게 가벼워졌다. - CI: GitHub Actions 3-OS 매트릭스 + OS별 벤치 하니스.
-
성능 arc: 컨텍스트 배지 정규식 ReDoS 픽스(420ms→0.08ms)로 시작해 번호화 캠페인.
dirty-gated Claude 스캔, 스타일
lru_cache, 행 단위 screen-delta(Claude/ssh 트래픽 28배 감소), first-paint 우선화. 무거운 스크롤백 화면을 경량 구현으로 교체(2.75배).
설계 전환: 측정 우선·한 변경에 한 레버라는 성능 방법론이 이때 자리 잡았다. 자세한 항목은 Code-Review 의 성능 절 참고.
전수 코드 리뷰가 개선 백로그를 낳았다. Claude 자동 관리 기능군이 들어온 시기다: 토큰
과용 자동 회피, 예산 압박 시 plan-mode 유도, 컨텍스트 하드스톱 자동 /compact, idle 시
자동 문서화→/clear, 정리 빈도 시간하한, 무장된 자동 동작의 카운트다운/취소 힌트.
"claude+pytmux 동시 종료" 조사에서는 "서버 사망 프레이밍은 정정 — 독립적 클라 종료 +
독립적 claude 종료로 분해(증거 기반)" 라는 결론이 나왔다.
- 토큰 가시성: PTY-외부 에스컬레이션 훅, 계정합 예산 게이트, 반복실패/장기턴 경고, 상태바 사용량 + 통합 팝업.
-
그림자 /usage: 헤드리스
claude -p /usage는 한도 숫자를 출력하지 않아 기각 → 숨은 스크랩 세션으로 실제 한도 숫자를 획득. - 보안 패스: 연결 인증 토큰, 피어-UID 검증 + 상태디렉터리 선점 방지, 0600/0700 권한, cols/rows 클램프 + base64 가드.
-
토큰 저장 SQLite 이행: JSONL →
.tokens.db(usagedb.py, WAL), 이력 임포트, 세션 기준 묶기, 서버측GROUP BY로 정확한 생애 합계. stdlibsqlite3라 신규 의존성 0. 대가는 grep/jq 투명성 상실(문서에 명시).
-
NCD("nc" = Norton Change Directory): 루트→cwd 디렉토리 트리, speed-search, Enter=cd,
⇧Enter/^O=새 패널. 코드네임 정정 일화 — 처음엔 양창 파일관리자로 적었으나 실 요구는
디렉토리 변경 전용 도구였다.
clientnc.py의NcdScreen. -
플러그인 시스템 설계: delete-to-disable 계약 —
plugins/<name>/디렉토리를 삭제하면 해당 기능이 명령·완성·디스패치·렌더에서 조용히 사라진다(코어는 하드 import 없이 레지스트리 훅 +getattr가드로만 접근). Claude 관련 코드의 플러그인 추출도 이때 착수했다.
플러그인 구조 전반은 Project-Overview 와 별도 플러그인 문서 참고.
-
토큰 모듈화: 토큰 기록을 코어→플러그인으로 완전 분리. 신설 훅(
server_init/server_opts_init/serialize/pane_closing/pane_init),plugin_opts네임스페이스 + 마이그레이션 shim, DB 물리 이전 + 타머신 자동 마이그레이션, 플러그인 소유 Pane 필드 (panestate.py). 계약 테스트로 "플러그인 부재 시 전 서브시스템 소멸"을 검증한다. - 토큰 회계 정확성 재설계: 핵심 통찰 = 화면 스크랩(현 응답 컨텍스트)과 /usage(청구창 점유%)는 의미가 달라 영영 일치시킬 수 없다 → "스크랩을 앱에 맞춰 보정"은 구조적으로 성립 불가. 채택 = 실측 /usage 1차화 + 추정/실측 분리 표기. 실측 스냅샷을 시계열로 영속하고, 게이트는 실측 기준으로(기본 ON, fail-open: 부재/stale/계정불일치 시 미개입).
- IME 한/영 배지 플러그인: macOS 입력 소스 폴링으로 입력 없이도 즉시 갱신(실측이 권위, 휴리스틱은 ssh/Linux 폴백).
-
i18n(한국어/영어): 프레임워크부터 플러그인 모달까지 전 표면을 단계적으로 롤아웃,
lang ko|en토글, 결정론적 테스트.
-
macOS CI wedge: 다단 진단 끝에 헤드리스 macOS 러너가 PTY 스위트를 코드로 못 고치는
인프라 레벨에서 wedge 된다고 결론(타임아웃·post-step 다 무력,
continue-on-error도 cancel≠fail 이라 무효). 매트릭스에서 제외해 green 복구. macOS 검증은 로컬이 권위. - 원격/페더레이션 attach(ssh stdio-proxy): 원격 셸 안에서 pytmux 를 띄우면 "재접속 반복" 루프가 났다(중첩 방지를 env 마커에만 의존). 해결 = env-독립 in-band 탐지(XTVERSION 프로브) + 별도 ssh exec 채널(8비트 클린 파이프가 길이-프레임 와이어 프로토콜 운반) + 로컬 모델에 원격 패널을 미러링하지 않고 per-client 플래그로 통째 릴레이(원격 서버 무수정· id 재작성 0) + 무한 루프 대신 유한 백오프. 이후 단계에서 원격 탭/외곽선을 분홍색으로 구별하고, 경계횡단 명령을 금지했다.
-
중첩 attach 자동 승격: 원격에서
pytmux를 다시 치면 거부 대신 그 패널의 실 ssh 목적지로 자동 remote-attach. 보안 철칙: attach 인자는 항상 wrapper 가 기록한 실제 목적지, 절대 요청의 자기보고가 아니다 — 위조자는 기껏 이미 신뢰된 호스트로 재트리거만 가능하다. - 프롬프트 히스토리 코어 제거 → 독립 플러그인 재구현. 스티키 헤더 완전 제거.
- 문서 정비: README/매뉴얼 갱신, 갤러리, 플러그인별 README + SVG 스크린샷.
- PytmuxApp 믹스인 분할 + ReDoS/TOCTOU 강화. 전체 코드 감사 항목을 전부 적용 (성능·보안 다수). 자세한 내용은 Code-Review 참고.
- owned-ConPTY 자식종료 감시자: 원격 페더레이션 탭이 좀비로 남던 Windows 버그 수정 (동기 ReadFile 이 셸 자가종료 시 EOF 미발생 → 감시 스레드가 대기 후 콘솔을 닫아 EOF 유발).
-
출력 캡처 플러그인 추출: 서버측을
plugins/rec로(신설 훅server_pty_output/server_shutdown), 배포 기본 OFF. 클라측 표시도 물리 이전. -
플러그인 관리 팝업:
:pluginson/off 토글(영속 설정으로 비활성 목록 저장). - 줌 비활성 패널 reflow 픽스 + 연합 ssh egress 허용목록(opt-in).
시대를 가로질러 두 원칙이 거의 모든 구조 결정을 좌우했다.
-
delete-to-disable 계약: 모든 선택 기능은 디렉토리 삭제 시 코어 오류 없이 깔끔히
사라져야 한다 → 데이터·개입이 전부 플러그인 안에 살고, 코어는 레지스트리 훅·
getattr로만 참조한다. -
스크랩/주입 정직성: pytmux 는 화면 스크랩 텍스트만 보고 키 주입으로만 행동한다 →
가진 것 이상의 확신을 주장하지 않는다. 실측 /usage > 스크랩 추정(항상
~라벨), 비가역 동작엔 두꺼운 게이트, OS 오버레이/대화형 거동은 "수동 검증 전용"으로 명시한다.
- ConPTY 좀비/멀티바이트 깨짐(Windows): 결론을 세 번 뒤집은 사가. 결정적 대비 = 번들 OpenConsole 은 스트리밍하지만 시스템 conhost 는 detached 모드에서 안 한다. 돌파 레시피 = 동기 named pipe + 블로킹 ReadFile + 일회성 숨은 콘솔. owned 백엔드를 Windows 기본으로 전환.
- 캡처-replay 프레임 경계 false-split: 고정 바이트 청크 절단이 상태 토큰을 유령진동시켜 한 응답을 수십 번 분할. 원인 = footer 가 동기출력 블록 안에서 토큰을 함께 갱신 → 중간 청크는 부분 프레임. 픽스 = 동기 종료 지점에서만 분할. 바이트 청크 ≠ 프레임.
- 스크랩 오탐: 자유텍스트 캡처가 산문을 계정/한도로 오인 → 포맷 제약 신호만 신뢰, 사용자 입력·소스줄 제외, 차단 동사 요구. 교훈 = 렌더 이상 신고는 원시 캡처 바이트부터.
- stale 문서 — 보고 전 검증: 백로그 항목 다수가 이미 구현됐는데 체크가 안 된 채 남아 있었다. 버그는 새 표면이 아니라 기존 표면이 조용히 불변식을 깰 때 난다 → 공유 데이터의 모든 소비자를 grep 으로 확인한다.
- 드라이버/헤드리스 검증의 한계와 그 과장: "오버레이는 헤드리스로 검증 불가"는 절반만 진실 — 테스트 하니스는 실제 합성 + 실제 플러그인 훅을 구동한다. 폰트 리거처·실픽셀폭만 사람 눈이 필요하다. 정량화 가능한 부분은 헤드리스로 닫는다.
소개 · 사용
- Project-Overview
- User-Manual
- Screenshots
- Settings-Popup
- Single-Session-Model
- Tmux-Feature-Comparison
플러그인
Claude Code 플러그인
- Claude-Code-Plugins
- Dev-Guide-claude-code
- Dev-Guide-claude-token-usage-view
- Dev-Guide-claude-prompt-history
- Dev-Guide-claude-resume
- Dev-Guide-claude-disable-feedback
플랫폼 · 성능
품질 · 보안
리뷰·분석 보고서
연혁
기여