package tui import ( "fmt" "log" "strings" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "ytgo/scraper" "ytgo/player" ) type videoItem struct { Video scraper.Video } // Implement list.Item for videoItem func (v videoItem) Title() string { return v.Video.Title } func (v videoItem) Description() string { return fmt.Sprintf("%s | %s | %s | %s", v.Video.Channel, v.Video.Duration, v.Video.UploadDate, v.Video.Views) } func (v videoItem) FilterValue() string { return v.Video.Title } type model struct { searchBar textinput.Model results list.Model videos []scraper.Video showSearch bool err error width int height int } // Initial model setup func initialModel() model { searchBar := textinput.New() searchBar.Placeholder = "Search YouTube" searchBar.Focus() delegate := list.NewDefaultDelegate() results := list.New([]list.Item{}, delegate, 50, 20) results.Title = "Search Results" return model{ searchBar: searchBar, results: results, showSearch: true, // Start in search mode } } // Init initializes the Bubble Tea program func (m model) Init() tea.Cmd { return textinput.Blink } // Update handles updates to the model based on user input func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": return m, tea.Quit case "/": m.showSearch = true m.searchBar.Focus() return m, nil case "esc": if m.showSearch { m.showSearch = false m.searchBar.Blur() } return m, nil case "enter": if m.showSearch { query := m.searchBar.Value() if query == "" { return m, nil // Ignore empty queries } // Fetch video results videos, err := scraper.FetchVideos(query) if err != nil { m.err = err log.Printf("Error fetching videos: %v", err) return m, nil } // Populate the results list items := make([]list.Item, len(videos)) for i, v := range videos { items[i] = videoItem{Video: v} } m.videos = videos m.results.SetItems(items) m.showSearch = false m.searchBar.Blur() } else { // Handle video selection if i := m.results.Index(); i != -1 { selectedVideo := m.videos[i] cmd := player.PlayVideo(selectedVideo.URL) return m, tea.ExecProcess(cmd, nil) } } return m, nil } case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height m.results.SetSize(msg.Width, msg.Height-3) // Leave space for search bar } var cmd tea.Cmd if m.showSearch { m.searchBar, cmd = m.searchBar.Update(msg) } else { m.results, cmd = m.results.Update(msg) } return m, cmd } // View renders the TUI func (m model) View() string { var s strings.Builder // Always show search bar at the top searchText := "Press '/' to search" if m.showSearch { searchText = m.searchBar.View() } s.WriteString(searchText + "\n\n") // Show results s.WriteString(m.results.View()) // Show error if any if m.err != nil { s.WriteString(fmt.Sprintf("\nError: %v", m.err)) } return s.String() } // Run starts the Bubble Tea program func Run() error { p := tea.NewProgram(initialModel()) return p.Start() }