Console client build guide
Developer build spec for the Phlix terminal (TUI) client — PHP 8.3+, SugarCraft stack. For end-user install and setup, see Console Client.
Table of Contents
- Overview
- Development Environment
- Project Structure
- Architecture
- API Client Implementation
- Rendering Pipeline
- Screen System
- Testing
- Building a PHAR
- Implementation Checklist
1. Overview
1.1 Platform Capabilities
The console client renders in any modern terminal using:
- Graphics protocols: sixel, kitty, iTerm2 — real image rendering
- Cell modes: half-block, quarter-block, ascii, ansi256, truecolor — character-based fallback
- ANSI escape codes: cursor control, colors, position
1.2 Key Libraries
| Library | Purpose |
|---|---|
sugarcraft/sugar | Core TUI framework |
sugarcraft/input | Keyboard/raw input handling |
| `sugarcraft mosaic | Mosaic grid layout |
sugarcraft/poster | Poster image rendering |
sugarcraft/video | Video frame decoding |
2. Development Environment
2.1 Required Software
1. PHP 8.3+ (CLI)
2. Composer 2.x
3. Git
4. ffmpeg + ffprobe (video decode)
5. ffplay or mpv (audio playback)
6. A terminal emulator with graphics protocol support:
- iTerm2 (macOS) — native sixel/iTerm2 support
- Kitty — native kitty protocol
- WezTerm — sixel support
- Windows Terminal — sixel support (Windows 11 22H2+)2.2 Project Setup Commands
# Clone the repository
git clone https://github.com/detain/phlix-console-client.git
cd phlix-console-client
# Install dependencies
composer install
# Verify installation
./bin/phlix doctor3. Project Structure
phlix-console-client/
├── bin/
│ └── phlix # CLI entrypoint
├── src/
│ ├── Api/
│ │ ├── ApiClient.php # Base API client
│ │ └── Admin/
│ │ └── AdminClient.php # Admin API endpoints
│ ├── Config/
│ │ ├── Config.php # Application configuration
│ │ ├── TokenStore.php # Auth token persistence
│ │ ├── ServerEntry.php # Server configuration entry
│ │ └── TokenBundle.php # Auth token bundle
│ ├── Console/
│ │ ├── App.php # Main application
│ │ ├── Capabilities.php # Terminal capability detection
│ │ └── ColorPalette.php # ANSI color handling
│ ├── Media/
│ │ ├── PosterLoader.php # Poster image loading
│ │ └── MosaicFactory.php # Mosaic grid factory
│ ├── Screen/
│ │ ├── Screen.php # Base screen interface
│ │ ├── HomeScreen.php # Home/library rail screen
│ │ ├── GridScreen.php # Virtualized poster grid
│ │ ├── DetailScreen.php # Media detail view
│ │ ├── PlayerScreen.php # Video player
│ │ └── AdminUsersScreen.php # Admin user management
│ ├── Store/
│ │ ├── AuthStore.php # Authentication state
│ │ ├── LibrariesStore.php # Libraries cache
│ │ └── MediaStore.php # Media items cache
│ └── Msg/
│ ├── Msg.php # Base message
│ ├── NavigateBackMsg.php
│ ├── OpenLibraryMsg.php
│ ├── OpenDetailMsg.php
│ └── ... # Other messages
├── tests/
│ ├── Unit/
│ └── Integration/
├── docs/
│ └── images/ # Render mode screenshots
├── composer.json
├── phpstan.neon
└── README.md4. Architecture
4.1 Application Flow
bin/phlix
└── buildContainer() — Build DI container
└── App::boot() — Bootstrap application
└── Program::run() — Main event loop
Event Loop:
Input → Dispatch → Screen → Render → Output4.2 Screen System
Screens implement Screen interface and use ThemedScreen trait:
interface Screen
{
public function render(): string; // Render the screen
public function handle(Msg $msg): ?Screen; // Handle a message, return next screen
public function columns(): int; // Terminal columns
public function rows(): int; // Terminal rows
}The App hand-rolls its own routing rather than using candy-core's ScreenStack. The ScreenStack discards a nested screen's updated model when navigating away, so it cannot host a stateful form — the Route enum (see src/Route.php:13-15) names the current destination instead:
/**
* The top-level screen the {@see App} is showing. The App hand-rolls routing
* (candy-core's ScreenStack discards a nested screen's updated model, so it
* can't host a stateful form) — this enum names the current destination.
*/
enum Route { ... }4.3 Message Passing
Screens communicate via immutable messages:
// Navigate to a library
$navigateMsg = new OpenLibraryMsg($libraryId);
// Open media detail
$detailMsg = new OpenDetailMsg($mediaId);
// Go back
$backMsg = new NavigateBackMsg();5. API Client Implementation
5.1 ApiClient Class
// src/Api/ApiClient.php
class ApiClient
{
public function __construct(string $serverUrl)
{
$this->baseUrl = rtrim($serverUrl, '/');
$this->token = TokenStore::default()->load()?->accessToken;
}
public function request(string $method, string $path, array $body = null): mixed
{
$url = $this->baseUrl . '/api/v1' . $path;
$headers = ['Content-Type: application/json'];
if ($this->token !== null) {
$headers[] = 'Authorization: Bearer ' . $this->token;
}
// ... HTTP request via cURL
}
// Authentication
public function login(string $username, string $password): array;
public function logout(): void;
// Libraries
public function getLibraries(): array;
public function getLibraryItems(string $libraryId, array $options = []): array;
// Media
public function getItem(string $itemId): array;
public function getPlaybackInfo(string $itemId): array;
public function getPoster(string $itemId, int $width = 400): string;
// User data
public function updateUserData(string $itemId, array $data): void;
public function getProgress(string $itemId): ?array;
public function saveProgress(string $itemId, array $progress): void;
// Search
public function search(string $query, array $options = []): array;
}6. Rendering Pipeline
6.1 Mosaic Grid
Posters are rendered in a mosaic grid using MosaicFactory:
// Create a mosaic for the poster grid
$mosaic = MosaicFactory::forPosterGrid($mode); // $mode = 'sixel', 'halfblock', etc.
// Render a single poster
$poster = $posterLoader->load($mediaItem, $width, $height);
// Render the grid
$grid = $mosaic->renderGrid($posters, $columns, $rows);6.2 Render Modes
// Cell modes (tile as colored text)
halfblock → 24-bit color half-block characters (▀)
quarterblock → dense color blocks (▘▝▖▗)
ascii → monochrome character ramp
ansi256 → 256-color character ramp
truecolor → 24-bit color character ramp
// Graphics modes (real images tiled)
sixel → Sixel protocol
kitty → Kitty protocol
iterm2 → iTerm2 inline images
auto → Half-block by default, graphics on demand6.3 Video Frame Extraction
Video frames are decoded via ffmpeg and converted to ANSI-escaped strings:
// Probe a video file using ffprobe
$report = $this->videoSpike->probeReport($path);
// Returns: duration, resolution, codecs, etc.
// Extract frames using ffmpeg
$frames = $this->videoSpike->frames($path, $count = 1, $cols = 60, $rows = 20, $mode = 'halfblock');
// Yields ANSI-escaped strings for each frame7. Screen System
7.1 Home Screen
The home screen displays library rails:
┌─ Phlix ─────────────────────────────────────────────┐
│ │
│ Movies TV Shows Music Audiobooks Photos │
│ ████████████████████████████████████████ │
│ [poster] [poster] [poster] [poster] [poster] │
│ │
│ [poster] [poster] [poster] [poster] [poster] │
│ │
└─────────────────────────────────────────────────────┘7.2 Grid Screen
Virtualized poster grid with filtering and sorting:
- Scroll handling (↑↓←→, Page Up/Down, Home/End)
- A–Z jump bar
- Filter by genre, year, etc.
- Sort by name, date added, rating
7.3 Detail Screen
Media detail with:
- Large hero poster
- Metadata (title, year, rating, duration, synopsis)
- Cast list with ANSI avatars
- Play button and action menu
7.4 Player Screen
In-terminal video player:
- Direct play through ffmpeg (HEVC/MKV/AV1 support)
- Scrubber with chapter ticks
- Resume from last position
- Progress reporting to server
- Up-next auto-advance
- On-demand subtitles
- Transcode fallback
8. Testing
# Run all tests
composer test
# Run unit tests only
vendor/bin/phpunit --testsuite Unit
# Run with coverage
vendor/bin/phpunit --coverage-html coverage-report/
# Static analysis
composer phpstan9. Building a PHAR
# Install box PHAR builder
composer require --dev humbug/box
# Build
./scripts/build-phar.sh
# Output: build/phlix.pharThe PHAR is a self-contained executable containing all dependencies.
10. Implementation Checklist
- [x] C0: Project bootstrap, composer.json, autoloader
- [x] C1: Config/TokenStore, Config::dir(), Config::load()
- [x] C2: ApiClient skeleton + GET /libraries
- [x] C3: HomeScreen with library rails
- [x] C4: GridScreen (virtualized poster grid)
- [x] C5: DetailScreen (hero, metadata, cast)
- [x] C6: PlayerScreen (ffmpeg direct play, scrubber, resume)
- [x] C7: Search, Command Palette, Toasts
- [x] C8: Admin sections (Dashboard, Users, Plugins, etc.)
- [x] C9: Music/Audiobook/Photo/Book browsers
- [x] C10: Polish, theming, Now-Playing bar, metrics HUD