# Herdr Workspace Tab Pane Architecture: Immutable Three-Layer Design

> Explore the herdr workspace tab pane architecture. Discover its immutable three-layer design for robust state management and efficient terminal operations. Learn how Herdr separates concerns.

- Repository: [Can Celik/herdr](https://github.com/ogulcancelik/herdr)
- Tags: architecture
- Published: 2026-05-31

---

**Herdr organizes its terminal multiplexer UI into three immutable, testable layers—Workspace containers, Tab-based pane trees, and pure BSP (Binary Space Partitioning) layout engines—that strictly separate state management from terminal runtime operations.**

The `ogulcancelik/herdr` repository implements a Rust-based terminal workspace manager where every UI mutation flows through a predictable hierarchy. Understanding the **herdr workspace tab pane architecture** reveals how the application maintains stable identities, handles dynamic window splits, and keeps layout calculations pure and unit-testable without touching PTY code.

## The Three-Layer Stack

Herdr’s terminal UI follows a strict delegation model across three architectural boundaries:

| Layer | Responsibility | Primary File |
|-------|---------------|--------------|
| **Workspace** | Logical collection of tabs, git metadata caching, and public pane numbering | [`src/workspace.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/workspace.rs) |
| **Tab** | Single pane tree ownership, runtime spawning, and pane state tracking | [`src/workspace/tab.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/workspace/tab.rs) |
| **TileLayout** | Pure BSP geometry calculations, split logic, and focus management | [`src/layout.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/layout.rs) |

Each layer communicates through explicit data structures—`Workspace` holds `Vec<Tab>`, each `Tab` owns one `TileLayout`, and `TileLayout` computes coordinates without side effects.

## Workspace Layer: The Container

The `Workspace` struct in [`src/workspace.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/workspace.rs) serves as the top-level entity that survives terminal session restarts. It tracks identity through a **stable `id`** string and derives display labels from **`identity_cwd`**.

Key responsibilities include:

- **Tab management** – Stores tabs in `pub tabs: Vec<Tab>` and tracks the visible surface via `pub active_tab: usize`
- **Git integration** – Caches repository state in `cached_git_branch`, `cached_git_ahead_behind`, and `cached_git_space` to avoid filesystem thrashing during UI renders
- **Public pane numbering** – Maintains `public_pane_numbers: HashMap<PaneId, usize>` to provide compact, user-facing indices that are recycled when panes close
- **Delegation** – All mutating operations (`create_tab`, `close_tab`, `move_tab`, `split_focused`, `close_focused`) forward heavy lifting to the active `Tab` instance, then update workspace bookkeeping

```rust
pub struct Workspace {
    pub id: String,
    pub custom_name: Option<String>,
    pub identity_cwd: PathBuf,
    pub tabs: Vec<Tab>,
    pub active_tab: usize,
    // Git caches and pane number maps omitted for brevity
}

```

## Tab Layer: The Pane Tree

Each `Tab` in [`src/workspace/tab.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/workspace/tab.rs) represents a distinct layout surface within a workspace. It bridges the immutable BSP geometry with the mutable terminal runtimes attached to each pane.

Critical structures include:

- **`root_pane: PaneId`** – The entry point into the binary space partition tree
- **`layout: TileLayout`** – The pure BSP engine that determines where panes appear on screen
- **`panes: HashMap<PaneId, PaneState>`** – Maps geometric pane IDs to runtime state (terminal ID, scrollback, visibility)
- **Runtime isolation** – Production code accesses the global `TerminalRuntimeRegistry`, while test code may use an internal `runtimes: HashMap<PaneId, TerminalRuntime>`

The `Tab` handles concrete user actions like `split_focused()`, `close_focused()`, and `split_focused_command()`, translating them into `TileLayout` mutations and spawning new `TerminalRuntime` instances via `TerminalRuntime::spawn*`.

```rust
pub struct Tab {
    pub custom_name: Option<String>,
    pub number: usize,
    pub root_pane: PaneId,
    pub layout: TileLayout,
    pub panes: HashMap<PaneId, PaneState>,
    pub zoomed: bool,
    // Event channels and render notifications omitted
}

```

## Layout Layer: Pure BSP Geometry

`TileLayout` in [`src/layout.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/layout.rs) embodies Herdr’s **state-is-separated-from-runtime** principle. It is a pure data structure containing a `Node` tree (either `Pane` or `Split`) and a `focus: PaneId` pointer.

All geometry calculations—`panes(area)` and `splits(area)`—are pure functions returning `PaneInfo` and `SplitBorder` objects without IO. Mutations like `split_focused()`, `close_focused()`, and `resize_focused()` only adjust the internal tree structure and focus pointer, making the layout engine fully unit-testable without mocking PTY devices.

```rust
pub struct TileLayout {
    root: Node,
    focus: PaneId,
}

```

## Interaction Flows

### Creating a Workspace

When the CLI initializes a session in [`src/app/creation.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/app/creation.rs), it calls `Workspace::new()`, which constructs the first tab via `Tab::new()` and returns a tuple of workspace state, terminal state, and the async runtime handle.

```rust
let (ws, term_state, runtime) = Workspace::new(
    std::env::current_dir()?,
    rows,
    cols,
    scrollback_limit_bytes,
    theme,
    shell_cfg,
    app_events,
    render_notify,
    render_dirty,
)?;

```

### Splitting a Pane

Horizontal or vertical splits flow through the workspace into the tab’s layout engine:

```rust
let new_pane = workspace.split_focused(
    Direction::Horizontal,
    rows,
    cols,
    None, // Use current cwd
    scrollback_limit_bytes,
    theme,
    shell_cfg,
)?;
workspace.register_new_pane(new_pane.pane_id);

```

`Workspace::split_focused` delegates to `Tab::split_focused`, which calls `layout.split_focused()` to mutate the BSP tree and inserts a new `PaneState` into the `panes` map. The workspace then registers the compact public number.

### Closing and Cleanup

When closing the focused pane, `Workspace::close_focused()` determines whether to destroy the entire tab or just prune one pane:

```rust
if workspace.close_focused() {
    // Workspace became empty; UI can exit or delete the workspace
}

```

If panes remain, `Tab::close_focused()` removes the entry from both `layout` and the `panes` map, returning a `DetachedPane` containing the terminal ID for runtime cleanup.

### Switching Tabs

`Workspace::switch_tab(idx)` updates `active_tab` and iterates through the newly visible tab’s `panes`, marking each `seen = true` so the UI can highlight new content.

## Key Files and Responsibilities

- **[`src/workspace.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/workspace.rs)** – Workspace orchestration, tab lifecycle management, public pane numbering compaction, and git status caching
- **[`src/workspace/tab.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/workspace/tab.rs)** – Pane tree manipulation, runtime spawning, split/close operations, and pane-to-terminal mapping
- **[`src/layout.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/layout.rs)** – Immutable BSP engine (`TileLayout`), geometry calculation, and split/resizing logic without side effects
- **[`src/pane.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/pane.rs)** – `PaneState` definition, storing terminal IDs and visibility flags
- **[`src/terminal/runtime.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/terminal/runtime.rs)** – Async process management and PTY spawning used indirectly by `Tab`
- **[`src/app/creation.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/app/creation.rs)** – High-level CLI entry point for workspace instantiation

## Summary

- **Herdr uses three strict layers**—`Workspace` (container), `Tab` (tree), and `TileLayout` (geometry)—to separate concerns.
- **Workspaces own identity and metadata**, including stable IDs, git caches, and compact public pane numbers.
- **Tabs manage runtime state**, mapping geometric `PaneId`s to `TerminalId`s while delegating layout to pure BSP logic.
- **Layout is pure and testable**; `TileLayout` in [`src/layout.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/layout.rs) calculates geometry without PTY dependencies.
- **All mutations delegate downward**; workspaces update bookkeeping only after tabs confirm successful layout changes.

## Frequently Asked Questions

### How does Herdr keep its layout calculations testable?

`TileLayout` in [`src/layout.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/layout.rs) is a pure data structure with no async code or OS dependencies. All geometry functions like `panes(area)` and `splits(area)` return immutable structs (`PaneInfo`, `SplitBorder`), allowing unit tests to verify BSP behavior without spawning real terminals.

### What is the purpose of public pane numbers?

The `public_pane_numbers` map in `Workspace` provides stable, user-facing indices (1, 2, 3) that remain compact even when internal `PaneId`s are UUIDs or large integers. When `close_focused()` removes a pane, the workspace renumbers remaining public indices to avoid gaps.

### What happens when the last pane in a tab closes?

`Workspace::close_focused()` checks the pane count before delegating. If the active tab would become empty, it triggers `close_active_tab_and_report()`, which may destroy the entire workspace if no tabs remain, signaling the UI to exit or switch contexts.

### How does Herdr separate state from terminal runtime?

The architecture isolates PTY code in `TerminalRuntime` (spawned in [`src/terminal/runtime.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/terminal/runtime.rs)) while keeping `Workspace`, `Tab`, and `TileLayout` as plain data structures. A `Tab` tracks pane state in a `HashMap<PaneId, PaneState>` but only holds `TerminalRuntime` references in test configurations; production code uses the global registry, ensuring layout logic never blocks on IO.