# Architecture of Tabby's Serial Terminal Implementation: A Deep Dive into the Modular Design

> Explore Tabby's serial terminal architecture featuring a modular three-layer design: binding, session, and UI. Discover cross-platform support and reusable terminal framework.

- Repository: [Eugene/tabby](https://github.com/Eugeny/tabby)
- Tags: architecture
- Published: 2026-03-03

---

**Tabby's serial terminal uses a three-layer architecture consisting of a binding layer (SerialService), a session layer (SerialSession), and a UI layer (SerialTabComponent), enabling cross-platform support through pluggable bindings while reusing the core terminal framework.**

The serial terminal implementation in the [Eugeny/tabby](https://github.com/Eugeny/tabby) repository demonstrates how this modern terminal emulator handles hardware serial connections using the same extensible framework that powers its SSH and local shell sessions. By leveraging a modular design with clear separation of concerns, the architecture of Tabby's serial terminal implementation provides consistent behavior across web and desktop platforms while supporting advanced features like configurable flow control and middleware-based data processing.

## The Three-Layer Architecture

Tabby's serial implementation is organized into three distinct layers that handle platform abstraction, session management, and user interaction.

### Binding Layer – SerialService

The **SerialService** class in [`tabby-serial/src/services/serial.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/services/serial.service.ts) acts as the platform abstraction layer, detecting and providing the appropriate serial port binding at runtime.

For web builds, it utilizes the **Web Serial API** (`WSABinding`), while desktop applications use a native C++ binding through the `autoDetect()` method. This service exposes essential utilities including `listPorts()` for enumerating available devices and `quickConnect()` for creating temporary connections via URL patterns like `/dev/ttyUSB0@9600`.

```typescript
import { SerialService } from 'tabby-serial/src/services/serial.service';
import { Injector } from '@angular/core';

async function showPortList(injector: Injector) {
    const serialService = injector.get(SerialService);
    const ports = await serialService.listPorts();
    // ports is an array of { name: string, description?: string }
    console.table(ports);
}

```

### Session Layer – SerialSession

The **SerialSession** class, defined in [`tabby-serial/src/api.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/api.ts), extends **BaseSession** from **tabby-terminal** to inherit generic terminal features including logging, middleware handling, and login script processing.

When instantiated, it creates a **SerialPortStream** using the detected binding and profile parameters (port path, baud rate, data bits, parity, and flow control). The session constructs a middleware chain that processes all data flowing between the serial device and the terminal:

- **TerminalStreamProcessor** – Parses terminal output and handles screen size changes
- **SlowFeedMiddleware** – Throttles outgoing data when `slowSend` is enabled in the profile
- **UTF8SplitterMiddleware** – Ensures proper UTF-8 boundary handling
- **InputProcessor** – Applies input-processing options from the profile configuration

The `start()` method opens the port, wires event handlers (`open`, `error`, `close`, `readable`, `end`), and forwards received data to the terminal via `emitOutput()`. It also emits service messages such as "Port opened" and "Port closed" to inform the UI of state changes.

### UI Layer – SerialTabComponent

**SerialTabComponent** in [`tabby-serial/src/components/serialTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/components/serialTab.component.ts) inherits from **ConnectableTerminalTabComponent**, which itself extends **BaseTerminalTabComponent** from the core terminal module.

This component manages the visual representation of the serial connection, including the toolbar, hot-key handling, and reconnection logic. During `initializeSession()`, it instantiates a `SerialSession` for the current profile and displays a loading spinner until the port opens successfully.

The component handles user actions such as changing baud rates through the `changeBaudRate()` method and wires hot-keys (`home`, `end`, `restart-serial-session`) through the shared hot-key service.

## Data Flow and Session Lifecycle

Understanding how these layers interact clarifies the architecture's efficiency.

1. **Profile Activation**: The user selects a serial profile or uses a quick-connect URL, triggering `SerialService.quickConnect()` to build a temporary `SerialProfile` stored in `localStorage`.

2. **Tab Creation**: The `ProfilesService` opens a new tab, instantiating `SerialTabComponent`.

3. **Session Initialization**: The component's `initializeSession()` method creates a `SerialSession` and calls `start()`.

4. **Port Opening**: The session uses `SerialService.detectBinding()` to obtain the correct platform binding and opens the port via `SerialPortStream`.

5. **Ready State**: On the `open` event, the UI spinner stops, a service message emits, and the terminal becomes interactive.

6. **Data Transmission**: User input flows from `SerialTabComponent` to `SerialSession.write()`, while incoming device data travels through the middleware chain to `emitOutput()`.

7. **Error Handling**: Connection failures or closures trigger service messages and session destruction, with the UI offering reconnection options.

## Key Design Decisions

### Shared Terminal Core

By extending `BaseSession` and `ConnectableTerminalTabComponent`, the serial implementation reuses the same middleware pipeline, UI toolbar, and hot-key handling as SSH or local shells. This avoids code duplication and ensures consistent user experience across connection types.

### Pluggable Binding Strategy

The `SerialService` abstraction allows identical UI code to execute in both browser environments (using WebSerial) and native Electron applications (using native bindings). This polymorphic approach eliminates platform-specific conditional logic from the session and UI layers.

### Middleware Flexibility

The architecture demonstrates configurable behavior through middleware insertion. When a profile enables `slowSend`, the session inserts `SlowFeedMiddleware` at the front of the pipeline without altering core session logic, illustrating a clean extension point for custom data processing.

### Profile-Centric Configuration

All serial parameters—including port path, baudrate, flow-control flags, and input processing options—reside in the `SerialProfile` interface. Runtime modifications, such as changing baud rates via the UI dialog, update both the profile and the underlying `SerialPortStream` through the `serial.update()` method.

## Practical Implementation Examples

### Programmatically Opening a Serial Tab

You can open a serial connection programmatically by constructing a `SerialProfile` and using the `ProfilesService`:

```typescript
import { ProfilesService } from 'tabby-core';
import { Injector } from '@angular/core';
import { SerialProfile } from 'tabby-serial/src/api';

function openSerialTab(injector: Injector, path: string, baud = 115200) {
    const profile: SerialProfile = {
        name: `Serial ${path}`,
        type: 'serial',
        options: {
            port: path,
            baudrate: baud,
            databits: 8,
            stopbits: 1,
            parity: 'none',
            rtscts: false,
            xon: false,
            xoff: false,
            xany: false,
            slowSend: false,
            input: { /* use default input processing */ },
            // StreamProcessingOptions and LoginScriptsOptions can be added here
        }
    };
    const profilesService = injector.get(ProfilesService);
    profilesService.openNewTabForProfile(profile);
}

```

### Changing Baud Rate Dynamically

To modify connection parameters during an active session:

```typescript
// Inside SerialTabComponent
await this.changeBaudRate();  // invokes the selector dialog and updates the session

// Internally this calls:
this.session?.serial?.update({ baudRate: newRate });
this.profile.options.baudrate = newRate;

```

## Summary

- **Tabby's serial terminal** employs a three-tier architecture separating platform bindings, session management, and UI components.
- **SerialService** ([`tabby-serial/src/services/serial.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/services/serial.service.ts)) handles cross-platform binding detection for both Web Serial API and native implementations.
- **SerialSession** ([`tabby-serial/src/api.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/api.ts)) extends the core terminal framework, implementing a middleware pipeline for data processing and device communication.
- **SerialTabComponent** ([`tabby-serial/src/components/serialTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/components/serialTab.component.ts)) provides the Angular-based UI, inheriting reconnection logic and toolbar functionality from `ConnectableTerminalTabComponent`.
- The design emphasizes **code reuse** through inheritance from `BaseSession` and profile-driven configuration for all connection parameters.

## Frequently Asked Questions

### How does Tabby handle serial connections differently in the browser versus desktop apps?

Tabby abstracts platform differences through the `SerialService.detectBinding()` method. For web builds, it returns `WSABinding` to leverage the Web Serial API, while desktop applications use `autoDetect()` to load native C++ bindings. This allows `SerialSession` and the UI components to remain platform-agnostic.

### What middleware does Tabby use for serial data processing?

The serial session implements a chain of middleware processors including `TerminalStreamProcessor` for screen handling, optional `SlowFeedMiddleware` for data throttling, `UTF8SplitterMiddleware` for character encoding, and `InputProcessor` for profile-specific input handling. These are chained together in `SerialSession.start()` according to the profile configuration.

### Where are the available baud rates defined in the Tabby source code?

The standard baud rate options are declared as the `BAUD_RATES` constant in [`tabby-serial/src/api.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/api.ts). This array is used to populate selection dialogs in the UI and to validate profile configurations during session initialization.

### Can I programmatically list available serial ports before opening a connection?

Yes, the `SerialService.listPorts()` method in [`tabby-serial/src/services/serial.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-serial/src/services/serial.service.ts) returns a Promise resolving to an array of port objects containing `name` and optional `description` properties. This is useful for building custom UI dialogs or automated connection scripts that need to detect hardware presence before attempting to open a port.