Skip to main content
Glama
tabs_component.go9.34 kB
package tui import ( "fmt" "github.com/charmbracelet/lipgloss" "github.com/standardbeagle/brummer/internal/process" ) // TabsComponent manages the tab bar rendering using Lipgloss type TabsComponent struct { // Dependencies processMgr *process.Manager notificationsController NotificationsControllerInterface systemController SystemControllerInterface // View configuration views []View debugMode bool // Current state activeView View width int // Styles titleStyle lipgloss.Style activeStyle lipgloss.Style inactiveStyle lipgloss.Style separatorStyle lipgloss.Style tabBarStyle lipgloss.Style } // NewTabsComponent creates a new tabs component func NewTabsComponent(processMgr *process.Manager, notificationsController NotificationsControllerInterface, systemController SystemControllerInterface, debugMode bool) *TabsComponent { tc := &TabsComponent{ processMgr: processMgr, notificationsController: notificationsController, systemController: systemController, debugMode: debugMode, } // Initialize views tc.views = []View{ViewProcesses, ViewLogs, ViewErrors, ViewURLs, ViewWeb, ViewAICoders, ViewSettings} if debugMode { tc.views = append(tc.views, ViewMCPConnections) } // Initialize styles tc.initStyles() return tc } // initStyles initializes all the Lipgloss styles func (tc *TabsComponent) initStyles() { // Title style tc.titleStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("226")) // Tab styles tc.activeStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("226")). Background(lipgloss.Color("235")). Padding(0, 1) tc.inactiveStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("245")). Padding(0, 1) tc.separatorStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("240")) // Tab bar container style with background tc.tabBarStyle = lipgloss.NewStyle(). Background(lipgloss.Color("0")) // Black background for visibility // Width will be set dynamically in Render() // No margins to keep header compact } // SetActiveView updates the active view func (tc *TabsComponent) SetActiveView(view View) { tc.activeView = view } // SetWidth updates the component width func (tc *TabsComponent) SetWidth(width int) { tc.width = width } // Render returns the rendered tab bar func (tc *TabsComponent) Render() string { // Build title section title := tc.renderTitle() // Build tabs section tabs := tc.renderTabs() // Create the header layout using Lipgloss headerContainer := lipgloss.NewStyle(). Width(tc.width) // Join title and tabs directly without extra margins fullHeader := lipgloss.JoinVertical( lipgloss.Left, title, tabs, // Render tabs directly without the tabBarStyle wrapper ) // Apply header container style with bottom border styledHeader := headerContainer. BorderStyle(lipgloss.NormalBorder()). BorderBottom(true). BorderTop(false). BorderLeft(false). BorderRight(false). BorderForeground(lipgloss.Color("240")). Render(fullHeader) return styledHeader } // renderTitle renders the title section with process info and notifications func (tc *TabsComponent) renderTitle() string { // Get process count information processes := tc.processMgr.GetAllProcesses() runningCount := 0 for _, proc := range processes { if proc.GetStatus() == process.StatusRunning { runningCount++ } } // Build title based on available width var baseTitle string var processInfo string if tc.width < 40 { // Ultra narrow - just emoji and counts baseTitle = "🐝" if len(processes) > 0 { processInfo = fmt.Sprintf(" %d/%d", runningCount, len(processes)) } } else if tc.width < 60 { // Narrow - short title baseTitle = "🐝 Brummer" if len(processes) > 0 { processInfo = fmt.Sprintf(" (%d/%d)", runningCount, len(processes)) } } else if tc.width < 100 { // Medium - abbreviated subtitle baseTitle = "🐝 Brummer - Dev Buddy" if len(processes) > 0 { if runningCount > 0 { processInfo = fmt.Sprintf(" (%d proc, %d run)", len(processes), runningCount) } else { processInfo = fmt.Sprintf(" (%d proc)", len(processes)) } } } else { // Full width - complete title baseTitle = "🐝 Brummer - Development Buddy" if len(processes) > 0 { if runningCount > 0 { processInfo = fmt.Sprintf(" (%d processes, %d running)", len(processes), runningCount) } else { processInfo = fmt.Sprintf(" (%d processes)", len(processes)) } } } // Add notification if active (skip for ultra narrow) notification := "" if tc.width >= 40 && tc.notificationsController != nil && tc.notificationsController.IsActive() { notificationStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("82")). Bold(true). MarginLeft(1) notificationText := tc.notificationsController.GetMessage() // Truncate notification for narrow screens if tc.width < 80 && len(notificationText) > 20 { notificationText = notificationText[:17] + "..." } notification = notificationStyle.Render(notificationText) } // Combine title parts titleText := baseTitle + processInfo // Create a container that uses the full width titleContainer := lipgloss.NewStyle(). Width(tc.width) // Render title and notification titleSection := tc.titleStyle.Render(titleText) if notification != "" { titleSection = lipgloss.JoinHorizontal( lipgloss.Top, titleSection, notification, ) } // Ensure the title is visible by not using the container if width is 0 if tc.width <= 0 { return titleSection } return titleContainer.Render(titleSection) } // getTabRenderMode determines how to render tabs based on available width func (tc *TabsComponent) getTabRenderMode() string { if tc.width < 40 { return "minimal" // Just key bindings: 1 2 3... } else if tc.width < 60 { return "abbreviated" // Short names: 1.Proc 2.Log... } else if tc.width < 100 { return "compact" // No icons: 1.Processes 2.Logs... } return "full" // Full with icons } // renderTabs renders the tab bar func (tc *TabsComponent) renderTabs() string { mode := tc.getTabRenderMode() var tabs []string for _, viewType := range tc.views { if cfg, ok := viewConfigs[viewType]; ok { tab := tc.renderTabResponsive(viewType, cfg, mode) tabs = append(tabs, tab) } } // Join tabs with proper spacing return lipgloss.JoinHorizontal( lipgloss.Top, tabs..., ) } // getAbbreviation returns abbreviated form of the title func (tc *TabsComponent) getAbbreviation(title string) string { abbreviations := map[string]string{ "Processes": "Proc", "Logs": "Log", "Errors": "Err", "URLs": "URL", "Web View": "Web", "AI Coders": "AI", "Settings": "Set", "MCP Connections": "MCP", } if abbr, ok := abbreviations[title]; ok { return abbr } // Default: take first 3-4 chars if len(title) > 4 { return title[:4] } return title } // renderTabResponsive renders a tab based on the current render mode func (tc *TabsComponent) renderTabResponsive(viewType View, cfg ViewConfig, mode string) string { var label string // Build label based on mode switch mode { case "minimal": // Just the key binding label = cfg.KeyBinding case "abbreviated": // Key + abbreviated name abbr := tc.getAbbreviation(cfg.Title) label = fmt.Sprintf("%s.%s", cfg.KeyBinding, abbr) case "compact": // Key + full name, no icon label = fmt.Sprintf("%s.%s", cfg.KeyBinding, cfg.Title) default: // "full" // Icon + key + full name label = fmt.Sprintf("%s.%s", cfg.KeyBinding, cfg.Title) if cfg.Icon != "" { label = cfg.Icon + " " + label } } // Get unread indicator (only show in non-minimal modes) indicatorIcon := "" if mode != "minimal" && tc.systemController != nil { indicators := tc.systemController.GetUnreadIndicators() if indicator, exists := indicators[viewType]; exists && indicator.Count > 0 { // Use simpler indicator for narrow screens if mode == "abbreviated" { indicatorIcon = "*" } else { indicatorIcon = " " + indicator.Icon } } } // Render the tab with appropriate style var tabContent string if viewType == tc.activeView { // Active tab with selection indicator (simpler for narrow screens) activeIndicator := "▶ " if mode == "minimal" { activeIndicator = ">" } tabContent = tc.activeStyle.Render(activeIndicator + label + indicatorIcon) } else { // Inactive tab tabContent = tc.inactiveStyle.Render(label + indicatorIcon) } // Add separator (simpler for narrow screens) if viewType != tc.views[len(tc.views)-1] { separatorChar := " │ " if mode == "minimal" { separatorChar = " " } else if mode == "abbreviated" { separatorChar = "|" } separator := tc.separatorStyle.Render(separatorChar) tabContent = lipgloss.JoinHorizontal( lipgloss.Top, tabContent, separator, ) } return tabContent } // renderTab renders a single tab (legacy method, keeping for compatibility) func (tc *TabsComponent) renderTab(viewType View, cfg ViewConfig) string { return tc.renderTabResponsive(viewType, cfg, "full") } // GetHeight returns the height of the rendered tab bar func (tc *TabsComponent) GetHeight() int { // Title (1) + tabs (1) + border (1) = 3 return 3 }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server