Skip to main content
Glama
layout_controller.go8.28 kB
package tui import ( "fmt" "strings" "github.com/charmbracelet/lipgloss" "github.com/standardbeagle/brummer/internal/logs" "github.com/standardbeagle/brummer/internal/process" "github.com/standardbeagle/brummer/internal/proxy" ) // LayoutController manages the overall TUI layout and core rendering type LayoutController struct { // Dependencies processMgr *process.Manager logStore *logs.Store mcpServer MCPServerInterface proxyServer *proxy.Server version string workingDir string // View state width int height int headerHeight int footerHeight int systemPanelHeight int showHelp bool currentView string systemPanelOpen bool selectedProcess string // Styles (initialized once) headerStyle lipgloss.Style processSelectorStyle lipgloss.Style systemPanelStyle lipgloss.Style helpStyle lipgloss.Style } // NewLayoutController creates a new layout controller func NewLayoutController(processMgr *process.Manager, logStore *logs.Store, mcpServer MCPServerInterface, proxyServer *proxy.Server, version, workingDir string) *LayoutController { lc := &LayoutController{ processMgr: processMgr, logStore: logStore, mcpServer: mcpServer, proxyServer: proxyServer, version: version, workingDir: workingDir, headerHeight: 3, footerHeight: 1, systemPanelHeight: 10, } // Initialize styles lc.initStyles() return lc } // initStyles initializes the lipgloss styles func (lc *LayoutController) initStyles() { lc.headerStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("229")). Background(lipgloss.Color("53")) lc.processSelectorStyle = lipgloss.NewStyle(). Background(lipgloss.Color("235")). Padding(0, 1) lc.systemPanelStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")) lc.helpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")) } // UpdateSize updates the viewport dimensions func (lc *LayoutController) UpdateSize(width, height int) { lc.width = width lc.height = height } // SetShowHelp sets whether to show help func (lc *LayoutController) SetShowHelp(show bool) { lc.showHelp = show } // SetCurrentView sets the current view name func (lc *LayoutController) SetCurrentView(view string) { lc.currentView = view } // SetSystemPanelOpen sets whether the system panel is open func (lc *LayoutController) SetSystemPanelOpen(open bool) { lc.systemPanelOpen = open } // SetSelectedProcess sets the selected process ID func (lc *LayoutController) SetSelectedProcess(processID string) { lc.selectedProcess = processID } // GetHeaderHeight returns the header height func (lc *LayoutController) GetHeaderHeight() int { return lc.headerHeight } // GetFooterHeight returns the footer height func (lc *LayoutController) GetFooterHeight() int { return lc.footerHeight } // GetSystemPanelHeight returns the system panel height func (lc *LayoutController) GetSystemPanelHeight() int { if lc.systemPanelOpen { return lc.systemPanelHeight } return 0 } // Note: RenderHeader is not used directly - the model.go renderHeader method // handles the complex header rendering with notifications and unread indicators // RenderFooter renders the application footer func (lc *LayoutController) RenderFooter() string { helpKeys := []string{ "Tab/←→: Switch View", "↑↓: Navigate", "Enter: Select", "q: Quit", "?: Help", } if lc.currentView == "Processes" { helpKeys = append([]string{"Space: Stop Process"}, helpKeys...) } else if lc.currentView == "Logs" { helpKeys = append([]string{"f: Filter", "c: Clear", "a: Auto-scroll"}, helpKeys...) } help := strings.Join(helpKeys, " • ") // Create footer with top border footerStyle := lc.helpStyle. Width(lc.width). BorderStyle(lipgloss.NormalBorder()). BorderTop(true). BorderBottom(false). BorderLeft(false). BorderRight(false). BorderForeground(lipgloss.Color("240")) return footerStyle.Render(help) } // RenderSystemPanel renders the system panel func (lc *LayoutController) RenderSystemPanel() string { if !lc.systemPanelOpen { return "" } // Get system logs systemLogs := lc.logStore.GetByProcess("system") // Take last few logs that fit in panel maxLogs := lc.systemPanelHeight - 2 // Account for border startIdx := 0 if len(systemLogs) > maxLogs { startIdx = len(systemLogs) - maxLogs } var content strings.Builder for _, log := range systemLogs[startIdx:] { style := lipgloss.NewStyle() if log.IsError { style = style.Foreground(lipgloss.Color("196")) } content.WriteString(style.Render(log.Content)) content.WriteString("\n") } return lc.systemPanelStyle. Width(lc.width - 2). Height(lc.systemPanelHeight). Render(content.String()) } // RenderHelpBar renders the help bar overlay func (lc *LayoutController) RenderHelpBar() string { if !lc.showHelp { return "" } helpContent := `🔨 Brummer Help Navigation: Tab, ←/→ Switch between views ↑/↓ Navigate lists Enter Select item q Quit application ? Toggle this help Process View: Space Stop selected process r Restart stopped process Logs View: f Filter logs c Clear filter a Toggle auto-scroll / Search in logs URLs View: Enter Copy URL to clipboard o Open URL in browser Settings: Enter Install/Copy selected item Web View: ↑/↓ Navigate requests Enter View request details Esc Back to list Press ? to close this help` helpBox := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("62")). Padding(1, 2). Width(60). Height(25). Render(helpContent) // Center the help box return lipgloss.Place(lc.width, lc.height, lipgloss.Center, lipgloss.Center, helpBox) } // RenderProcessSelector renders the process selector for logs view func (lc *LayoutController) RenderProcessSelector() string { processes := lc.processMgr.GetAllProcesses() items := make([]string, 0, len(processes)+1) // Add "All" option allStyle := lc.processSelectorStyle if lc.selectedProcess == "" { allStyle = allStyle.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("229")) } items = append(items, allStyle.Render("All")) // Add process options for _, p := range processes { style := lc.processSelectorStyle if p.ID == lc.selectedProcess { style = style.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("229")) } emoji := "🟢" if p.GetStatus() != process.StatusRunning { emoji = "🔴" } items = append(items, style.Render(fmt.Sprintf("%s %s", emoji, p.Name))) } return lipgloss.JoinHorizontal(lipgloss.Top, items...) } // GetAvailableHeight calculates the available height for content func (lc *LayoutController) GetAvailableHeight() int { totalHeight := lc.height usedHeight := lc.headerHeight + lc.footerHeight if lc.systemPanelOpen { usedHeight += lc.systemPanelHeight } availableHeight := totalHeight - usedHeight if availableHeight < 1 { availableHeight = 1 } return availableHeight } // RenderMainView renders the complete view with layout // Note: This is not currently used as the model.go handles the main layout func (lc *LayoutController) RenderMainView(content string) string { var sections []string // Main content sections = append(sections, content) // System panel (if open) if lc.systemPanelOpen { sections = append(sections, lc.RenderSystemPanel()) } // Footer sections = append(sections, lc.RenderFooter()) // Combine all sections mainView := lipgloss.JoinVertical(lipgloss.Left, sections...) // Overlay help if shown if lc.showHelp { return lc.RenderHelpBar() } return mainView } // UpdateSizes recalculates component sizes based on terminal dimensions func (lc *LayoutController) UpdateSizes(termWidth, termHeight int) { lc.width = termWidth lc.height = termHeight // Adjust system panel height if needed maxSystemPanelHeight := termHeight / 3 if lc.systemPanelHeight > maxSystemPanelHeight { lc.systemPanelHeight = maxSystemPanelHeight } }

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