# How Tabby Implements Split Panes and Tab Management: A Deep Dive into the Tree Model

> Explore how Tabby uses a recursive tree model for split panes and tab management. Learn about its Angular components, drop zones, and layout recovery system for an efficient terminal experience.

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

---

**Tabby implements split panes and tab management using a recursive tree data structure called `SplitContainer` that hierarchically organizes tabs and nested containers, with Angular components rendering draggable spanners, drop zones, and handling layout persistence through the `TabRecovery` system.**

The open-source terminal emulator [Tabby](https://github.com/Eugeny/tabby) (formerly Terminus) provides a flexible interface for arranging terminal sessions in complex layouts. Unlike simple grid systems, Tabby's approach to **split panes and tab management** uses a lightweight tree model to represent pane hierarchies, enabling dynamic splitting, dragging, and resizing while maintaining state across sessions. This architecture is implemented primarily within the `tabby-core` package and centers on the relationship between data structures and their Angular component representations.

## The Recursive Tree Architecture

At the heart of Tabby's system lies a recursive tree model that treats every split as a node containing either leaf tabs or additional split containers.

### SplitContainer Data Structure

The `SplitContainer` class, defined in [[`tabby-core/src/components/splitTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/components/splitTab.component.ts)](https://github.com/Eugeny/tabby/blob/master/tabby-core/src/components/splitTab.component.ts#L13-L30), serves as the foundational data structure. Each container maintains:

- **`orientation`**: A character flag where `'h'` indicates horizontal splitting (side-by-side) and `'v'` indicates vertical splitting (stacked)
- **`children`**: An ordered array containing either `BaseTabComponent` instances (leaf nodes) or nested `SplitContainer` objects
- **`ratios`**: A parallel array storing each child's size as a fractional proportion of the total container space

This design allows arbitrarily nested layouts where a horizontal split might contain vertical subdivisions, creating complex grid-like arrangements from a simple binary tree structure.

### Rendering with SplitTabComponent

The `SplitTabComponent` acts as the root Angular controller for the entire tree. Implemented in the same file (lines 61-115), it owns the root `SplitContainer` and manages the translation from data model to DOM elements. Key responsibilities include:

- **Geometry calculation**: The `layoutInternal` method (lines 90-128) recursively walks the tree to compute absolute pixel dimensions for each pane based on the relative ratios
- **Focus management**: Tracks the currently active leaf tab (`focusedTab`) and updates the window title via `updateTitle()`
- **Hotkey registration**: Maps keyboard shortcuts for split directions (right, bottom, top, left) to the `splitTab()` method during construction

### UI Components: Spanners and Drop Zones

Two specialized Angular components handle user interaction with the tree structure:

**`SplitTabSpannerComponent`** ([[`splitTabSpanner.component.ts`](https://github.com/Eugeny/tabby/blob/main/splitTabSpanner.component.ts)](https://github.com/Eugeny/tabby/blob/master/tabby-core/src/components/splitTabSpanner.component.ts#L27-L66)): Renders the draggable divider bars between panes. During drag operations, it calculates delta movements in screen space and translates these into ratio adjustments for the adjacent siblings, emitting change events that trigger layout recalculation.

**`SplitTabDropZoneComponent`**: Creates invisible rectangular targets around each pane and splitter. Instantiated within `layoutInternal` (lines 95-119), these zones accept dragged tabs and invoke `SplitTabComponent.add()` to handle insertion logic.

## Adding and Splitting Tabs Programmatically

Tabby provides both user-driven and API-level methods for manipulating the pane tree, with all operations ultimately flowing through the `SplitTabComponent` class.

### The splitTab Method

When users trigger a split action via keyboard shortcut, the `splitTab()` method (lines 87-102) executes the following sequence:

```typescript
async splitTab(tab: BaseTabComponent, dir: SplitDirection): Promise<BaseTabComponent|null> {
    const newTab = await this.tabsService.duplicate(tab)   // Clone via recovery token
    if (newTab) {
        await this.addTab(newTab, tab, dir)               // Insert relative to source
    }
    return newTab
}

```

This approach leverages `TabsService.duplicate()` to create a functionally identical tab instance before positioning it within the tree.

### The add Method and Tree Insertion

The `add()` method (lines 54-118) contains the core insertion logic for the split panes and tab management system:

1. **Orientation resolution**: If the target container's orientation doesn't match the requested split direction, the method inserts a new intermediate `SplitContainer` to accommodate the layout change (lines 95-107)
2. **Ratio adjustment**: Scales existing sibling ratios proportionally to make room for the new pane, then inserts a new ratio value and splices the child into the `children` array (lines 109-119)
3. **View attachment**: Creates Angular component references for new leaf tabs via `attachTabView` and renders them into the calculated rectangular regions (lines 122-128)
4. **Tree normalization**: Calls `normalize()` to collapse empty containers and flatten single-child nodes, ensuring the tree remains minimal (line 127)

To programmatically split the currently focused pane to the right:

```typescript
async function splitRight(currentSplit: SplitTabComponent) {
    const focused = currentSplit.getFocusedTab()
    if (!focused) return
    // Duplicate the tab and insert it on the right side of the focused pane
    await currentSplit.splitTab(focused, 'r')
}

```

To add a brand-new terminal tab next to an existing one:

```typescript
import { TerminalTabComponent } from './terminalTab.component';

async function addTerminalNextTo(tab: BaseTabComponent, direction: SplitDirection) {
    const newTab = tabsService.create<TerminalTabComponent>({
        type: TerminalTabComponent,
        inputs: { title: 'New terminal' }
    })
    await splitTab.addTab(newTab, tab, direction)   // direction = 'b' | 't' | 'l' | 'r'
}

```

### Normalization and Equalization

The `SplitContainer.normalize()` method (lines 52-84) maintains tree integrity by removing empty containers, flattening nested containers with single children, and re-balancing the `ratios` array when structural changes occur. For user-initiated layout resets, `root.equalize()` (lines 90-96) forces all direct siblings to share equal proportional space by resetting their ratios to uniform values.

## Removing Tabs and Container Cleanup

The `removeTab()` method (lines 30-44) handles tab deletion by locating the tab within its parent container's `children` array, removing the associated ratio, redistributing the freed space among remaining siblings, and destroying the Angular view reference. When the final pane is removed, the entire `SplitTabComponent` destroys itself (lines 45-48), ensuring no orphaned containers persist in memory.

## Resizing Panes via Mouse and Keyboard

Tabby implements dual input mechanisms for adjusting pane dimensions within the split panes and tab management framework.

### Mouse Drag with SplitTabSpannerComponent

The spanner component captures mouse deltas during drag operations and converts screen-space movements into relative ratio adjustments. When the user releases the divider, it emits a `change` event containing the new ratio values for the two adjacent panes (lines 57-64), which `SplitTabComponent` applies through the `layout()` recalculation cycle.

### Keyboard Navigation with resizePane

For keyboard-driven workflows, the `resizePane()` method (lines 70-86) traverses upward through the tree hierarchy to locate a container with matching orientation. Once found, it adjusts the focused pane's ratio by adding or subtracting the configured `paneResizeStep` value:

```typescript
// Increase width of focused pane
splitTab.resizePane('h')   // horizontal step +

// Decrease width of focused pane  
splitTab.resizePane('dh')  // decrease horizontal step

```

Both mechanisms ultimately invoke `layout()` to synchronize the DOM with the updated tree state.

## Tab Lifecycle and Factory Services

Beyond the visual tree, Tabby's split panes and tab management relies on robust factory services for component instantiation and lifecycle management.

### TabsService Factory

Located in [[`tabby-core/src/services/tabs.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/tabs.service.ts)](https://github.com/Eugeny/tabby/blob/master/tabby-core/src/services/tabs.service.ts), this singleton handles tab creation through two primary methods:

- **`create()`** (lines 33-43): Resolves the Angular component factory for a given tab type, instantiates it, assigns input properties, and subscribes to the tab's `destroyed$` observable to manage cleanup
- **`duplicate()`** (lines 45-56): Generates a recovery token from the source tab, clones its configuration, and invokes `create()` to produce an identical instance

This factory pattern ensures that split operations always work with properly initialized tab components regardless of the underlying tab type (terminal, SSH, serial, etc.).

### BaseTabComponent Abstraction

All leaf nodes in the split tree extend `BaseTabComponent` ([[`baseTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/baseTab.component.ts)](https://github.com/Eugeny/tabby/blob/master/tabby-core/src/components/baseTab.component.ts)), which provides **focus streams** (`focused$` and `blurred$`), activity indicators, and standardized lifecycle hooks (`canClose` checks). By enforcing this interface, the `SplitTabComponent` can treat heterogeneous tab types uniformly within the tree structure.

### Focus and Navigation

Keyboard navigation between panes uses geometric calculations rather than tree traversal. The `navigate()` and `navigateLinear()` methods (lines 78-88) compute the screen rectangle of each visible pane (`getPaneRect`) and determine the nearest neighbor in the requested cardinal direction, allowing intuitive focus movement regardless of the underlying tree depth.

## Persistence and Session Recovery

The split layout achieves serializability through the `TabRecovery` system. `SplitContainer.serialize()` (lines 9-23) recursively walks the tree, recording each node's `type`, `orientation`, `ratios`, and child recovery tokens. During session restoration, `SplitTabComponent.recoverContainer()` (lines 30-58) reconstructs the tree by recursively invoking `TabRecoveryService` to reinstantiate saved tabs.

To persist and restore a layout:

```typescript
// Save layout
const token = await splitTab.getRecoveryToken()
localStorage.setItem('myLayout', JSON.stringify(token))

// Later – restore
const saved = JSON.parse(localStorage.getItem('myLayout')!)
await splitTab.recoverContainer(splitTab.root, saved)
splitTab.layout()

```

## Summary

- **Tree Model**: Tabby uses a recursive `SplitContainer` structure with `orientation`, `children`, and `ratios` properties to represent arbitrary nested split layouts in [`splitTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/splitTab.component.ts)
- **Rendering Pipeline**: `SplitTabComponent` translates the tree into DOM elements using `layoutInternal`, while `SplitTabSpannerComponent` and drop zones handle user interactions
- **Tab Operations**: The `splitTab()` and `add()` methods handle insertion with automatic orientation matching and ratio balancing, while `removeTab()` manages cleanup and container collapse via `normalize()`
- **Factory Services**: `TabsService` creates and duplicates tabs via recovery tokens, ensuring type-agnostic handling within the split tree
- **Persistence**: Layouts serialize through `SplitContainer.serialize()` and restore via `recoverContainer()`, maintaining complex pane arrangements across sessions

## Frequently Asked Questions

### How does Tabby store the size of each split pane?

Tabby stores pane dimensions within the `ratios` array of each `SplitContainer` node. These values represent fractional proportions (0.0 to 1.0) of the parent container's total available space. When the user drags a spanner or triggers a keyboard resize command, the system modifies these ratios and recursively recalculates absolute pixel dimensions through the `layout()` method in [`splitTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/splitTab.component.ts).

### Can I programmatically create a split layout without using the UI?

Yes. You can manipulate the split panes and tab management system directly through the `SplitTabComponent` API. First create a tab using `TabsService.create()`, then insert it relative to an existing tab using `splitTab.addTab(newTab, existingTab, direction)` where direction accepts `'r'`, `'l'`, `'t'`, or `'b'` for right, left, top, and bottom splits respectively. Finally, call `layout()` to render the changes.

### What happens when I close the last tab in a split container?

When `removeTab()` detects that no children remain in the root container, it triggers the destruction of the entire `SplitTabComponent` instance (lines 45-48 in [`splitTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/splitTab.component.ts)). The `normalize()` method automatically cleans up intermediate containers that become empty during tab removal, ensuring the tree structure remains minimal and memory is properly released through Angular's component lifecycle hooks.

### How does Tabby remember my split layout after restarting?

Tabby serializes the entire split tree using `SplitContainer.serialize()`, which captures the orientation, ratios, and recovery tokens for each tab. This data is stored through the `TabRecoveryService`. On application launch, `SplitTabComponent.recoverContainer()` reconstructs the tree by deserializing the saved structure and recreating each tab via its stored recovery token, then applies the saved ratios to restore the exact geometric layout.