mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
Merge pull request #486 from wtfutil/WTF-42-rss-reader
WTF-42 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 `help:"An array of RSS and Atom feed URLs"`
|
||||
feedLimit int `help:"The maximum number of stories to display for each feed"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user