Skip to content

Why pytmux on Windows

Woojin Kim edited this page Jun 20, 2026 · 1 revision

왜 Windows에서 tmux 대신 pytmux인가

이 문서는 "왜 굳이 또 하나의 터미널 멀티플렉서를 만들었나, 그것도 Windows를 1급 대상으로?"에 대한 솔직한 엔지니어링 근거입니다. 결론부터 말하면: tmux는 POSIX/PTY 가정 위에 세워져 있어 네이티브 Windows에서는 돌지 않고, WSL/Cygwin이라는 호환 레이어를 깔아야 합니다. pytmux는 그 레이어 없이 네이티브 Windows의 ConPTY를 직접 겨냥하는 단일 Python/Textual 코드베이스로 그 간극을 메웁니다.

첫 실행 — 평소 쓰던 셸이 그대로 전체 화면으로(macOS·Linux·Windows 동일 경험)

1. tmux는 왜 네이티브 Windows에서 안 도는가

tmux는 본질적으로 POSIX 프로그램입니다. 핵심 동작들이 다음 같은 Unix 전용 기능에 직접 묶여 있습니다.

  • PTY: pty/forkpty로 의사 터미널을 만들고 fcntl/termiosioctl(예: TIOCSWINSZ)로 윈도우 크기를 조절합니다.
  • 프로세스 모델: fork() + exec()로 셸을 띄우고, setsid로 데몬을 분리하며, killpg/프로세스 그룹/시그널(SIGHUP·SIGKILL 등)로 자식 트리를 다룹니다.
  • IPC: 서버↔클라이언트를 AF_UNIX(유닉스 도메인 소켓)로 잇습니다.
  • 포그라운드 프로세스 그룹: tcgetpgrp 등으로 어떤 프로그램이 전경인지 파악합니다.

Windows에는 fork도, AF_UNIX 중심 모델도, 시그널의 같은 의미도 없습니다. 그래서 tmux를 Windows에서 쓰려면 거의 항상 WSL·Cygwin·MSYS2 같은 POSIX 호환 레이어 위에서 돌립니다. 이는 곧 별도 리눅스 배포판/런타임 설치, 파일시스템· 경로·줄바꿈 경계 문제, 네이티브 Windows 프로그램과의 통합 마찰을 떠안는다는 뜻입니다. "그냥 깔아서 바로"가 안 됩니다.

같은 벽은 pytmux 초기에도 그대로 나타났습니다. 네이티브 Windows Python에서 그냥 실행하면 wcwidth 정도는 설치로 풀리지만, 곧바로 ModuleNotFoundError: No module named 'fcntl'에서 막힙니다 — 여기서부터는 "패키지 설치"가 아니라 **포팅(부분 재작성)**의 영역입니다.

2. 네이티브 Windows가 대신 제공하는 것 — ConPTY/conhost

Windows 10(1809+)부터는 의사 콘솔 API인 ConPTY(Pseudo Console)가 표준으로 제공됩니다. 핵심 개념은 이렇습니다.

  • HPCON: CreatePseudoConsole로 만드는 의사 콘솔 핸들. Unix의 PTY master에 대응하는 자리이며, 입력/출력 파이프 한 쌍과 묶입니다.
  • conhost 모델: 자식 프로그램(셸 등)은 conhost가 관리하는 콘솔에 붙고, VT 시퀀스로 출력합니다. 멀티플렉서는 그 출력 파이프를 읽어 자기 화면 모델로 렌더링하고, 입력 파이프로 키 입력을 흘려보냅니다.
  • 윈도우 크기 조절은 ioctl 대신 ResizePseudoConsole로, 프로세스 종료는 시그널 대신 TerminateProcess/Job 오브젝트(자식 트리 정리)로 합니다.

즉 Windows에는 터미널 멀티플렉싱에 필요한 1급 메커니즘이 이미 OS에 있습니다. 부족한 건 그걸 tmux 스타일로 엮어 주는 멀티플렉서일 뿐입니다.

3. pytmux는 네이티브 Windows를 어떻게 직접 겨냥하는가

pytmux는 WSL/Cygwin 없이 네이티브 Windows를 직접 지원합니다. 핵심 설계는 세 가지 플랫폼 추상 레이어로 압축됩니다.

  • 단일 크로스플랫폼 코드베이스: 화면 모델·VT 파싱·레이아웃·UI(Textual)는 OS에 의존하지 않는 순수 로직이라 macOS·Linux·Windows에서 그대로 공유됩니다. OS 차이는 얇은 백엔드 레이어 뒤로 격리됩니다.
  • ConPTY 백엔드: PTY 계층을 백엔드로 분리해, POSIX에서는 pty.fork를, Windows에서는 ConPTY를 씁니다. Windows 추가 의존성은 pywinpty 하나 (pip install pywinpty)이며, 검증된 ConPTY 래퍼입니다.
  • 이벤트 루프 적응: Windows asyncio의 기본 Proactor 루프는 임의 파이프에 add_reader를 못 씁니다. 그래서 클라이언트 통신은 TCP 루프백으로, ConPTY 출력 읽기는 패널마다 리더 스레드가 블로킹 read 후 루프에 안전하게 넘기는 펌프 구조로 풀었습니다.
  • IPC 적응: AF_UNIX 대신 TCP 루프백(127.0.0.1:랜덤포트) + 포트 번호를 사용자 로컬 데이터 경로의 작은 파일에 기록하는 방식으로 서버↔클라이언트를 잇습니다.
  • 데몬/프로세스 적응: fork+setsid 대신 분리 기동(콘솔 창 없이 백그라운드로 서버 실행), killpg 대신 TerminateProcess/Job 오브젝트로 자식 트리를 정리합니다.

작업 보존 재시작 — 아웃오브프로세스 pty-host

pytmux의 강점 하나는 셸·실행 중 프로그램·스크롤백을 살린 채 서버 코드만 새 이미지로 교체하는 작업 보존 재시작입니다. POSIX에서는 제자리 re-exec로 충분하지만, Windows에서 ConPTY(HPCON)는 그걸 만든 프로세스가 사라지면 함께 무너집니다. 그래서 Windows에서는 별도 프로세스인 pty-host가 ConPTY를 영구 소유하도록 분리했습니다. 서버 프로세스가 재시작돼도 pty-host가 HPCON을 계속 쥐고 있어, 그 위의 셸/프로그램이 끊기지 않습니다. (롤백이 필요하면 환경 변수로 끌 수 있게 두었습니다.)

작업 보존 재시작 점검 — 무엇이 살아남는지 미리 보여주는 드라이런

4. 트레이드오프 — 정직하게

네이티브 Windows를 직접 겨냥한 대가로, POSIX에 직접 대응물이 없는 일부 기능은 크래시 없이 우아하게 폴백하되 약화됩니다.

  • 패널 cwd 상속: 새 분할이 현재 디렉터리에서 시작하는 기능은 Windows에 간단한 per-process cwd 조회가 없어 폴백(서버 cwd에서 시작)합니다.
  • fg 명령 기반 탭 자동이름·ssh 감지: ConPTY가 포그라운드 프로세스 그룹을 노출하지 않아 고정 탭 이름으로 폴백합니다.
  • 시그널 의미: graceful 종료 동작이 TerminateProcess 계열로 대체되며 미세하게 다릅니다.
  • 콘솔 코드페이지/UTF-8: 일부 콘솔 환경에서 출력 인코딩을 맞춰야 할 수 있습니다.

그리고 Windows 콘솔 특유의 입력 아티팩트(예: 수정자 단독 키다운에서 오는 널 문자)는 별도 가드로 흡수했습니다. 이런 항목들은 "기능이 빠질 뿐 서버는 정상 동작" 원칙으로 처리됩니다.

5. 왜 tmux를 감싸지 않고 단일 Python/Textual 구현을 택했나

tmux를 어떤 식으로든 래핑하는 길도 생각할 수 있습니다. 하지만 그 길은 결국 Windows에 POSIX 호환 레이어를 다시 강제합니다(WSL/Cygwin). 그러면 "네이티브 Windows에서 의존성 추가만으로 바로"라는 목표가 무너지고, 두 세계(호환 레이어 안의 tmux와 바깥의 네이티브 프로그램) 사이의 경계 마찰을 항구적으로 떠안습니다.

대신 단일 Python/Textual 구현은 다음을 줍니다.

  • 하나의 코드베이스, 세 OS: 렌더·UI·플러그인 로직이 OS 무관하게 공유되고, 차이는 얇은 백엔드 뒤로만 들어갑니다. 기능을 OS마다 따로 구현하지 않습니다.
  • 마우스·메뉴 1급 UX: tmux 명령을 외우지 않아도 메뉴와 명령 프롬프트로 거의 모든 동작을 할 수 있게 처음부터 설계했습니다 — 이건 tmux를 래핑해선 못 얻습니다.
  • 의존성 최소: Python + Textual + pyte + wcwidth, Windows에선 pywinpty 하나 추가. POSIX 호환 레이어 설치가 필요 없습니다.
  • 검증 가능성: 헤드리스로 전 스위트를 돌려 회귀를 잡고, Windows ConPTY 경로는 클라우드 CI(windows-latest)에서 자동 검증합니다 — 바이트 왕복(CJK 포함)과 자식 종료→EOF 감지까지 매 푸시마다 확인됩니다.

컨테이너로 자동화하면 되지 않나?

검토했지만, 불가능하고 동시에 불필요합니다. Windows 컨테이너는 Windows 커널을 공유해야 해서 비-Windows 호스트(특히 Apple Silicon arm64)에서는 원리적으로 못 돌고, Windows 호스트가 있어도 컨테이너는 헤드리스라 인터랙티브 콘솔 왕복을 보장하지 못합니다. 우리가 자동화하고 싶었던 ConPTY 검증은 이미 클라우드 CI의 windows-latest가 실측 PASS로 수행하고 있어, 컨테이너 경로는 이점이 없습니다. 실 Claude TUI 같은 "사람이 보는" 항목만 실 기기 라이브 검증으로 남깁니다.


관련 문서

Clone this wiki locally