1
0
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:
Chris Cummer
2019-07-03 21:10:52 -07:00
parent 9d38f5439d
commit f19f1ee86d
9 changed files with 240 additions and 12 deletions

View 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")
}

View 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
}

View 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)
}
}

View File

@@ -18,7 +18,7 @@ type Widget struct {
}
func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget {
widget := Widget{
widget := &Widget{
KeyboardWidget: wtf.NewKeyboardWidget(app, pages, settings.common),
ScrollableWidget: wtf.NewScrollableWidget(app, settings.common, true),
@@ -31,7 +31,7 @@ func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *
widget.KeyboardWidget.SetView(widget.View)
return &widget
return widget
}
/* -------------------- Exported Functions -------------------- */
@@ -53,9 +53,7 @@ func (widget *Widget) Refresh() {
var stories []Story
for idx := 0; idx < widget.settings.numberOfStories; idx++ {
story, e := GetStory(storyIds[idx])
if e != nil {
// panic(e)
} else {
if e == nil {
stories = append(stories, story)
}
}
@@ -66,21 +64,22 @@ func (widget *Widget) Refresh() {
widget.Render()
}
/* -------------------- Unexported Functions -------------------- */
// Render sets up the widget data for redrawing to the screen
func (widget *Widget) Render() {
if widget.stories == nil {
return
}
title := fmt.Sprintf("%s - %sstories", widget.CommonSettings.Title, widget.settings.storyType)
title := fmt.Sprintf("%s - %s stories", widget.CommonSettings.Title, widget.settings.storyType)
widget.Redraw(title, widget.contentFrom(widget.stories), false)
}
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) contentFrom(stories []Story) string {
var str string
for idx, story := range stories {
for idx, story := range stories {
u, _ := url.Parse(story.URL)
row := fmt.Sprintf(

View File

@@ -29,7 +29,7 @@ type Settings struct {
// NewSettingsFromYAML creates a new settings instance from a YAML config block
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
settings := Settings{
settings := &Settings{
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, ymlConfig, globalConfig),
accounts: wtf.ToStrs(ymlConfig.UList("accounts")),
@@ -45,7 +45,7 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co
settings.common.RefreshInterval = minRefreshInterval
}
return &settings
return settings
}
// HasSince returns TRUE if there's a valid "since" value setting, FALSE if there is not

View File

@@ -72,7 +72,7 @@ func (widget *Widget) contentFrom(data []*Status) string {
color = widget.settings.colors.pwned
}
str = str + fmt.Sprintf(" [%s]%s[white]\n", color, stat.Account)
str += fmt.Sprintf(" [%s]%s[white]\n", color, stat.Account)
}
return str