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

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)

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, 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:

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

File-Panel Manager Layer (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:
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)

Each panel is a self-contained Bubble Tea model defined in 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 manages cursor movement (MoveUp, MoveDown, PageUp, PageDown), sort.go implements sorting algorithms (SortByName, SortBySize), and 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:

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), the panel manager (filepanel/model.go), and individual panel instances (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 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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →