package main import ( "context" "fmt" "log" "os" "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/mmcdole/gofeed" ) type rss_info struct { title string description string link string } func (i rss_info) Title() string { return i.title } func (i rss_info) Description() string { return i.description } func (i rss_info) Link() string { return i.link } func (i rss_info) FilterValue() string { return i.title } type model struct { list list.Model delegateKeys *delegateKeyMap } var ( appStyle = lipgloss.NewStyle().Padding(1, 2) titleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFDF5")). Background(lipgloss.Color("#CC11FF")). Padding(0, 1) statusMessageStyle = lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#FF44FF", Dark: "#CC11FF"}). Render enterKeyBinding = key.NewBinding( key.WithKeys("enter"), key.WithHelp("enter", "write playlist"), ) ) func newModel() model { var ( delegateKeys = newDelegateKeyMap() ) // Make initial list of items items := make([]list.Item, 0) argsCount := len(os.Args[1:]) if argsCount >= 2 { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() for _, rss := range os.Args[2:] { fp := gofeed.NewParser() feed, err := fp.ParseURLWithContext(rss, ctx) if err != nil { log.Fatal(err) os.Exit(1) } for _, item := range feed.Items { items = append(items, rss_info{ title: item.Title, description: item.Description, link: item.Link, }) } } } else { log.Fatal(fmt.Errorf("%v", "usage: rss2mpv playlist-file rss-url...")) os.Exit(1) } // Setup list delegate := newItemDelegate(delegateKeys) rssList := list.New(items, delegate, 0, 0) rssList.Title = "RSS Feed Links" rssList.Styles.Title = titleStyle return model{ list: rssList, delegateKeys: delegateKeys, } } func (m model) Init() tea.Cmd { return tea.EnterAltScreen } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: h, v := appStyle.GetFrameSize() m.list.SetSize(msg.Width-h, msg.Height-v) case tea.KeyMsg: // Don't match any of the keys below if we're actively filtering. if m.list.FilterState() == list.Filtering { break } switch { case key.Matches(msg, enterKeyBinding): playlist, err := os.OpenFile(os.Args[1], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { log.Fatal(err) os.Exit(1) } defer playlist.Close() for _, item := range m.list.Items() { playlist.WriteString(item.(rss_info).link + "\n") } m.list.NewStatusMessage(statusMessageStyle("Wrote to " + os.Args[1] + "!")) return m, nil } } // This will also call our delegate's update function. newListModel, cmd := m.list.Update(msg) m.list = newListModel cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m model) View() string { return appStyle.Render(m.list.View()) } func main() { if err := tea.NewProgram(newModel()).Start(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } os.Exit(0) }