mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
WTF-42 WIP Add FeedReader, an RSS/Atom feed reader
This commit is contained in:
16
modules/feedreader/keyboard.go
Normal file
16
modules/feedreader/keyboard.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package feedreader
|
||||
|
||||
import "github.com/gdamore/tcell"
|
||||
|
||||
func (widget *Widget) initializeKeyboardControls() {
|
||||
widget.SetKeyboardChar("/", widget.ShowHelp, "Show/hide this help widget")
|
||||
widget.SetKeyboardChar("r", widget.Refresh, "Refresh widget")
|
||||
widget.SetKeyboardChar("j", widget.Next, "Select next item")
|
||||
widget.SetKeyboardChar("k", widget.Prev, "Select previous item")
|
||||
widget.SetKeyboardChar("o", widget.openStory, "Open story in browser")
|
||||
|
||||
widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next item")
|
||||
widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous item")
|
||||
widget.SetKeyboardKey(tcell.KeyEnter, widget.openStory, "Open story in browser")
|
||||
widget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, "Clear selection")
|
||||
}
|
||||
31
modules/feedreader/settings.go
Normal file
31
modules/feedreader/settings.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package feedreader
|
||||
|
||||
import (
|
||||
"github.com/olebedev/config"
|
||||
"github.com/wtfutil/wtf/cfg"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTitle = "Feed Reader"
|
||||
)
|
||||
|
||||
// Settings defines the configuration properties for this module
|
||||
type Settings struct {
|
||||
common *cfg.Common
|
||||
|
||||
feeds []string
|
||||
feedLimit int
|
||||
}
|
||||
|
||||
// NewSettingsFromYAML creates a new settings instance from a YAML config block
|
||||
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
|
||||
settings := &Settings{
|
||||
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, ymlConfig, globalConfig),
|
||||
|
||||
feeds: wtf.ToStrs(ymlConfig.UList("feeds")),
|
||||
feedLimit: ymlConfig.UInt("feedLimit", -1),
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
165
modules/feedreader/widget.go
Normal file
165
modules/feedreader/widget.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package feedreader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
// FeedItem represents an item returned from an RSS or Atom feed
|
||||
type FeedItem struct {
|
||||
item *gofeed.Item
|
||||
viewed bool
|
||||
}
|
||||
|
||||
// Widget is the container for RSS and Atom data
|
||||
type Widget struct {
|
||||
wtf.KeyboardWidget
|
||||
wtf.ScrollableWidget
|
||||
|
||||
stories []*FeedItem
|
||||
parser *gofeed.Parser
|
||||
settings *Settings
|
||||
}
|
||||
|
||||
// NewWidget creates a new instance of a widget
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget {
|
||||
widget := &Widget{
|
||||
KeyboardWidget: wtf.NewKeyboardWidget(app, pages, settings.common),
|
||||
ScrollableWidget: wtf.NewScrollableWidget(app, settings.common, true),
|
||||
|
||||
parser: gofeed.NewParser(),
|
||||
settings: settings,
|
||||
}
|
||||
|
||||
widget.SetRenderFunction(widget.Render)
|
||||
widget.initializeKeyboardControls()
|
||||
widget.View.SetInputCapture(widget.InputCapture)
|
||||
|
||||
widget.KeyboardWidget.SetView(widget.View)
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// Fetch retrieves RSS and Atom feed data
|
||||
func (widget *Widget) Fetch(feedURLs []string) ([]*FeedItem, error) {
|
||||
data := []*FeedItem{}
|
||||
|
||||
for _, feedURL := range feedURLs {
|
||||
feedItems, err := widget.fetchForFeed(feedURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, feedItem := range feedItems {
|
||||
data = append(data, feedItem)
|
||||
}
|
||||
}
|
||||
|
||||
data = widget.sort(data)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Refresh updates the data in the widget
|
||||
func (widget *Widget) Refresh() {
|
||||
feedItems, err := widget.Fetch(widget.settings.feeds)
|
||||
if err != nil {
|
||||
widget.Redraw(widget.CommonSettings.Title, err.Error(), true)
|
||||
}
|
||||
|
||||
widget.stories = feedItems
|
||||
widget.SetItemCount(len(feedItems))
|
||||
|
||||
widget.Render()
|
||||
}
|
||||
|
||||
// Render sets up the widget data for redrawing to the screen
|
||||
func (widget *Widget) Render() {
|
||||
if widget.stories == nil {
|
||||
return
|
||||
}
|
||||
|
||||
title := widget.CommonSettings.Title
|
||||
widget.Redraw(title, widget.contentFrom(widget.stories), false)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) fetchForFeed(feedURL string) ([]*FeedItem, error) {
|
||||
feed, err := widget.parser.ParseURL(feedURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
feedItems := []*FeedItem{}
|
||||
|
||||
for idx, gofeedItem := range feed.Items {
|
||||
if widget.settings.feedLimit >= 1 && idx >= widget.settings.feedLimit {
|
||||
// We only want to get the widget.settings.feedLimit latest articles,
|
||||
// not all of them. To get all, set feedLimit to < 1
|
||||
break
|
||||
}
|
||||
|
||||
feedItem := &FeedItem{
|
||||
item: gofeedItem,
|
||||
viewed: false,
|
||||
}
|
||||
|
||||
feedItems = append(feedItems, feedItem)
|
||||
}
|
||||
|
||||
return feedItems, nil
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(data []*FeedItem) string {
|
||||
var str string
|
||||
|
||||
for idx, feedItem := range data {
|
||||
rowColor := widget.RowColor(idx)
|
||||
|
||||
if feedItem.viewed {
|
||||
// Grays out viewed items in the list, while preserving background highlighting when selected
|
||||
rowColor = "gray"
|
||||
if idx == widget.Selected {
|
||||
rowColor = fmt.Sprintf("gray:%s", widget.settings.common.Colors.HighlightBack)
|
||||
}
|
||||
}
|
||||
|
||||
row := fmt.Sprintf(
|
||||
"[%s]%2d. %s[white]",
|
||||
rowColor,
|
||||
idx+1,
|
||||
feedItem.item.Title,
|
||||
)
|
||||
|
||||
str += wtf.HighlightableHelper(widget.View, row, idx, len(feedItem.item.Title))
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// feedItems are sorted by published date
|
||||
func (widget *Widget) sort(feedItems []*FeedItem) []*FeedItem {
|
||||
sort.Slice(feedItems, func(i, j int) bool {
|
||||
return feedItems[i].item.Published < feedItems[j].item.Published
|
||||
})
|
||||
|
||||
return feedItems
|
||||
}
|
||||
|
||||
func (widget *Widget) openStory() {
|
||||
sel := widget.GetSelected()
|
||||
|
||||
if sel >= 0 && widget.stories != nil && sel < len(widget.stories) {
|
||||
story := widget.stories[sel]
|
||||
story.viewed = true
|
||||
|
||||
wtf.OpenFile(story.item.Link)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user