A desktop operating system shell built with Tauri 2 (Rust backend) and SolidJS (TypeScript frontend). The system features a domain-based WASD keyboard navigation system, windowed application management, and a modular HMI (Human-Machine Interface) architecture.
HyphaeicOS follows a Rust-first architecture where the backend owns all application state and input handling, while the frontend serves as a reactive UI layer.
┌─────────────────────────────────────────────────────────────────────┐
│ Rust Backend │
│ ┌───────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │
│ │ Asset Loader │ │ Domain Navigator│ │ State Manager │ │
│ │ (remote cache)│ │ (WASD navigation)│ │ (window tracking) │ │
│ └───────────────┘ └─────────────────┘ └───────────────────────┘ │
│ ↕ Tauri Events (IPC) │
├─────────────────────────────────────────────────────────────────────┤
│ TypeScript Frontend │
│ ┌───────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │
│ │ Controller │ │ Domain │ │ Interface │ │
│ │ (event relay) │ │ (navigation UI) │ │ (visual layout) │ │
│ └───────────────┘ └─────────────────┘ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
The Rust backend (src-tauri/) handles all core functionality including input capture, navigation logic, window management, and asset caching.
- Tauri 2.0: Cross-platform desktop framework
- tauri-plugin-global-shortcut: OS-level keyboard capture
- tauri-plugin-opener: System opener integration
- reqwest: HTTP client for asset downloading
- tokio: Async runtime for file I/O
- serde: JSON serialization/deserialization
- uuid: Unique window identifiers
- portable-pty: PTY support for terminal emulation
- rodio: Low-latency audio playback and mixing
src-tauri/src/
├── lib.rs # Main entry point, Tauri commands, event emission
├── main.rs # Application bootstrap
├── assetLoader/ # Remote asset downloading and caching
│ ├── mod.rs
│ └── asset_loader.rs # URL-based asset management
├── audio/ # Low-latency audio system
│ ├── mod.rs # AudioSystem struct and initialization
│ ├── sfx.rs # Sound effects (decode-on-load, instant playback)
│ └── ambience.rs # Ambient audio with crossfading
├── inputHandler/ # WASD navigation system
│ ├── mod.rs
│ ├── types.rs # Data structures (Domain, Button, Gate, etc.)
│ ├── domain_navigator.rs # Navigation logic and cursor management
│ └── spatial.rs # Spatial navigation algorithms
├── pty/ # Terminal emulation
│ └── mod.rs # PTY session management
└── state/ # Application state management
├── mod.rs # StateManager (window tracking, slots)
└── window.rs # Window types (WindowInstance, WindowState)
The navigation system provides WASD-based cursor movement across UI "domains" (containers of interactive elements).
Key Concepts:
- Domain: A UI container with navigable elements (buttons, gates)
- Button: An interactive element that can receive focus
- Gate: A boundary element for switching between domains
- Cursor: The currently focused element (managed by Rust)
Layout Modes:
list-vertical: Elements in a vertical list (W/S navigation)list-horizontal: Elements in a horizontal list (A/D navigation)grid: 2D grid layout with configurable columnsspatial: Free-form positioning using screen coordinates
How it works:
- Global shortcuts (WASD, Enter, Space) are captured at OS level
- Rust processes navigation based on current domain's layout mode
- Cursor position is updated in Rust state
- Events are emitted to frontend via Tauri IPC
// Navigation result types
enum NavigationResult {
CursorMoved { domain_id, element_id, element_type },
AtGate { gate_id, target_domain },
DomainSwitched { from_domain, to_domain, new_element_id },
BoundaryReached,
NoActiveDomain,
Error { message },
}The state manager handles windows and the dual-slot compositor system.
StateManager Structure:
pub struct StateManager {
pub windows: HashMap<String, WindowInstance>, // All active windows
pub window_stack: Vec<String>, // Focus history (z-order)
pub left_slot: Option<String>, // Window ID in left slot
pub right_slot: Option<String>, // Window ID in right slot
}WindowInstance:
id: UUID for the windowcontent_key: What to render (e.g.,"SYS_TERMINAL")state:Minimized|Maximized|Hidden|Closingslot:Left|Rightsource_element_id/source_domain_id: For focus return on close
Window Lifecycle:
spawn_window()→ Creates window in first available slot, emitswindow-createdset_window_state()→ Transitions between states, emitswindow-state-changedclose_window()→ Sets state toClosing, triggers animationremove_window()→ Removes from state, emitswindow-closedandreturn-focus
Manages pseudo-terminal sessions for terminal emulation using portable-pty.
Key Features:
- Reference counting: Multiple components can share a session
- Thread-safe: Background reader thread buffers PTY output
- Platform-aware: PowerShell on Windows, bash on Unix
- Graceful cleanup: Handles Windows ConPTY quirks
Session Lifecycle:
pty_spawn(session_id)→ Creates PTY session with shell processpty_write(session_id, data)→ Writes to PTY stdinpty_read(session_id)→ Drains output buffer (non-blocking)pty_resize(session_id, rows, cols)→ Resizes terminalpty_close(session_id)→ Decrements ref count, cleans up when zero
Downloads and caches remote assets (images, videos, audio, documents) to the local app data directory.
Features:
- URL-to-filename hashing for cache keys
- Type-based subdirectories (
images/,videos/, etc.) - Cache status checking
- Cache clearing (per-type or all)
Tauri Commands:
load_asset(url, asset_type) -> AssetInfo
is_asset_cached(url, asset_type) -> bool
get_asset_cache_path(url, asset_type) -> String
clear_asset_cache(asset_type?) -> StringA low-latency audio system built on rodio with two distinct engines:
SFX Engine (sfx.rs):
- Decode-on-load strategy: All sound effects are decoded to raw PCM at startup
- Instant playback: Creating a cursor over pre-decoded buffers takes nanoseconds
- Fire-and-forget: No management needed after triggering
Ambience Engine (ambience.rs):
- Virtual Timeline: All ambient tracks conceptually play simultaneously, mixed dynamically
- PCM Buffering: MP3s decoded to
Vec<f32>for seamless looping viarepeat_infinite() - Crossfading: Dedicated background thread handles volume ramping at ~100Hz
- Domain-aware: Automatically switches ambient tracks based on active UI domain
How It Works:
// SFX - instant feedback
audio_system.play_sfx("nav"); // Navigation sound
audio_system.play_sfx("click"); // Click sound
// Ambience - domain-based switching with crossfade
audio_system.on_domain_change("new-domain-id");Performance:
- Memory: ~30MB per 3-minute stereo ambient track (uncompressed PCM) // note for future: this must be trimmable. surely we don't need to keep everything in memory at once, can we do something like keep a virtual timeline, then 10 seconds of every track in a single rolling window loading in and out of memory?
- CPU: Negligible (fade thread mostly sleeping, mixing handled by OS audio thread)
| Command | Parameters | Description |
|---|---|---|
register_domain |
domainId, parentDomain?, layoutMode, gridColumns? |
Register a navigation domain |
unregister_domain |
domainId |
Remove a domain |
register_button |
domainId, buttonId, bounds?, order |
Add a button to a domain |
unregister_button |
domainId, buttonId |
Remove a button |
register_gate |
gateId, sourceDomain, targetDomain, direction, entryPoint? |
Add a domain gate |
set_active_domain |
domainId |
Set which domain receives input |
handle_wasd_input |
key |
Process navigation (usually via global shortcuts) |
switch_domain |
- | Activate current gate |
emit_cursor_position |
- | Force emit cursor-moved event |
| Command | Parameters | Description |
|---|---|---|
spawn_window |
contentKey, sourceElementId?, sourceDomainId? |
Create a new window |
close_window |
id |
Begin window close animation |
remove_window |
id |
Remove window from state |
set_window_state |
id, windowState |
Change window state |
| Command | Parameters | Description |
|---|---|---|
pty_spawn |
sessionId |
Spawn a new PTY session |
pty_write |
sessionId, data |
Write to PTY stdin |
pty_read |
sessionId |
Read buffered PTY output |
pty_resize |
sessionId, rows, cols |
Resize terminal |
pty_close |
sessionId |
Close PTY session |
get_system_banner |
sessionId |
Get boot banner for terminal |
| Command | Parameters | Description |
|---|---|---|
toggle_fullscreen |
- | Toggle window fullscreen mode |
set_global_shortcuts_enabled |
enabled |
Enable/disable WASD shortcuts |
greet |
name |
Test command |
Events emitted by Rust to notify the frontend of state changes:
| Event | Payload | Description |
|---|---|---|
cursor-moved |
{ domain_id, element_id, element_type } |
Cursor position changed |
button-activate |
{ domain_id, element_id, element_type } |
Button activation (Enter/Space) |
at-gate |
{ gate_id, target_domain } |
Cursor reached a gate |
domain-switched |
{ from_domain, to_domain, new_element_id } |
Domain switch completed |
boundary-reached |
{ direction } |
Cursor hit domain edge |
window-created |
WindowInstance |
New window spawned |
window-closed |
string (id) |
Window removed |
window-state-changed |
WindowInstance |
Window state updated |
return-focus |
{ domain_id, element_id } |
Return focus after window close |
The frontend (src/) is a reactive UI layer that listens to Tauri events and renders the interface.
- SolidJS 1.9: Reactive UI framework
- Vite 6: Build tool and dev server
- TypeScript 5.6: Type safety
- @tauri-apps/api: Tauri event/invoke bindings
src/
├── App.tsx # Root component
├── App.css # Global styles
├── index.tsx # Entry point
├── vite-env.d.ts # Vite type definitions
└── HMI/ # Human-Machine Interface components
├── store.ts # Window store (SolidJS reactive state)
├── A_Controller/ # Input hub (event translation)
├── A_Domain/ # Domain wrapper component
├── A_Interface/ # Visual layout composition
├── Button/ # Interactive button component
├── Background/ # Background layer
├── OSbar/ # Navigation/status bar
├── WindowManager/ # Window and compositor components
│ ├── Compositor/ # Dual-slot compositor
│ └── Window/ # Window chrome and content
└── TESTING_DUMMY/ # Test content component
The Controller is the single point of Tauri event handling. It translates Rust events into DOM CustomEvents for UI components.
Responsibilities:
- Listen to all Tauri events from Rust
- Dispatch DOM CustomEvents to components
- Handle button activation via
document.getElementById().click() - Manage global shortcut enable/disable on focus/blur
- Handle F11 fullscreen toggle
HMR-Safe Design: The Controller uses a window-level singleton pattern to survive Hot Module Reload:
window.__HYPHA_CONTROLLER_STATE__ = {
tauriListenersActive: boolean,
tauriUnlisteners: UnlistenFn[],
domCleanup: (() => void) | null
}A wrapper component that registers a navigation domain with Rust.
Usage:
<Domain id="main-menu" layoutMode="list-vertical">
<Button id="btn-start" order={0} onClick={handleStart} />
<Button id="btn-options" order={1} onClick={handleOptions} />
</Domain>Provides Context:
useDomain(): Get domain context (id, isReady)useDomainId(): Get just the domain ID
SolidJS reactive store for window state:
export const [windowStore, setWindowStore] = createStore<WindowStoreState>({
windows: [],
});
// Helpers
addWindow(window: WindowInstance)
removeWindow(id: string)
updateWindow(updatedWindow: WindowInstance)
getWindowInSlot(slot: CompositorSlot)┌─────────────────────────────────────────────────────────────────────┐
│ USER PRESSES WASD KEY │
└────────────────────────────────┬────────────────────────────────────┘
│ OS-level key event
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Rust: Global Shortcut Handler │
│ (tauri-plugin-global-shortcut) │
└────────────────────────────────┬────────────────────────────────────┘
│ Direct Rust call
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Rust: DomainNavigator.handle_wasd_input() │
│ → Updates cursor_position │
│ → Returns NavigationResult │
└────────────────────────────────┬────────────────────────────────────┘
│ app.emit("cursor-moved", payload)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ TypeScript: Controller.tsx │
│ listen('cursor-moved') callback │
│ → window.dispatchEvent(CustomEvent) │
└────────────────────────────────┬────────────────────────────────────┘
│ DOM CustomEvent
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Component: Button_IC.tsx │
│ → Checks if element_id matches │
│ → setIsFocused(true/false) │
│ → CSS class updates │
└─────────────────────────────────────────────────────────────────────┘
Frontend → Backend (invoke):
import { invoke } from "@tauri-apps/api/core";
// Register domain on mount
await invoke('register_domain', {
domainId: 'main-menu',
layoutMode: 'list-vertical'
});
// Spawn window on button click
await invoke('spawn_window', {
contentKey: 'TESTING_DUMMY',
sourceElementId: 'btn-open',
sourceDomainId: 'osbar-nav'
});Backend → Frontend (events):
import { listen } from "@tauri-apps/api/event";
// Listen for cursor movement
const unlisten = await listen('cursor-moved', (event) => {
window.dispatchEvent(new CustomEvent('sys-cursor-move', {
detail: event.payload
}));
});- Node.js 18+ and pnpm
- Rust 1.75+ with
cargo - Tauri CLI:
cargo install tauri-cli
# Install dependencies
pnpm install
# Start development server (runs both Vite and Tauri)
pnpm tauri dev# Build the application
pnpm tauri buildHyphaeicOS/
├── src/ # Frontend source (SolidJS/TypeScript)
├── src-tauri/ # Backend source (Rust/Tauri)
│ ├── src/ # Rust source files
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Tauri configuration
├── public/ # Static assets
├── dist/ # Build output
├── package.json # Node.js dependencies
├── vite.config.ts # Vite configuration
└── tsconfig.json # TypeScript configuration
cd src-tauri
cargo testTests include:
- Domain registration and unregistration
- Button registration with auto-focus
- Navigation (list, grid, spatial)
- Boundary detection
- Gate switching
In Development Mode:
- Press X key to dump navigation state to console
- Press F11 to toggle fullscreen
- Press F12 to open DevTools
Query Commands:
// Get all registered domains
const domains = await invoke('get_all_domains');
// Get current cursor position
const cursor = await invoke('get_cursor_position');
// Get detailed domain info
const info = await invoke('debug_domain', { domainId: 'main-menu' });{
"productName": "HyphaeicOS",
"version": "0.1.0",
"identifier": "com.eonk.hyphaeicos",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"app": {
"windows": [{
"title": "HyphaeicOS",
"width": 800,
"height": 600
}],
"security": {
"assetProtocol": {
"enable": true,
"scope": { "allow": ["$APPDATA/**"] }
}
}
}
}- SolidJS plugin enabled
- Dev server on port 1420
- HMR configured for Tauri
Detailed documentation is available in the following files:
| Document | Location | Description |
|---|---|---|
| Asset Loader README | src-tauri/src/assetLoader/README.md |
Remote/local asset loading and caching |
| State Management README | src-tauri/src/state/README.md |
Windows, compositor, and PTY sessions |
| Audio System README | src-tauri/src/audio/README.md |
Audio architecture and integration guide |
| Input Handler README | src-tauri/src/inputHandler/INPUT_HANDLER_README.md |
Complete navigation system docs |
| Controller Architecture | src/HMI/A_Controller/ARCHITECTURE.md |
Frontend input handling design |
| Domain Navigation Example | src-tauri/src/inputHandler/DOMAIN_NAVIGATION_EXAMPLE.md |
Usage examples |
| Implementation Summary | src-tauri/src/inputHandler/IMPLEMENTATION_SUMMARY.md |
Technical details |
- Rust-First: Backend owns all state; frontend is reactive
- Event-Driven: Unidirectional data flow via Tauri events
- Domain-Based Navigation: Structured UI regions for keyboard control
- Dual-Slot Compositor: Two-window side-by-side layout
- HMR-Safe: Development experience survives hot reload