Herdr Workspace Tab Pane Architecture: Immutable Three-Layer Design
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 |
| Tab | Single pane tree ownership, runtime spawning, and pane state tracking | src/workspace/tab.rs |
| TileLayout | Pure BSP geometry calculations, split logic, and focus management | 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 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 viapub active_tab: usize - Git integration – Caches repository state in
cached_git_branch,cached_git_ahead_behind, andcached_git_spaceto 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 activeTabinstance, then update workspace bookkeeping
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 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 treelayout: TileLayout– The pure BSP engine that determines where panes appear on screenpanes: 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 internalruntimes: 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*.
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 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.
pub struct TileLayout {
root: Node,
focus: PaneId,
}
Interaction Flows
Creating a Workspace
When the CLI initializes a session in 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.
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:
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:
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– Workspace orchestration, tab lifecycle management, public pane numbering compaction, and git status cachingsrc/workspace/tab.rs– Pane tree manipulation, runtime spawning, split/close operations, and pane-to-terminal mappingsrc/layout.rs– Immutable BSP engine (TileLayout), geometry calculation, and split/resizing logic without side effectssrc/pane.rs–PaneStatedefinition, storing terminal IDs and visibility flagssrc/terminal/runtime.rs– Async process management and PTY spawning used indirectly byTabsrc/app/creation.rs– High-level CLI entry point for workspace instantiation
Summary
- Herdr uses three strict layers—
Workspace(container),Tab(tree), andTileLayout(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
PaneIds toTerminalIds while delegating layout to pure BSP logic. - Layout is pure and testable;
TileLayoutinsrc/layout.rscalculates 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 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 PaneIds 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) 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →