Skip to main content
Glama
input_controller.go8.81 kB
package tui import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/standardbeagle/brummer/internal/process" "github.com/standardbeagle/brummer/internal/tui/system" ) // InputController handles all keyboard input and routing type InputController struct { // Dependencies model *Model // Will be refactored to use interface in future iteration keys keyMap viewConfigs map[View]ViewConfig debugMode bool } // NewInputController creates a new input controller func NewInputController(model *Model, keys keyMap, viewConfigs map[View]ViewConfig, debugMode bool) *InputController { return &InputController{ model: model, keys: keys, viewConfigs: viewConfigs, debugMode: debugMode, } } // HandleKeyMsg processes keyboard input and returns whether it was handled func (ic *InputController) HandleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) { // Handle script selector view if ic.model.currentView() == ViewScriptSelector && ic.model.scriptSelectorController != nil { handled, cmd := ic.model.scriptSelectorController.HandleKeyMsg(msg) if handled { return ic.model, cmd, true } } // Handle command window first if ic.model.commandWindowController.IsShowingCommandWindow() { return ic.handleCommandWindow(msg) } // Handle "/" key for Brummer commands - check if we should intercept it if msg.String() == "/" || (msg.Type == tea.KeyRunes && len(msg.Runes) == 1 && msg.Runes[0] == '/') { // Check if we should intercept the slash command shouldIntercept := true if ic.model.currentView() == ViewAICoders && ic.model.aiCoderController != nil { shouldIntercept = ic.model.aiCoderController.ShouldInterceptSlashCommand() } if shouldIntercept { ic.model.showCommandWindow() return ic.model, nil, true } // If not intercepting, fall through to PTY handling } // Check if we're in AI Coders view - route input to the controller // The controller will handle both focused and unfocused states if ic.model.currentView() == ViewAICoders && ic.model.aiCoderController != nil { // Route all input to the AI Coder controller when in AI Coders view // It will handle Enter to focus, keys when focused, etc. var cmd tea.Cmd cmd = ic.model.aiCoderController.Update(msg) // Only mark as handled if we're focused or it's a key the PTY view handles if ic.model.aiCoderController.IsTerminalFocused() { return ic.model, cmd, true } // Check if it's a key the PTY view handles when unfocused switch msg.String() { case "enter", "f11", "ctrl+h", "ctrl+n", "ctrl+shift+p", "ctrl+d", "f12", "pgup", "pgdown": return ic.model, cmd, true } // For other keys, continue to global key handling } // Handle global keys if model, cmd, handled := ic.handleGlobalKeys(msg); handled { return model, cmd, true } // Handle view-specific keys switch { case key.Matches(msg, ic.keys.ClearErrors): if ic.model.currentView() == ViewErrors { ic.model.handleClearErrors() return ic.model, nil, true } case key.Matches(msg, ic.keys.Enter): cmd := ic.handleEnter() return ic.model, cmd, true case key.Matches(msg, ic.keys.RunDialog): if !ic.model.commandWindowController.IsShowingRunDialog() { ic.model.showRunDialog() } return ic.model, nil, true } return ic.model, nil, false } // handleGlobalKeys handles keys that work across all views func (ic *InputController) handleGlobalKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) { switch { case key.Matches(msg, ic.keys.Quit): // Check if there are running processes runningProcesses := 0 for _, proc := range ic.model.processMgr.GetAllProcesses() { if proc.GetStatus() == process.StatusRunning { runningProcesses++ } } if runningProcesses > 0 { return ic.model, tea.Sequence( tea.Printf("Stopping %d running processes...\n", runningProcesses), func() tea.Msg { _ = ic.model.processMgr.Cleanup() // Ignore cleanup errors during shutdown return tea.Msg(nil) }, tea.Printf("%s", renderExitScreen()), tea.Quit, ), true } else { return ic.model, tea.Sequence( tea.Printf("%s", renderExitScreen()), tea.Quit, ), true } case key.Matches(msg, ic.keys.Tab): ic.model.cycleView() return ic.model, nil, true case msg.String() == "shift+tab": ic.model.cyclePrevView() return ic.model, nil, true case msg.String() == "left": ic.model.cyclePrevView() return ic.model, nil, true case msg.String() == "right": ic.model.cycleView() return ic.model, nil, true case key.Matches(msg, ic.keys.ClearScreen): ic.model.handleClearScreen() return ic.model, nil, true case key.Matches(msg, ic.keys.Back): if ic.model.currentView() == ViewFilters { ic.model.navController.SwitchTo(ViewLogs) } else if ic.model.currentView() == ViewLogs || ic.model.currentView() == ViewErrors || ic.model.currentView() == ViewURLs { ic.model.navController.SwitchTo(ViewProcesses) } return ic.model, nil, true case key.Matches(msg, ic.keys.Priority): if ic.model.currentView() == ViewLogs { // Toggle high priority in LogsViewController ic.model.logsViewController.ToggleHighPriority() ic.model.updateLogsView() } return ic.model, nil, true case key.Matches(msg, ic.keys.RestartAll): if ic.model.currentView() == ViewProcesses { ic.model.logStore.Add("system", "System", "Restarting all running processes...", false) return ic.model, ic.model.handleRestartAll(), true } return ic.model, nil, true case key.Matches(msg, ic.keys.CopyError): return ic.model, ic.model.handleCopyError(), true case key.Matches(msg, ic.keys.ClearLogs): if ic.model.currentView() == ViewLogs { ic.model.handleClearLogs() } return ic.model, nil, true case key.Matches(msg, ic.keys.ToggleError): if ic.model.systemController != nil && ic.model.systemController.HasMessages() { // Toggle system panel via layout controller if ic.model.layoutController != nil { current := ic.model.layoutController.GetSystemPanelHeight() > 0 ic.model.layoutController.SetSystemPanelOpen(!current) } } return ic.model, nil, true case key.Matches(msg, ic.keys.ClearMessages): // Clear system messages - create new controller instance if ic.model.systemController != nil && ic.model.systemController.HasMessages() { // For now, just create a new controller instance to clear messages ic.model.systemController = system.NewController(100) // Force immediate re-render return ic.model, tea.ClearScreen, true } return ic.model, nil, true } // Handle number keys for view switching for viewType, cfg := range ic.viewConfigs { if msg.String() == cfg.KeyBinding { // Skip MCP connections view if not in debug mode if viewType == ViewMCPConnections && !ic.debugMode { continue } ic.model.switchToView(viewType) return ic.model, nil, true } } return ic.model, nil, false // Key not handled } // handleEnter handles the Enter key based on current view func (ic *InputController) handleEnter() tea.Cmd { switch ic.model.currentView() { case ViewProcesses: if i, ok := ic.model.processViewController.GetProcessesList().SelectedItem().(processItem); ok { ic.model.selectedProcess = i.process.ID ic.model.navController.SwitchTo(ViewLogs) ic.model.updateLogsView() } } return nil } // handleCommandWindow handles input when command window is open func (ic *InputController) handleCommandWindow(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) { commandAutocomplete := ic.model.commandWindowController.GetCommandAutocomplete() switch msg.String() { case "esc": ic.model.commandWindowController.HideCommandWindow() return ic.model, nil, true case "backspace": if commandAutocomplete.Value() == "" { ic.model.commandWindowController.HideCommandWindow() return ic.model, nil, true } case "enter": // If there are suggestions available, apply the selected one first if len(commandAutocomplete.GetSuggestions()) > 0 { // Simulate a tab key press to apply the selected suggestion tabMsg := tea.KeyMsg{Type: tea.KeyTab} *commandAutocomplete, _ = commandAutocomplete.Update(tabMsg) } // Validate the command first if valid, errMsg := commandAutocomplete.ValidateInput(); !valid { // Set error message in the autocomplete component commandAutocomplete.SetError(errMsg) return ic.model, nil, true } // Execute the command value := commandAutocomplete.Value() ic.model.commandWindowController.HideCommandWindow() // Handle the command through slash command handler ic.model.handleSlashCommand(value) return ic.model, nil, true } // Update the autocomplete component newAuto, cmd := commandAutocomplete.Update(msg) *commandAutocomplete = newAuto return ic.model, cmd, true }

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