# How Superfile's Multi-Panel File Manager Architecture Works Internally

> Explore Superfile's multi-panel file manager architecture. Discover its three-layer hierarchy, independent panel management, and Bubble Tea TUI framework integration for efficient navigation and rendering.

- Repository: [Yorukot/superfile](https://github.com/yorukot/superfile)
- Tags: internals
- Published: 2026-07-26

---

**Superfile implements its multi-panel interface through a three-layer hierarchy built on the Bubble Tea TUI framework, where a top-level application model delegates to a file-panel manager that maintains a slice of independent panel instances, each handling their own navigation, selection, and rendering.**

Superfile is a modern terminal file manager developed by `yorukot` that provides a multi-panel interface similar to dual-pane file managers but with support for dynamic splitting and resizing. The implementation of superfile's multi-panel file manager architecture relies on the Bubble Tea framework's Model-Update-View pattern to coordinate state across independent file panels. This design separates concerns between application-level orchestration, panel management logistics, and individual directory view state.

## The Three-Layer Architectural Stack

Superfile divides its UI architecture into three distinct layers, each with specific responsibilities for managing the multi-panel experience.

### Application Model Layer ([`src/internal/model.go`](https://github.com/yorukot/superfile/blob/main/src/internal/model.go))

The top-level `model` struct serves as the central coordinator for the entire application. It holds a `fileModel` field (`m.fileModel`) that represents the complete set of file panels, alongside other UI components like the sidebar and help menu.

In [`model.go`](https://github.com/yorukot/superfile/blob/main/model.go), the `Update(msg tea.Msg)` method receives all Bubble Tea messages and routes them appropriately. For panel-specific actions, it calls `m.updateComponentState(msg)`, which delegates to the currently focused file panel. The `handleKeyInput` function processes global keybindings—including *split*, *close*, and *focus next/previous* actions—and invokes methods like `m.splitPanel()` when users create new panels.

When splitting occurs, the application model delegates creation to the panel manager:

```go
func (m *model) splitPanel() (tea.Cmd, error) {
    return m.fileModel.CreateNewFilePanel(m.getFocusedFilePanel().Location)
}

```

### File-Panel Manager Layer ([`src/internal/ui/filepanel/model.go`](https://github.com/yorukot/superfile/blob/main/src/internal/ui/filepanel/model.go))

The `fileModel` struct acts as the controller for all file panels, maintaining a slice `FilePanels []filepanel.Model` that holds each panel instance. This layer handles the lifecycle and layout of panels without managing their internal directory state.

Key responsibilities include:

- **Dynamic creation**: `CreateNewFilePanel(dir string)` inserts a new panel into the slice, updates `FocusedPanelIndex`, and triggers layout recomposition.
- **Responsive sizing**: `SetDimensions` divides available terminal width by `PanelCount` and enforces a minimum width (`filepanel.MinWidth`) for usability.
- **Horizontal composition**: The `Render()` method stitches individual panel outputs side-by-side using **Lip-gloss**:

```go
func (fm *fileModel) Render() string {
    var parts []string
    for _, p := range fm.FilePanels {
        parts = append(parts, p.Render())
    }
    return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
}

```

### Individual File Panel Layer ([`src/internal/ui/filepanel/types.go`](https://github.com/yorukot/superfile/blob/main/src/internal/ui/filepanel/types.go))

Each panel is a self-contained Bubble Tea model defined in [`types.go`](https://github.com/yorukot/superfile/blob/main/types.go) as `type Model struct`. This layer encapsulates all state for viewing a single directory, including:

- **Directory state**: `Location string` stores the current path, while `Elements []Element` holds the file list.
- **Navigation state**: Cursor position, render index for scrolling, and search bar state (`textinput.Model`).
- **Interaction mode**: `PanelMode` distinguishes between `BrowserMode` (normal navigation) and `SelectMode` (multi-select operations).

Individual panels handle their own logic through separate modules: [`navigation.go`](https://github.com/yorukot/superfile/blob/main/navigation.go) manages cursor movement (`MoveUp`, `MoveDown`, `PageUp`, `PageDown`), [`sort.go`](https://github.com/yorukot/superfile/blob/main/sort.go) implements sorting algorithms (`SortByName`, `SortBySize`), and [`render.go`](https://github.com/yorukot/superfile/blob/main/render.go) handles the visual output for that specific panel.

## Panel Interaction and Message Flow

Understanding how these layers communicate reveals the dynamic behavior of the multi-panel system.

### Focus Management

The `fileModel` tracks `FocusedPanelIndex` to determine which panel receives keyboard input. When users press the *focus next* hotkey, the application model executes:

```go
func (m *model) focusNextPanel() tea.Cmd {
    if m.fileModel.PanelCount() == 0 {
        return nil
    }
    m.fileModel.FocusedPanelIndex = (m.fileModel.FocusedPanelIndex + 1) % m.fileModel.PanelCount()
    return nil
}

```

This index determines message routing in the `Update` loop—only the focused panel processes navigation keys, while others remain static.

### Synchronization and Updates

After processing any message, the top-level model calls `m.fileModel.UpdateFilePanelsIfNeeded(false)` to refresh panel contents. This ensures that directory changes, file operations, or external modifications propagate to the correct panels while maintaining each panel's independent scroll position and selection state.

### Responsive Layout on Resize

When terminal dimensions change, `handleWindowResize` triggers `fileModel.SetDimensions`, which recalculates each panel's width. The algorithm distributes available space evenly among active panels while respecting minimum width constraints, ensuring the interface remains usable even with multiple splits.

## Summary

- **Three-layer architecture**: Superfile separates concerns into the application model ([`model.go`](https://github.com/yorukot/superfile/blob/main/model.go)), the panel manager ([`filepanel/model.go`](https://github.com/yorukot/superfile/blob/main/filepanel/model.go)), and individual panel instances ([`filepanel/types.go`](https://github.com/yorukot/superfile/blob/main/filepanel/types.go)).
- **Slice-based management**: Panels are stored in `FilePanels []filepanel.Model`, allowing dynamic insertion and removal via `CreateNewFilePanel` and related methods.
- **Horizontal composition**: The manager uses `lipgloss.JoinHorizontal` to render panels side-by-side, with `SetDimensions` handling responsive width calculations.
- **Focus tracking**: `FocusedPanelIndex` determines message routing, enabling keyboard navigation to affect only the active panel.
- **Independent state**: Each panel maintains its own directory listing, cursor position, search filter, and selection set while the manager coordinates global layout.

## Frequently Asked Questions

### How does superfile handle window resizing with multiple panels open?

When the terminal resizes, the `handleWindowResize` function in [`src/internal/model.go`](https://github.com/yorukot/superfile/blob/main/src/internal/model.go) triggers `fileModel.SetDimensions`. This method divides the available width by the current `PanelCount` and assigns proportional widths to each panel while enforcing a minimum width constraint (`filepanel.MinWidth`). The layout recalculates automatically, and the next render cycle displays the adjusted panels.

### What is the maximum number of panels supported in superfile?

The architecture uses a dynamic slice (`FilePanels []filepanel.Model`) rather than a fixed array, so the theoretical limit depends on terminal width and the minimum panel width enforcement. In practice, the usable limit is determined by the `SetDimensions` logic, which prevents panels from becoming narrower than the configured minimum.

### How does focus management work between panels?

Superfile tracks the active panel using `FocusedPanelIndex` in the `fileModel` struct. When users trigger focus change commands (via hotkeys like *focus next* or *focus previous*), the `focusNextPanel` method updates this index using modulo arithmetic to wrap around the panel count. The `Update` method in [`src/internal/model.go`](https://github.com/yorukot/superfile/blob/main/src/internal/model.go) uses this index to delegate keyboard messages only to the focused panel while updating others only during synchronization cycles.

### How does superfile keep directory contents synchronized across panels?

Each panel maintains its own `Elements []Element` slice representing directory contents. The top-level model calls `UpdateFilePanelsIfNeeded(false)` after message processing, which allows each panel to refresh its file listing independently. This ensures that panels showing the same directory remain synchronized with the filesystem while preserving individual scroll positions and selection states.