Architecture of the Interactive TUI in witr: How Tab Navigation Works

The witr terminal user interface is built on the Bubble Tea framework and uses a centralized MainModel struct to manage four distinct data views, supporting both keyboard (1-4 keys) and mouse-driven tab navigation.

The witr repository provides a system monitoring tool with an interactive terminal UI. Its architecture cleanly separates state management, rendering, and input handling using the Bubble Tea Elm architecture. This article examines the core components in internal/tui and explains exactly how users switch between Processes, Ports, Containers, and Locks tabs.

Core Architecture of the witr TUI

The TUI implementation resides in the internal/tui package and follows the Model-Update-View pattern required by the Bubble Tea framework.

The MainModel State Container

At the center of the architecture is the MainModel struct defined in internal/tui/model.go. This struct implements the Bubble Tea Model interface and maintains all UI state, including the current view state, active tab, focus state, and data tables for each resource type.

The model defines a tab enumeration that represents the four logical views: tabProcesses, tabPorts, tabContainers, and tabLocks. These constants control which data source is displayed and refreshed.

View Rendering Pipeline

The View() method in internal/tui/view.go acts as the rendering dispatcher. It inspects the current state field and delegates to specialized view functions like viewList(), viewProcessDetail(), or viewContainerDetail().

When in the list state, viewList() assembles the screen by selecting the appropriate table based on activeTab and arranging it within a lipgloss-styled layout. The tab bar itself is rendered as a static string within the outer style, not as a separate interactive component.

Event Handling with Update

The Update() method in internal/tui/update.go processes all Bubble Tea messages, including keyboard input, mouse events, and window resizes. This is where tab navigation logic is implemented. The method handles key presses for tab switching, interprets mouse coordinates for tab bar clicks, and manages focus transitions between the main table and detail panes.

How Tab Navigation Works in witr

Tab navigation in witr supports both keyboard shortcuts and mouse interactions, allowing users to switch between the four monitoring views seamlessly.

Keyboard Navigation Using Number Keys

When the UI is in the list state (stateList) and no search input has focus, pressing the number keys 1 through 4 immediately switches to the corresponding tab. This logic is handled in the handleKey helper within internal/tui/update.go.

case "1": m.activeTab = tabProcesses
case "2": m.activeTab = tabPorts
case "3": m.activeTab = tabContainers
case "4": m.activeTab = tabLocks

After updating m.activeTab, the system queues a refresh command specific to that tab—such as m.refreshPorts() or m.refreshContainers()—to ensure the data is current when the view renders.

Mouse Interaction on the Tab Bar

The TUI interprets clicks on the first line of the terminal (Y-coordinate = 1) as tab selection events. The Update() function checks the X-coordinate of the click against pre-calculated column ranges for each tab label.

if msg.Y == 1 && isClick {
    if msg.X >= 8 && msg.X < 22 {   // "1. Processes"
        m.activeTab = tabProcesses
    } else if msg.X >= 22 && msg.X < 32 { // "2. Ports"
        m.activeTab = tabPorts
    } // ... additional ranges for Containers and Locks
}

A mouse click also resets the focus to the main list by setting m.listFocus = focusMain, ensuring the user can immediately interact with the newly selected tab's data.

Focus Management vs Tab Switching

It is important to distinguish between tab navigation and focus navigation. Pressing the Tab key does not change the active tab; instead, it toggles focus between the main data table and the side or detail pane. This allows users to navigate within a tab's interface without switching data views.

State Transitions and Data Refresh

Changing m.activeTab does not alter the high-level m.state, which remains stateList. The next rendering cycle picks up the new tab value and displays the corresponding table. Each tab maintains its own data-fetching logic, such as refreshProcesses() or refreshLocks(), which is triggered immediately upon tab activation to populate the view with live system data.

Key Source Files

Understanding the witr TUI architecture requires familiarity with these specific files:

Summary

  • The witr TUI is implemented using the Bubble Tea framework in the internal/tui package.
  • Tab navigation is controlled by the activeTab field in MainModel, supporting both keyboard (keys 1-4) and mouse clicks on the first line.
  • The View() method dispatches rendering based on state, while Update() in internal/tui/update.go handles all input events.
  • Pressing Tab changes focus between UI elements but does not switch tabs.
  • Each tab triggers its own refresh command (e.g., refreshPorts()) when activated to ensure data consistency.

Frequently Asked Questions

What framework does witr use for its TUI?

The witr project uses the Bubble Tea framework, which implements the Elm architecture pattern for Go terminal applications. This framework requires a Model struct with Update() and View() methods, which witr implements in the internal/tui package.

How do I switch between tabs using the keyboard in witr?

Press the number keys 1, 2, 3, or 4 when not actively editing a search field. These map directly to Processes, Ports, Containers, and Locks respectively, as implemented in the handleKey function within internal/tui/update.go.

Why doesn't the Tab key change tabs in witr?

In witr, the Tab key toggles focus between the main data table and the side detail pane rather than switching tabs. This design choice allows users to navigate within a tab's interface before switching to a different data view using the number keys or mouse.

How does witr detect which tab I clicked with the mouse?

The TUI checks if the click occurred on line 1 (the tab bar) and compares the X-coordinate against hardcoded ranges: 8-21 for Processes, 22-31 for Ports, and subsequent ranges for Containers and Locks. This logic is defined in internal/tui/update.go alongside the keyboard handling code.

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 →