Skip to main content
Glama
controller.go6.64 kB
package filebrowser import ( "fmt" "os" "path/filepath" "sort" "strings" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // FileItem represents a file or directory in the file browser type FileItem struct { Name string IsDir bool Path string } // fileBrowserItem implements list.Item for file browser type fileBrowserItem struct { name string path string isDir bool } func (i fileBrowserItem) FilterValue() string { return i.name } func (i fileBrowserItem) Title() string { if i.isDir { return "📁 " + i.name } return "📄 " + i.name } func (i fileBrowserItem) Description() string { if i.isDir { return "Directory" } return "File" } // Controller manages file browser state and operations type Controller struct { showing bool currentPath string fileList []FileItem listModel list.Model // For list navigation // Callbacks onFileSelect func(path string) onCancel func() } // NewController creates a new file browser controller func NewController() *Controller { homeDir, _ := os.UserHomeDir() listModel := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) listModel.Title = "File Browser" listModel.SetShowStatusBar(false) return &Controller{ showing: false, currentPath: homeDir, fileList: []FileItem{}, listModel: listModel, } } // SetCallbacks sets the callback functions func (c *Controller) SetCallbacks(onFileSelect func(string), onCancel func()) { c.onFileSelect = onFileSelect c.onCancel = onCancel } // Show displays the file browser func (c *Controller) Show() { c.showing = true c.LoadFileList() } // Hide hides the file browser func (c *Controller) Hide() { c.showing = false } // IsShowing returns whether the file browser is visible func (c *Controller) IsShowing() bool { return c.showing } // GetCurrentPath returns the current directory path func (c *Controller) GetCurrentPath() string { return c.currentPath } // GetFileList returns the current file list func (c *Controller) GetFileList() []FileItem { return c.fileList } // GetListModel returns the list model for external manipulation func (c *Controller) GetListModel() *list.Model { return &c.listModel } // GetSelectedIndex returns the currently selected index in the list func (c *Controller) GetSelectedIndex() int { return c.listModel.Index() } // SetListSize updates the size of the list model func (c *Controller) SetListSize(width, height int) { c.listModel.SetSize(width, height) } // LoadFileList loads files and directories for the current path func (c *Controller) LoadFileList() { files, err := os.ReadDir(c.currentPath) if err != nil { return } c.fileList = []FileItem{} // Add parent directory option if not at root if c.currentPath != "/" { c.fileList = append(c.fileList, FileItem{ Name: "..", IsDir: true, Path: filepath.Dir(c.currentPath), }) } // Add directories first var dirs []FileItem var regularFiles []FileItem for _, file := range files { // Skip hidden files if strings.HasPrefix(file.Name(), ".") { continue } fullPath := filepath.Join(c.currentPath, file.Name()) item := FileItem{ Name: file.Name(), IsDir: file.IsDir(), Path: fullPath, } if file.IsDir() { dirs = append(dirs, item) } else { regularFiles = append(regularFiles, item) } } // Sort directories and files separately sort.Slice(dirs, func(i, j int) bool { return strings.ToLower(dirs[i].Name) < strings.ToLower(dirs[j].Name) }) sort.Slice(regularFiles, func(i, j int) bool { return strings.ToLower(regularFiles[i].Name) < strings.ToLower(regularFiles[j].Name) }) // Combine the lists c.fileList = append(c.fileList, dirs...) c.fileList = append(c.fileList, regularFiles...) // Update the list model c.updateListModel() } // updateListModel updates the list model with current file list func (c *Controller) updateListModel() { items := []list.Item{} for _, file := range c.fileList { items = append(items, fileBrowserItem{ name: file.Name, path: file.Path, isDir: file.IsDir, }) } c.listModel.SetItems(items) } // HandleInput processes keyboard input for the file browser func (c *Controller) HandleInput(msg tea.KeyMsg) tea.Cmd { selectedIndex := c.listModel.Index() switch msg.String() { case "esc": c.Hide() if c.onCancel != nil { c.onCancel() } return nil case "enter": if selectedIndex >= 0 && selectedIndex < len(c.fileList) { selected := c.fileList[selectedIndex] if selected.IsDir { c.currentPath = selected.Path c.LoadFileList() } else if c.onFileSelect != nil { c.Hide() c.onFileSelect(selected.Path) } } return nil } return nil } // Render renders the file browser UI func (c *Controller) Render(width, height int) string { if !c.showing { return "" } selectedIndex := c.listModel.Index() // Create styles dialogStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("63")). Padding(1, 2). Width(width - 20). MaxHeight(height - 10). Background(lipgloss.Color("235")) titleStyle := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("230")). MarginBottom(1) pathStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("241")). MarginBottom(1) selectedStyle := lipgloss.NewStyle(). Background(lipgloss.Color("62")). Foreground(lipgloss.Color("230")) dirStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("33")) // Build content var content strings.Builder // Title content.WriteString(titleStyle.Render("Select MCP Configuration File")) content.WriteString("\n") // Current path content.WriteString(pathStyle.Render(fmt.Sprintf("📁 %s", c.currentPath))) content.WriteString("\n\n") // File list for i, item := range c.fileList { line := item.Name if item.IsDir { line = "📁 " + line } else { line = "📄 " + line } if i == selectedIndex { content.WriteString(selectedStyle.Render(line)) } else if item.IsDir { content.WriteString(dirStyle.Render(line)) } else { content.WriteString(line) } content.WriteString("\n") } // Help text content.WriteString("\n") helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241")) content.WriteString(helpStyle.Render("↑/↓: Navigate • Enter: Select • Esc: Cancel")) // Create dialog dialog := dialogStyle.Render(content.String()) // Use lipgloss to center the dialog return lipgloss.Place( width, height, lipgloss.Center, lipgloss.Center, dialog, lipgloss.WithWhitespaceChars(" "), ) }

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