Architecture¶
wmux is a Go PTY daemon that manages persistent terminal sessions over Unix sockets. The codebase follows Domain-Driven Design (DDD) combined with Package-Oriented Design, enforced by goframe.
High-Level Overview¶
┌──────────────────────────────────────┐
│ cmd/wmux │ CLI / entry point
│ cmd/wmux-tmux │ tmux compatibility shim
├──────────────────────────────────────┤
│ pkg/client │ Public Go SDK for integrators
├──────────────────────────────────────┤
│ internal/ │ Core implementation
│ ┌────────────────────────────────┐ │
│ │ daemon session transport │ │ Domain layer (business logic)
│ ├────────────────────────────────┤ │
│ │ platform/ │ │ Shared infrastructure
│ │ ansi auth config event │ │
│ │ history ipc logger │ │
│ │ protocol pty recording │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
cmd/ contains executable entry points. wmux is the main CLI binary that
bootstraps the fx application. wmux-tmux is a thin shim that translates tmux
commands into wmux equivalents.
pkg/client is the public Go SDK. External programs import this package to communicate with a running wmux daemon without going through the CLI.
internal/ holds all private implementation code, split into two layers: domain packages and platform packages.
DDD + goframe Conventions¶
Each domain package follows a strict file catalog:
| File | Purpose |
|---|---|
entity.go |
Types, structs, sentinel errors (var Err* = errors.New(...)) |
service.go |
Repository interface, Service struct, business logic |
module.go |
var Module = fx.Options(fx.Provide(...)) -- fx wiring only |
options.go |
Functional options: type Option func(*T), With* constructors |
events.go |
Domain event types |
values.go |
Extra value objects |
*_mock.go |
Generated mocks (go.uber.org/mock) |
*_test.go |
Tests (same package, not _test suffix) |
Domains communicate exclusively through the fx dependency injection container. A domain never imports another domain directly; instead, it depends on interfaces that fx satisfies at startup.
Import Rules¶
can import
┌────────────┐ ──────────────► stdlib
│ cmd/* │ ──────────────► anything (domains, platform, external)
└────────────┘
┌────────────┐ ──────────────► stdlib
│ domain/* │ ──────────────► internal/platform/*
└────────────┘ ──────╳──────► other domains
──────╳──────► external libs (except fx in module.go)
┌────────────┐ ──────────────► stdlib
│ platform/* │ ──────────────► external libs
└────────────┘ ──────╳──────► internal/<domain>/
| Source | Allowed targets | Forbidden targets |
|---|---|---|
cmd/* |
stdlib, domains, platform, external libs | -- |
internal/<domain>/ |
stdlib, internal/platform/* |
other domains, external libs (except go.uber.org/fx in module.go) |
internal/platform/* |
stdlib, external libs | internal/<domain>/ |
goframe enforces these rules at lint time (make lint).
Key Design Decisions¶
Why DDD + Package-Oriented Design¶
A PTY daemon touches many concerns: process lifecycle, terminal emulation, IPC, authentication, configuration, and more. DDD provides clear boundaries between these concerns. Each domain owns its entities, business rules, and persistence interfaces. Package-Oriented Design ensures that Go packages map one-to-one to these bounded contexts, preventing the "everything imports everything" problem common in Go monoliths.
Why fx (Dependency Injection)¶
Domains cannot import each other, yet they need to collaborate. fx wires
dependencies at startup through constructor injection: each domain declares
what it provides and what it requires via its module.go. This keeps domain
code free of infrastructure coupling while allowing the cmd/ layer to
compose the full application.
Why a Binary Protocol¶
wmux uses a custom binary framing protocol (version + type + length + payload) for communication between the CLI client and the daemon over Unix sockets. Compared to text-based protocols like JSON-over-newline:
- Efficiency: Terminal output is high-bandwidth (screen redraws, scrolling). Binary framing avoids escaping overhead and enables zero-copy forwarding of raw PTY output.
- Framing clarity: Length-prefixed frames eliminate the need for delimiter parsing, which is error-prone when the payload itself contains arbitrary bytes (terminal escape sequences, binary data).
- Two-channel architecture: The protocol uses separate control and stream channels over the same Unix socket. Control handles low-frequency RPCs (create, resize, list). Stream carries high-frequency PTY I/O. This separation prevents a flood of terminal output from starving control messages.