Skip to content

Plugin System

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

플러그인 시스템 소개

pytmux 의 기능 중 상당수(시계 오버레이, 달력, IME 상태 배지, Claude Code 통합 등)는 코어가 아니라 플러그인으로 구현되어 있습니다. 이 페이지는 그 플러그인 시스템의 개념과 아키텍처를 설명합니다. 실제로 플러그인을 하나 작성하는 단계별 가이드는 별도 페이지 Plugin-Authoring-Guide 를 참고하세요.

한 줄 요약: pytmuxlib/plugins/<name>/ 하위의 서브패키지를 선택적으로 불러와 명령·자동완성·디스패치·메시지·렌더·서버 요청을 코어에 합칩니다. 디렉토리를 통째로 지우면 그 기능은 명령 검색·자동완성·디스패치 어디에도 나타나지 않고 조용히 비활성화됩니다. 코어는 플러그인을 직접 import 하지 않고 오직 레지스트리를 통해서만 닿기 때문입니다.


1. 왜 플러그인인가

  • 거대하고 선택적인 기능(예: 디렉토리 트리, Claude Code 통합)을 코어에서 떼어내 한 디렉토리에 응집시키고, 그 디렉토리를 지우는 것만으로 기능을 끌 수 있게 합니다.
  • 코어(client/clientutil/serverio/server 등)는 기능 모듈을 하드 참조하지 않습니다. 명령 이름·메시지 타입·서버 액션·렌더 단계를 레지스트리에 위임하므로, 플러그인이 없으면 해당 경로는 에러 없이 no-op 이 됩니다.

이 설계 덕분에 코어는 "일반 멀티플렉서"라는 작고 안정적인 표면을 유지하고, 무거운 도메인 기능은 독립적으로 추가·제거·실험할 수 있습니다.


2. delete-to-disable 모델

플러그인 시스템의 핵심 계약은 delete-to-disable(지워서 끄기)입니다.

pytmuxlib/plugins/
├── __init__.py              # 로더 + Registry(훅 계약 한곳)
├── clock/                   # 디렉토리 = 한 플러그인
│   └── __init__.py          #   PLUGIN 객체를 노출
├── calendar/
├── claude-code/
└── ...
  • pytmuxlib/plugins/<name>/ 디렉토리 하나가 곧 플러그인 하나입니다.
  • 그 디렉토리를 통째로 삭제하면, 해당 기능은 명령 팔레트·자동완성·디스패치·렌더 어디에도 나타나지 않고 조용히 사라집니다. 코어 코드를 한 줄도 고칠 필요가 없습니다.
  • 가장 큰 플러그인인 claude-code 까지도 이 계약을 만족합니다. 디렉토리를 지우면 서버와 클라이언트 모두 에러 없이 동작하며, Claude 관련 헤더·상태줄 세그먼트·클릭존·토큰 팝업· ESC 동선이 전부 사라지고 평범한 터미널 멀티플렉서로 남습니다.

런타임에 가역적으로 끄고 싶다면(디렉토리를 지우지 않고) 플러그인 관리 팝업에서 토글할 수 있습니다. 이 경우 비활성 플러그인은 모든 훅 디스패치에서 건너뛰어져 무동작이 되며, 설정에 영속됩니다. delete-to-disable 과 달리 되돌릴 수 있습니다.


3. Registry / 훅 아키텍처

3.1 로딩

  • pytmuxlib/plugins/__init__.pyload()Registry 를 만듭니다.
  • pkgutil.iter_modulesplugins/ 하위의 서브패키지(디렉토리 + __init__.py)를 찾아 importlib.import_module 로 불러오고, 각 모듈의 PLUGIN 객체를 모읍니다.
  • import 가 깨진 플러그인은 조용히 건너뜁니다 — 하나가 망가져도 앱 전체가 멈추지 않습니다.
  • 클라이언트와 서버 양쪽load() 를 호출해 각자 self.plugins 로 보관합니다.

3.2 코어는 레지스트리를 통해서만 닿는다

코어는 절대 from pytmuxlib.plugins.clock import ... 같은 직접 import 를 하지 않습니다. 대신 모든 상호작용이 Registry 의 메서드/프로퍼티를 통과합니다. 대표적인 통합 지점:

  • 명령 디스패치: client._run_command 가 코어 분기 후 plugins.handle_command(...) 폴백.
  • 서버 메시지 처리: client._dispatch 의 마지막 else 에서 plugins.handle_message(...).
  • 알 수 없는 서버 액션: serverio._handle_cmd 의 else 에서 plugins.handle_server_request(...).
  • 명령 목록/자동완성: 코어의 COMMANDS·COMPLETIONSplugins.commands· plugins.completions병합해 팔레트·? 목록·자동완성에 노출.

따라서 어떤 플러그인이 없으면 그 경로는 자연스럽게 빈 값/no-op 으로 처리됩니다.

3.3 모든 멤버는 선택적(덕 타이핑)

PLUGIN 객체는 자신이 참여하고 싶은 훅만 노출하면 됩니다. 노출하지 않은 훅은 레지스트리가 빈 값으로 취급합니다. 한 플러그인이 명령 한 줄만 더할 수도, 서버 믹스인과 렌더 단계까지 전부 참여할 수도 있습니다.

PLUGIN 객체가 가질 수 있는 식별 속성:

속성 용도
name 플러그인 이름(토글·영속 키)
description 관리 팝업/진단 표시
category 분류 라벨(예: "오버레이")
default_enabled False 면 배포 기본 OFF(예: 레코딩)

3.4 무게 규칙(중요)

플러그인의 __init__.pytextual 을 모듈 최상단에서 import 하지 않습니다. 서버 프로세스도 같은 load() 코드를 읽기 때문입니다. 화면(textual) 등 무거운 의존은 실제로 필요한 시점에 메서드 안에서 지연 import 합니다(예: from .screens import SomeScreen).

3.5 하이픈 디렉토리명

claude-code 처럼 하이픈이 든 디렉토리명도 됩니다. importlib.import_module(...) 와 패키지 내부 상대 import(from .screens import X)는 하이픈과 무관하게 동작합니다. 단 외부 절대 import 문(from pytmuxlib.plugins.claude-code...)은 문법 오류이므로, 테스트 등 외부에서 가져올 땐 importlib.import_module(...) 를 씁니다.


4. 훅 카테고리

훅 계약은 pytmuxlib/plugins/__init__.pyRegistry 한 곳에 모여 있습니다. 크게 네 갈래로 나눌 수 있습니다.

4.1 명령·메타데이터 훅

의미
commands 코어 COMMANDS 에 합쳐져 ?/팔레트·자동완성에 노출
noarg 인자 없이 즉시 실행해도 되는 명령
completions 자동완성 추가 후보
command_options 팔레트 옵션(선택지) 스키마
pane_scoped 패널 단위로 동작하는 명령 표시
menu_items 컨텍스트/전체 메뉴 항목 기여

4.2 서버측 훅

서버는 단일 스레드 asyncio 루프입니다. 서버측 훅은 스캔·상태·입력·명령 등 서버 동작에 부수효과를 더합니다.

의미
server_mixins Server동적 베이스 클래스로 합성될 믹스인 제공
server_init / server_opts_init / server_opts_serialize 서버 초기화·옵션 로드/저장 참여
handle_server_request 알 수 없는 서버 액션을 받아 회신 메시지(dict) 반환
server_scan 패널 주기 스캔 참여
server_filter_rows 서버가 보내는 행을 표시 필터링
server_status / server_pane_overview status·개요 메시지에 필드 주입
server_input / server_paste / server_pty_output 입력·붙여넣기·PTY 출력 가로채기
server_pending / server_usage_refresh 보류 상태·주기 갱신
server_command 서버 명령 액션 처리
server_shutdown 종료 정리

동적 믹스인 합성: 코어 server.py 는 특정 믹스인을 import 하지 않고 plugins.Registry.server_mixins() 가 돌려준 클래스들을 Server 의 베이스로 합성합니다 (원래의 MRO 보존). 그래서 일부 Server 메서드는 server.py 에 정의가 없고 런타임에 플러그인이 더한 것입니다.

4.3 클라이언트측·렌더 훅

의미
attach_client 앱 인스턴스마다 1회 — 인스턴스 글루(클로저 메서드) 설치
client_unload 앱 종료 시 정리(예: 보조 프로세스 종료)
handle_command / handle_message 클라 명령·서버 메시지 처리
client_tick / client_key 주기 틱·키 입력 참여
client_overlay / client_overlay_key / client_close_overlay 패널을 덮는 오버레이 레이어(시계·달력)
client_render 콘텐츠 레이어 그리기(스티키 헤더·클릭존 스캔 등)
client_status status 메시지의 플러그인 필드 흡수
client_statusbar / client_statusbar_update / client_statusbar_init / client_statusbar_badges 하단 상태줄 세그먼트·배지 렌더
client_status_tabs 통합 상태 팝업에 탭 기여

attach_client 는 특히 중요합니다. 클라이언트 앱(PytmuxApp)이 팩토리 안의 중첩 클래스로 정의되어 서버처럼 동적 믹스인 합성을 못 쓰기 때문에, 플러그인은 attach_client 에서 app.some_method = closure 형태로 인스턴스 클로저를 설치해 기능을 붙입니다. 코어는 그런 메서드를 getattr(app, ..., 기본값) 가드로만 읽으므로 플러그인이 없어도 안 깨집니다.

4.4 라이프사이클·패널 훅

의미
pane_init 패널 생성 시 플러그인 소유 필드 설치
pane_reset 패널 리셋 시 정리
pane_closing 패널 종료 정리
pane_serialize / pane_restore 세션 유지 재시작 시 패널 상태 직렬화/복원

5. 플러그인 발견(Discovery)

  • 발견은 _discover()pkgutil.iter_modulesplugins/ 하위의 서브패키지만 순회하며 이루어집니다(디렉토리 + __init__.py 조건). 단일 .py 모듈은 무시됩니다.
  • 별도의 등록 명단이 없습니다 — 디렉토리를 추가하면 자동 발견되고, 지우면 사라집니다.
  • import 예외가 난 플러그인은 건너뛰므로 다른 플러그인이나 앱 전체에 영향을 주지 않습니다.
  • 발견된 플러그인 중 default_enabled = False 인 것은 배포 기본 OFF 로 두고, 사용자 설정에 별도 지정이 없으면 비활성 상태로 시작합니다.

6. 플러그인 옵션 네임스페이스(plugin opts)

플러그인이 영속 설정을 가질 때, 코어 옵션과 충돌하지 않도록 plugin_opts 네임스페이스 밑에 저장합니다.

  • 서버 초기화 때 server_opts_init 훅으로 플러그인이 자기 옵션을 plugin_opts 에서 읽습니다(과거 top-level 키 하위호환 포함).
  • 저장 때는 server_opts_serialize 훅이 dict 를 돌려주고, 코어는 그것을 설정 파일의 plugin_opts 키 밑에 불투명하게(opaque) 저장합니다. 코어는 그 키들의 의미를 알지 못합니다 — 단지 보관할 뿐입니다.
  • 플러그인이 없으면 plugin_opts 는 비어 사라지므로, 설정 파일도 깨끗하게 유지됩니다.

이 불투명 저장 덕분에 코어는 플러그인이 어떤 설정을 갖는지 몰라도 되고, 플러그인은 자유롭게 설정을 추가·제거할 수 있습니다.


7. 번들 플러그인

저장소에 함께 들어 있는 레퍼런스/실사용 플러그인입니다(작성법·사례는 Plugin-Authoring-Guide 참고).

플러그인 무엇 참여하는 축
clock 패널을 큰 블록 시계로 덮는 오버레이 클라 오버레이(레퍼런스)
calendar 패널을 이번 달 달력으로 덮는 오버레이 클라 오버레이(clock 미러)
ncd Norton Change Directory 풍 디렉토리 트리 모달 + 서버 왕복
ime-indicator 우상단 IME(한/영) 상태 배지 키 관찰 + 렌더
claude-prompt-history Claude 프롬프트 히스토리(미리보기·팝업·점프) 서버 입력 훅 + 모달
claude-token-usage-view 사용 한도 막대 + 리셋 카운트다운 status 흡수 + 모달
claude-resume Claude 세션 자동 재개 보조 서버 훅
claude-disable-feedback 비모달 피드백 오버레이 표시 필터 서버 필터 + 렌더
rec 패널 입출력 레코딩(기본 OFF) 서버 출력 훅
claude-code Claude Code 통합(스캔·상태·토큰·렌더 전체) 모든 축

대표 사례:

  • clock / calendar — 가장 작은 레퍼런스. 패널 위에 한 레이어를 덮는 오버레이만으로 완결됩니다. 클라 오버레이 훅의 모범 예시입니다.
  • ncd — 모달 화면 + 서버 왕복의 예시. 서버 측은 handle_server_request(디렉토리 나열·조상 사슬), 클라 측은 handle_message(결과 표시)로 처리합니다.
  • claude-code — 가장 큰 플러그인. 주기 스캔 루프, 상태줄/헤더 렌더, 토큰 회계, 옵션 영속, 다수 명령과 팝업이 얽혀 있어 거의 모든 훅 카테고리에 참여합니다. 그럼에도 delete-to-disable 계약을 완전히 만족합니다 — 코어에는 이 플러그인의 메서드를 이름으로 직접 부르는 코드가 한 곳도 없으며, 디렉토리를 지우면 일반 멀티플렉서로 깔끔히 남습니다.

claude-code 가 모든 훅을 동원하면서도 코어를 전혀 오염시키지 않는다는 점이, 이 플러그인 시스템이 큰 기능까지 감당할 수 있음을 보여 줍니다.


관련 문서

Clone this wiki locally