Skip to main content
Glama
script_selector.go9.53 kB
package tui import ( "fmt" "sort" "strings" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" "github.com/standardbeagle/brummer/internal/process" ) // NewScriptSelectorAutocomplete creates an autocomplete for initial script selection func NewScriptSelectorAutocomplete(scripts map[string]string) CommandAutocomplete { ti := textinput.New() ti.Placeholder = "Type to search scripts..." ti.Prompt = "" ti.Focus() ti.CharLimit = 100 c := CommandAutocomplete{ input: ti, availableScripts: scripts, showDropdown: true, } // Initialize with all scripts as suggestions c.updateScriptSelectorSuggestions() return c } // NewScriptSelectorAutocompleteWithProcessManager creates a script selector with process manager func NewScriptSelectorAutocompleteWithProcessManager(scripts map[string]string, processMgr *process.Manager) CommandAutocomplete { c := NewScriptSelectorAutocomplete(scripts) c.processMgr = processMgr // Re-update suggestions with process manager filter c.updateScriptSelectorSuggestions() return c } // updateScriptSelectorSuggestions updates suggestions for script selector mode func (c *CommandAutocomplete) updateScriptSelectorSuggestions() { value := strings.ToLower(c.input.Value()) // Get running scripts if process manager is available runningScripts := make(map[string]bool) if c.processMgr != nil { for _, proc := range c.processMgr.GetAllProcesses() { if proc.GetStatus() == process.StatusRunning { runningScripts[proc.Name] = true } } } // Get all scripts that aren't already running scripts := make([]string, 0, len(c.availableScripts)) for name := range c.availableScripts { if !runningScripts[name] { scripts = append(scripts, name) } } sort.Strings(scripts) // Filter based on input if value == "" { c.suggestions = scripts } else { c.suggestions = []string{} for _, script := range scripts { if strings.Contains(strings.ToLower(script), value) { c.suggestions = append(c.suggestions, script) } } } c.selected = 0 c.showDropdown = len(c.suggestions) > 0 } // RenderScriptSelector renders the script selector view func (m *Model) renderScriptSelector() string { // Adaptive sizing based on terminal size var containerWidth, containerHeight int var showSkipSection bool var padding int // Determine container size based on terminal dimensions if m.width < 40 { containerWidth = m.width - 4 padding = 1 } else if m.width < 80 { containerWidth = min(70, m.width-8) padding = 2 } else { containerWidth = 80 padding = 2 } if m.height < 12 { containerHeight = m.height - 2 showSkipSection = false } else if m.height < 20 { containerHeight = min(15, m.height-2) showSkipSection = true } else { containerHeight = 18 // Much more compact max height showSkipSection = true } // Create a centered container with adaptive sizing containerStyle := lipgloss.NewStyle(). Width(containerWidth). Height(containerHeight). Padding(padding, padding). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("226")) // Calculate content width contentWidth := containerWidth - (2*padding + 2) // Account for padding and border // Title titleStyle := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("226")). MarginBottom(1). // Reduced from 2 Width(contentWidth). Align(lipgloss.Center) var title string if m.height < 12 { title = titleStyle.Render("🐝 Select Script") } else { title = titleStyle.Render("🐝 Brummer - Select a Script to Run") } // Skip scripts section (conditional) var skipSection string if showSkipSection { skipStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("86")). Bold(true). MarginBottom(0). // Reduced from 1 Width(contentWidth). Align(lipgloss.Center) if m.scriptSelectorController.GetScriptSelector().arbitraryMode { if containerWidth < 60 { skipSection = skipStyle.Render("🚀 Arbitrary Command Mode") } else { skipSection = skipStyle.Render("🚀 Arbitrary Command Mode - Type any command to run") } } else { if containerWidth < 60 { skipSection = skipStyle.Render("💡 Ctrl+S: skip • Ctrl+N: arbitrary • /: commands") } else { skipSection = skipStyle.Render("💡 Skip Scripts: Ctrl+S to skip, Ctrl+N for arbitrary commands, or / for command palette") } } } // Instructions instructionStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("245")). MarginBottom(1). // Reduced from 2 Width(contentWidth). Align(lipgloss.Center) var instructions string if m.scriptSelectorController.GetScriptSelector().arbitraryMode { if containerWidth < 50 { instructions = instructionStyle.Render("Enter: run command • Esc: exit") } else { instructions = instructionStyle.Render("Type any command (e.g., 'ls', 'node server.js') • Enter to run • Esc to exit") } } else { if containerWidth < 50 { instructions = instructionStyle.Render("↑↓ Navigate • Enter: run • Esc: exit") } else { instructions = instructionStyle.Render("Type script name or ↑↓ Navigate • Enter to run script • Esc/Ctrl+C to exit") } } // Input field inputStyle := lipgloss.NewStyle(). Width(contentWidth). MarginBottom(1) inputView := inputStyle.Render(m.scriptSelectorController.GetScriptSelector().View()) // Dropdown suggestions with proper width (hide in arbitrary mode) var dropdownView string if !m.scriptSelectorController.GetScriptSelector().arbitraryMode { dropdownView = m.scriptSelectorController.GetScriptSelector().RenderScriptSelectorDropdownWithWidth(6, contentWidth) // Reduced from 10 to 6 } // Error message if any errorMsg := m.scriptSelectorController.GetScriptSelector().GetErrorMessage() errorView := "" if errorMsg != "" { errorStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("196")). Bold(true). MarginTop(1) errorView = errorStyle.Render("⚠ " + errorMsg) } // Available scripts info removed to save space // Combine all elements var contentParts []string contentParts = append(contentParts, title) if showSkipSection && skipSection != "" { contentParts = append(contentParts, skipSection) } contentParts = append(contentParts, instructions) contentParts = append(contentParts, inputView) if dropdownView != "" { contentParts = append(contentParts, dropdownView) } if errorView != "" { contentParts = append(contentParts, errorView) } // Script count section removed to save vertical space content := lipgloss.JoinVertical(lipgloss.Left, contentParts...) container := containerStyle.Render(content) // Center the container on screen return lipgloss.Place( m.width, m.height, lipgloss.Center, lipgloss.Center, container, ) } // RenderScriptSelectorDropdown renders the dropdown for script selector func (c CommandAutocomplete) RenderScriptSelectorDropdown(maxSuggestions int) string { return c.RenderScriptSelectorDropdownWithWidth(maxSuggestions, c.width) } // RenderScriptSelectorDropdownWithWidth renders the dropdown with specific width func (c CommandAutocomplete) RenderScriptSelectorDropdownWithWidth(maxSuggestions int, containerWidth int) string { if !c.showDropdown || len(c.suggestions) == 0 { return "" } var s strings.Builder // Use dynamic width based on container, with sensible limits dropdownWidth := containerWidth if dropdownWidth < 20 { dropdownWidth = 20 } if dropdownWidth > 80 { dropdownWidth = 80 } selectedStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("226")). Background(lipgloss.Color("237")). Width(dropdownWidth). Padding(0, 1) normalStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("252")). Width(dropdownWidth). Padding(0, 1) scriptDescStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("242")). Italic(true) count := len(c.suggestions) if count > maxSuggestions { count = maxSuggestions } for i := 0; i < count; i++ { scriptName := c.suggestions[i] scriptCmd := c.availableScripts[scriptName] // Calculate available space for script command prefixLength := 3 // "▶ " or " " nameLength := len(scriptName) paddingLength := 4 // padding (2 chars each side) spacerLength := 2 // space between name and command availableForCmd := dropdownWidth - prefixLength - nameLength - paddingLength - spacerLength if availableForCmd < 0 { availableForCmd = 0 } // Truncate command if necessary if len(scriptCmd) > availableForCmd && availableForCmd > 3 { scriptCmd = scriptCmd[:availableForCmd-3] + "..." } else if len(scriptCmd) > availableForCmd { scriptCmd = "" // Hide command if no space } // Format the display without fixed width formatting var display string if scriptCmd != "" { display = fmt.Sprintf("%s %s", scriptName, scriptDescStyle.Render(scriptCmd)) } else { display = scriptName } if i == c.selected { s.WriteString(selectedStyle.Render("▶ " + display)) } else { s.WriteString(normalStyle.Render(" " + display)) } if i < count-1 { s.WriteString("\n") } } // Show more indicator if there are more suggestions if len(c.suggestions) > maxSuggestions { moreStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("241")). Italic(true). Width(dropdownWidth). Align(lipgloss.Center) s.WriteString("\n") moreCount := len(c.suggestions) - maxSuggestions s.WriteString(moreStyle.Render(fmt.Sprintf("... and %d more", moreCount))) } return s.String() }

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