.
terminews-1.2.2/README.md 0000664 0000000 0000000 00000005576 14022715460 0015026 0 ustar 00root root 0000000 0000000 # terminews [](https://travis-ci.org/antavelos/terminews)
**terminews** is a terminal based application (TUI) which makes use of the [gocui](https://github.com/jroimartin/gocui) and [gofeed](https://github.com/mmcdole/gofeed) libraries and allows you to manage RSS resources and display their news feed. Currently it is only compatible with _Linux_ environments.
[](https://asciinema.org/a/WKvIugMqbohNtxqCZHHPDcWRr)
## Installation
**terminews** is currently available only on _Linux_.
### Dependencies
* [Sqlite3](https://www.sqlite.org/)
For storing site' data and bookmarking news.
### From binary
You may download and run the binary from the [latest release](https://github.com/antavelos/terminews/releases/latest)
### From source code
go get github.com/antavelos/terminews
cd $GOPATH/src/github.com/antavelos/terminews
go build
./terminews
## Usage
### Layout
The terminal is split in 3 different areas:
1. **Sites list** which contains the list of the user's saved sites.
2. **News list** which contains the news feed (list of news' titles) of the currently selected site.
3. **Summary** which contains extra information of the currently selected event.

### Key bindings
Key combination | Description
---|---
Tab|Focuses between the Sites list and the News list alternately
Enter|Retrieves the news feed of the currently selected site or submits user input
Ctrlo|Downloads the content of the currently selected event.
CtrlAlto|Opens the currently selected event using the default browser
Ctrln|Prompts the user to add a new site (URL)
Ctrlf|Prompts the user to search among the existing sites. Multiple terms are allowed and they are used conjunctively
Ctrlq|Closes any window (input prompt, event content) displayed on top of the main windows
Ctrlb|Adds or removes the currently selected event in the bookmarks list
CtrlAltb|Displays the bookmarked events
Del|Deletes the selected site of the selected bookmarked event depending on which list is currently focused
↑|Moves to the previous list item circularly
↓|Moves to the next list item circularly
PgUp|Moves to the previous list page circularly
PgDn|Moves to the next list page circularly
Ctrlh|Opens up the Help window
Ctrlc|Exits the application
## Credits
* [GOCUI](https://github.com/jroimartin/gocui) for the UI
* [gofeed](https://github.com/mmcdole/gofeed) for retrieving the RSS feed
* [GoOse](https://github.com/advancedlogic/GoOse) for extracting article content
terminews-1.2.2/_config.yml 0000664 0000000 0000000 00000000032 14022715460 0015654 0 ustar 00root root 0000000 0000000 theme: jekyll-theme-hacker terminews-1.2.2/content.go 0000664 0000000 0000000 00000004553 14022715460 0015542 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package main
import (
// "fmt"
"github.com/advancedlogic/GoOse"
// "golang.org/x/net/html"
// "net/http"
"strings"
)
// func ParseUrl(url string, ch chan string, done chan bool) {
// resp, err := http.Get(url)
// defer func() {
// // Notify that we're done after this function
// done <- true
// }()
// if err != nil {
// return
// }
// b := resp.Body
// defer b.Close() // close Body when the function returns
// z := html.NewTokenizer(b)
// inParagraph := false
// for {
// tt := z.Next()
// switch tt {
// case html.ErrorToken:
// // End of the document, we're done
// return
// case html.StartTagToken:
// t := z.Token()
// // Check if the token is an tag
// isParagraph := t.Data == "p"
// if isParagraph {
// inParagraph = true
// }
// case html.EndTagToken:
// t := z.Token()
// isParagraph := t.Data == "p"
// if isParagraph {
// inParagraph = false
// }
// case html.TextToken:
// if inParagraph {
// t := fmt.Sprint(z.Token())
// ch <- strings.TrimSpace(html.UnescapeString(t))
// }
// }
// }
// }
// func GetContent(url string) []string {
// ch := make(chan string)
// done := make(chan bool)
// go ParseUrl(url, ch, done)
// content := []string{}
// for {
// select {
// case text := <-ch:
// content = append(content, text)
// case <-done:
// return content
// }
// }
// }
func GetContent(url string) ([]string, error) {
g := goose.New()
article, err := g.ExtractFromURL(url)
if err != nil {
return nil, err
}
lines := strings.Split(article.CleanedText, "\n\n")
return lines, nil
}
terminews-1.2.2/ctrl.go 0000664 0000000 0000000 00000044723 14022715460 0015037 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package main
import (
"fmt"
"log"
"net/url"
"os/exec"
"strings"
"github.com/antavelos/terminews/db"
c "github.com/jroimartin/gocui"
)
// UpdateSummary updates the summary View based on the currently selected
// news item
func UpdateSummary() error {
Summary.Clear()
currItem := NewsList.CurrentItem()
if currItem == nil {
return nil
}
event := currItem.(db.Event)
authorLine := fmt.Sprintf("%v %v", Bold.Sprint("By:"), event.Author)
publishedLine := fmt.Sprintf("%v %v", Bold.Sprint("Published on:"), event.Published)
urlLine := fmt.Sprintf("%v %v", Bold.Sprint("URL:"), event.Url)
w, _ := Summary.Size()
summaryLine := strings.Join(JustifiedLines(event.Summary, w-2), "\n ")
_, err := fmt.Fprintf(Summary, "\n\n %v\n %v\n %v\n\n\n %v",
authorLine, publishedLine, urlLine, Bold.Sprint(summaryLine))
return err
}
func eventInBookmarks(event db.Event) (db.Event, bool) {
for _, b := range CurrentBookmarks {
if event.Url == b.Url {
return b, true
}
}
return db.Event{}, false
}
// UpdateNews updates the news list according to the given events
func UpdateNews(events []db.Event, from string) error {
NewsList.Reset()
Summary.Clear()
if len(events) == 0 {
NewsList.SetTitle(fmt.Sprintf("No news in %v", from))
return nil
}
NewsList.SetTitle(fmt.Sprintf("News from: %v", from))
data := make([]interface{}, len(events))
for i, e := range events {
if _, ok := eventInBookmarks(e); ok {
e.Title = fmt.Sprintf(" %v", e.Title)
}
data[i] = e
}
return NewsList.SetItems(data)
}
// LoadSites loads the sites from DB and displays them in the list
func LoadSites() error {
SitesList.SetTitle("Sites")
sites, err := tdb.GetSites()
if err != nil {
fmt.Errorf("Failed to load sites: %v", err)
}
if len(sites) == 0 {
SitesList.SetTitle("No sites yet... (Ctrl-n to add)")
SitesList.Reset()
NewsList.Reset()
NewsList.SetTitle("No news yet...")
return nil
}
data := make([]interface{}, len(sites))
for i, rr := range sites {
data[i] = rr
}
return SitesList.SetItems(data)
}
// createContentView creates a view where the contents of thecurrently selected
// event will be displayed
func createContentView(g *c.Gui) error {
tw, th := g.Size()
v, err := g.SetView(CONTENT_VIEW, tw/8, th/8, (tw*7)/8, (th*7)/8)
if err != nil && err != c.ErrUnknownView {
return err
}
ContentList = CreateList(v, false)
setTopWindowTitle(g, CONTENT_VIEW, "")
_, err = g.SetCurrentView(CONTENT_VIEW)
return err
}
// createPromptView creates a general purpose view to be used as input source
// from the user
func createPromptView(g *c.Gui, title string) error {
tw, th := g.Size()
v, err := g.SetView(PROMPT_VIEW, tw/6, (th/2)-1, (tw*5)/6, (th/2)+1)
if err != nil && err != c.ErrUnknownView {
return err
}
v.Editable = true
setTopWindowTitle(g, PROMPT_VIEW, title)
g.Cursor = true
_, err = g.SetCurrentView(PROMPT_VIEW)
return err
}
func createHelpView(g *c.Gui, title string) error {
tw, th := g.Size()
v, err := g.SetView(HELP_VIEW, tw/6, th/5, (tw*5)/6, (th*4)/5)
if err != nil && err != c.ErrUnknownView {
return err
}
v.Wrap = true
setTopWindowTitle(g, HELP_VIEW, title)
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Tab"), "Focuses between the Sites list and the News list alternately\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Enter"), "Retrieves the news feed of the currently selected site or submits user input\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+O"), "Downloads the content of the currently selected event.\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+Alt+O"), "Opens the currently selected event using the default browser\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+n"), "Prompts the user to add a new site (URL)\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+f"), "Prompts the user to search among the existing sites. Multiple terms are allowed and they are used conjunctively\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+q"), "Closes any window (input prompt, event content) displayed on top of the main windows\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+b"), "Adds or removes the currently selected event in the bookmarks list\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+Alt+b"), "Displays the bookmarked events\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Del"), "Deletes the selected site of the selected bookmarked event depending on which list is currently focused\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("ArrowUp;"), "Moves to the previous list item circularly\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("ArrowDown;"), "Moves to the next list item circularly\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("PgUp"), "Moves to the previous list page circularly\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("PgDn"), "Moves to the next list page circularly\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+c"), "Exits the application\n\n")
fmt.Fprintf(v, " %v: %v", Bold.Sprint("Ctrl+h"), "Opens up the Help window\n\n")
_, err = g.SetCurrentView(HELP_VIEW)
return err
}
// deleteContentView deletes the current prompt view
func deleteContentView(g *c.Gui) error {
g.Cursor = false
return g.DeleteView(CONTENT_VIEW)
}
// deletePromptView deletes the current prompt view
func deletePromptView(g *c.Gui) error {
g.Cursor = false
return g.DeleteView(PROMPT_VIEW)
}
// deleteHelpView deletes the current help view
func deleteHelpView(g *c.Gui) error {
g.Cursor = false
return g.DeleteView(HELP_VIEW)
}
func setTopWindowTitle(g *c.Gui, view_name, title string) {
v, err := g.View(view_name)
if err != nil {
log.Println("Error on setTopWindowTitle", err)
return
}
v.Title = fmt.Sprintf("%v (Ctrl-q to close)", title)
}
func isNewSitePrompt(v *c.View) bool {
return strings.Contains(v.Title, "New site") || strings.Contains(v.Title, "try again")
}
func isFindPrompt(v *c.View) bool {
return strings.Contains(v.Title, "Search ")
}
func isBookmarksNews() bool {
return strings.Contains(NewsList.Title, "My bookmarks")
}
// eventSatisfiesSearch searches within thr title and the summary of an event
// and if a list of terms exists conjuctively and case insensitively
func eventSatisfiesSearch(terms []string, e db.Event) bool {
for _, term := range terms {
tl := strings.ToLower(term)
title := strings.ToLower(e.Title)
summary := strings.ToLower(e.Summary)
if !strings.Contains(title, tl) && !strings.Contains(summary, tl) {
return false
}
}
return true
}
// findEvents downloads the events of every available site and returns those
// which match the given terms
func findEvents(terms []string, c chan db.Event, done chan bool) {
defer func() {
done <- true
}()
sites, err := tdb.GetSites()
if err != nil {
return
}
for _, site := range sites {
events, err := DownloadEvents(site.Url)
if err != nil {
continue
}
for _, e := range events {
if eventSatisfiesSearch(terms, e) {
c <- e
}
}
}
}
// Key binding functions
func Quit(g *c.Gui, v *c.View) error {
return c.ErrQuit
}
func SwitchView(g *c.Gui, v *c.View) error {
switch v.Name() {
case SITES_VIEW:
g.SelFgColor = c.ColorGreen | c.AttrBold
if v == SitesList.View {
NewsList.Focus(g)
SitesList.Unfocus()
if strings.Contains(NewsList.Title, "bookmarks") {
g.SelFgColor = c.ColorMagenta | c.AttrBold
}
}
case NEWS_VIEW:
SitesList.Focus(g)
NewsList.Unfocus()
}
return nil
}
func ListUp(g *c.Gui, v *c.View) error {
switch v.Name() {
case SITES_VIEW:
if err := SitesList.MoveUp(); err != nil {
log.Println("Error on SitesList.MoveUp()", err)
return err
}
case NEWS_VIEW:
if err := NewsList.MoveUp(); err != nil {
log.Println("Error on NewsList.MoveUp()", err)
return err
}
if err := UpdateSummary(); err != nil {
log.Println("Error on UpdateSummary()", err)
return err
}
case CONTENT_VIEW:
if err := ContentList.MoveUp(); err != nil {
log.Println("Error on ContentList.MoveUp()", err)
return err
}
}
return nil
}
func ListDown(g *c.Gui, v *c.View) error {
switch v.Name() {
case SITES_VIEW:
if err := SitesList.MoveDown(); err != nil {
log.Println("Error on SitesList.MoveDown()", err)
return err
}
case NEWS_VIEW:
if err := NewsList.MoveDown(); err != nil {
log.Println("Error on NewsList.MoveDown()", err)
return err
}
if err := UpdateSummary(); err != nil {
log.Println("Error on UpdateSummary()", err)
return err
}
case CONTENT_VIEW:
if err := ContentList.MoveDown(); err != nil {
log.Println("Error on ContentList.MoveDown()", err)
return err
}
}
return nil
}
func ListPgDown(g *c.Gui, v *c.View) error {
switch v.Name() {
case SITES_VIEW:
if err := SitesList.MovePgDown(); err != nil {
log.Println("Error on SitesList.MovePgDown()", err)
return err
}
case NEWS_VIEW:
if err := NewsList.MovePgDown(); err != nil {
log.Println("Error on NewsList.MovePgDown()", err)
return err
}
if err := UpdateSummary(); err != nil {
log.Println("Error on UpdateSummary()", err)
return err
}
case CONTENT_VIEW:
if err := ContentList.MovePgDown(); err != nil {
log.Println("Error on ContentList.MovePgDown()", err)
return err
}
}
return nil
}
func ListPgUp(g *c.Gui, v *c.View) error {
switch v.Name() {
case SITES_VIEW:
if err := SitesList.MovePgUp(); err != nil {
log.Println("Error on SitesList.MovePgUp()", err)
return err
}
case NEWS_VIEW:
if err := NewsList.MovePgUp(); err != nil {
log.Println("Error on NewsList.MovePgUp()", err)
return err
}
if err := UpdateSummary(); err != nil {
log.Println("Error on UpdateSummary()", err)
return err
}
case CONTENT_VIEW:
if err := ContentList.MovePgUp(); err != nil {
log.Println("Error on ContentList.MovePgUp()", err)
return err
}
}
return nil
}
func OnEnter(g *c.Gui, v *c.View) error {
switch v.Name() {
case SITES_VIEW:
currItem := SitesList.CurrentItem()
if currItem == nil {
return nil
}
site := currItem.(db.Site)
Summary.Clear()
NewsList.Clear()
NewsList.Focus(g)
g.SelFgColor = c.ColorGreen | c.AttrBold
NewsList.Title = " Fetching ... "
g.Update(func(g *c.Gui) error {
events, err := DownloadEvents(site.Url)
if err != nil {
NewsList.Title = fmt.Sprintf(" Failed to load news from: %v ", site.Name)
NewsList.Clear()
} else {
NewsList.Focus(g)
if err := UpdateNews(events, site.Name); err != nil {
log.Println("Error on UpdateNews", err)
return err
}
if err := UpdateSummary(); err != nil {
log.Println("Error on UpdateSummary", err)
return err
}
}
return nil
})
case PROMPT_VIEW:
if isNewSitePrompt(v) {
url := strings.TrimSpace(v.ViewBuffer())
if len(url) == 0 {
return nil
}
g.Update(func(g *c.Gui) error {
feed, err := CheckUrl(url)
if err != nil {
setTopWindowTitle(g, PROMPT_VIEW, "Invalid URL, try again:")
g.SelFgColor = c.ColorRed | c.AttrBold
return nil
}
_, err = tdb.GetSiteByUrl(url)
if err != nil {
if _, ok := err.(db.NotFound); !ok {
setTopWindowTitle(g, PROMPT_VIEW, "Site already exists, try again:")
g.SelFgColor = c.ColorRed | c.AttrBold
return nil
}
} else {
log.Println("Error o GetSiteByUrl", err)
return err
}
rr := db.Site{Name: feed.Title, Url: url}
if err := tdb.AddSite(rr); err != nil {
log.Println("Error on AddSite", err)
return err
}
deletePromptView(g)
g.SelFgColor = c.ColorGreen | c.AttrBold
SitesList.Focus(g)
if err = LoadSites(); err != nil {
log.Println("Error on LoadSites", err)
return err
}
return nil
})
}
if isFindPrompt(v) {
NewsList.Reset()
NewsList.Focus(g)
SitesList.Unfocus()
NewsList.Title = " Searching ... "
deletePromptView(g)
terms := strings.Split(strings.TrimSpace(v.ViewBuffer()), " ")
done := make(chan bool)
cevent := make(chan db.Event)
go findEvents(terms, cevent, done)
go func() {
ct := 0
for {
select {
case <-done:
g.Update(func(g *c.Gui) error {
NewsList.SetTitle(fmt.Sprintf("%v event(s) found", ct))
return nil
})
return
case event := <-cevent:
g.Update(func(g *c.Gui) error {
NewsList.AddItem(g, event)
NewsList.SetTitle(fmt.Sprintf("%v event(s) found so far...", ct))
return nil
})
ct++
}
}
}()
}
}
return nil
}
func AddBookmark(g *c.Gui, v *c.View) error {
var err error
if v.Name() == NEWS_VIEW {
g.Update(func(g *c.Gui) error {
currItem := NewsList.CurrentItem()
if currItem == nil {
return nil
}
event := currItem.(db.Event)
if bookmark, ok := eventInBookmarks(event); ok {
if err := tdb.DeleteEvent(bookmark.Id); err != nil {
log.Println("Error on DeleteEvent", err)
return err
}
event.Title = event.Title[5:]
} else {
if err := tdb.AddEvent(event); err != nil {
log.Println("Error on AddEvent", err)
return err
}
event.Title = fmt.Sprintf(" %v", event.Title)
}
if CurrentBookmarks, err = tdb.GetEvents(); err != nil {
log.Println("Error on GetEvents", err)
return err
}
NewsList.UpdateCurrentItem(event)
if err := NewsList.DrawCurrentPage(); err != nil {
log.Println("Error while updating event on bookmark", err)
return err
}
return nil
})
}
return nil
}
func LoadBookmarks(g *c.Gui, v *c.View) error {
var err error
name := v.Name()
if name == PROMPT_VIEW || name == CONTENT_VIEW {
return nil
}
CurrentBookmarks, err = tdb.GetEvents()
if err != nil {
log.Println("Error on AddEvent", err)
return err
}
source := "My bookmarks"
if err != nil {
NewsList.Title = fmt.Sprintf(" Failed to load news from: %v ", source)
NewsList.Clear()
} else {
NewsList.Focus(g)
if err := UpdateNews(CurrentBookmarks, source); err != nil {
log.Println("Error on UpdateNews", err)
return err
}
if err := UpdateSummary(); err != nil {
log.Println("Error on UpdateSummary", err)
return err
}
}
g.SelFgColor = c.ColorMagenta | c.AttrBold
return nil
}
func DeleteEntry(g *c.Gui, v *c.View) error {
switch v.Name() {
case SITES_VIEW:
currItem := SitesList.CurrentItem()
if currItem == nil {
return nil
}
rr := currItem.(db.Site)
if err := tdb.DeleteSite(rr.Id); err != nil {
log.Println("Error on DeleteSite", err)
return err
}
if err := LoadSites(); err != nil {
log.Println("Error on LoadSites", err)
return err
}
case NEWS_VIEW:
if strings.Contains(NewsList.Title, "My bookmarks") {
currItem := NewsList.CurrentItem()
if currItem == nil {
return nil
}
event := currItem.(db.Event)
if err := tdb.DeleteEvent(event.Id); err != nil {
log.Println("Error on DeleteEvent", err)
return err
}
if err := LoadBookmarks(g, v); err != nil {
log.Println("Error on LoadBookmarks", err)
return err
}
}
}
return nil
}
func RemoveTopView(g *c.Gui, v *c.View) error {
switch v.Name() {
case PROMPT_VIEW:
SitesList.Focus(g)
if err := deletePromptView(g); err != nil {
log.Println("Error on deletePromptView", err)
return err
}
case CONTENT_VIEW:
NewsList.Focus(g)
if err := deleteContentView(g); err != nil {
log.Println("Error on deleteContentView", err)
return err
}
case HELP_VIEW:
NewsList.Focus(g)
if err := deleteHelpView(g); err != nil {
log.Println("Error on deleteHelpView", err)
return err
}
}
if isBookmarksNews() {
g.SelFgColor = c.ColorMagenta | c.AttrBold
} else {
g.SelFgColor = c.ColorGreen | c.AttrBold
}
return nil
}
func AddSite(g *c.Gui, v *c.View) error {
if err := createPromptView(g, "New site URL:"); err != nil {
log.Println("Error on createPromptView", err)
return err
}
return nil
}
func Find(g *c.Gui, v *c.View) error {
if err := createPromptView(g, "Search with multiple terms:"); err != nil {
log.Println("Error on createPromptView", err)
return err
}
return nil
}
func LoadContent(g *c.Gui, v *c.View) error {
if v.Name() == NEWS_VIEW {
if err := createContentView(g); err != nil {
log.Println("Error on createContentView", err)
return err
}
g.SelFgColor = c.ColorGreen | c.AttrBold
cv, _ := g.View(CONTENT_VIEW)
cv.Title = "Fetching..."
g.Update(func(g *c.Gui) error {
ContentList.Focus(g)
currItem := NewsList.CurrentItem()
if currItem == nil {
return nil
}
site := SitesList.CurrentItem().(db.Site)
event := currItem.(db.Event)
CurrentContent, _ = GetContent(getContentURL(site, event.Url))
if err := UpdateContent(g, CurrentContent); err != nil {
log.Println("Error on UpdateContent", err)
return err
}
ContentList.SetTitle(fmt.Sprintf("%v (Ctrl-q to close)", event.Title))
return nil
})
}
return nil
}
func UpdateContent(g *c.Gui, content []string) error {
w, _ := ContentList.Size()
ContentList.AddItem(g, "")
for _, text := range content {
lines := JustifiedLines(text, w-2)
for _, l := range lines {
err := ContentList.AddItem(g, l)
if err != nil {
log.Println("Error on ContentList.AddItem", err)
return err
}
}
ContentList.AddItem(g, "")
}
return nil
}
func OpenBrowser(g *c.Gui, v *c.View) error {
currItem := NewsList.CurrentItem()
if currItem == nil {
return nil
}
event := currItem.(db.Event)
if v.Name() == NEWS_VIEW {
cmd := exec.Command("xdg-open", event.Url)
if err := cmd.Run(); err != nil {
log.Println("Error on opening browser", err)
return err
}
}
return nil
}
func Help(g *c.Gui, v *c.View) error {
if err := createHelpView(g, " Help "); err != nil {
log.Println("Error on createHelpView", err)
return err
}
return nil
}
func getContentURL(site db.Site, contentUrl string) string {
if !strings.HasPrefix(contentUrl, "http") {
u, err := url.Parse(site.Url)
if err != nil {
return contentUrl // it would have failed anyway (todo: maybe log error message and display it to end user?)
}
return fmt.Sprintf("%s://%s/%s", u.Scheme, u.Host, strings.TrimPrefix(contentUrl, "/"))
}
return contentUrl
} terminews-1.2.2/ctrl_test.go 0000664 0000000 0000000 00000002107 14022715460 0016064 0 ustar 00root root 0000000 0000000 package main
import (
"github.com/antavelos/terminews/db"
"testing"
)
func TestGetContentURL(t *testing.T) {
type test struct {
site db.Site
url string
expected string
}
tests := []test{
{
site: db.Site{Url: "https://blog.example.org/index.xml"},
url: "/2021/02/a-blog-post",
expected: "https://blog.example.org/2021/02/a-blog-post",
},
{
site: db.Site{Url: "http://blog.example.org/index.xml"},
url: "2021/02/a-blog-post",
expected: "http://blog.example.org/2021/02/a-blog-post",
},
{
site: db.Site{Url: "https://blog.example.org/feed.rss"},
url: "https://blog.example.org/super-blog-post",
expected: "https://blog.example.org/super-blog-post",
},
{
site: db.Site{Url: "http://blog.example.org/feed.rss"},
url: "http://blog.example.org/super-blog-post",
expected: "http://blog.example.org/super-blog-post",
},
}
for _, test := range tests {
value := getContentURL(test.site, test.url)
if value != test.expected {
t.Errorf("got: %s want: %s", value, test.expected)
}
}
}
terminews-1.2.2/db/ 0000775 0000000 0000000 00000000000 14022715460 0014117 5 ustar 00root root 0000000 0000000 terminews-1.2.2/db/db.go 0000664 0000000 0000000 00000003060 14022715460 0015032 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package db
import (
"database/sql"
"path"
_ "github.com/mattn/go-sqlite3"
)
type TDB struct {
*sql.DB
}
func InitDB(appDir string) (*TDB, error) {
dbpath := path.Join(appDir, "terminews.db")
db, err := sql.Open("sqlite3", dbpath)
if err != nil || db == nil {
return nil, err
}
tdb := &TDB{db}
if err = tdb.CreateTables(); err != nil {
return nil, err
}
return tdb, nil
}
func (tdb *TDB) CreateTables() error {
ssql := []string{
GetSiteSql(),
GetEventSql(),
}
for _, s := range ssql {
_, err := tdb.Exec(s)
if err != nil {
return err
}
}
return nil
}
func (tdb *TDB) DropTables() error {
ssql := []string{
"DROP TABLE site;",
"DROP TABLE event;",
}
for _, s := range ssql {
_, err := tdb.Exec(s)
if err != nil {
return err
}
}
return nil
}
terminews-1.2.2/db/db_test.go 0000664 0000000 0000000 00000007217 14022715460 0016101 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package db
import (
"database/sql"
"os"
"testing"
_ "github.com/mattn/go-sqlite3"
)
const dbpath = "test.db"
var tdb *TDB
func SetUp() {
db, err := sql.Open("sqlite3", dbpath)
if err != nil || db == nil {
panic("DB failed to be initialized.")
}
tdb = &TDB{db}
tdb.CreateTables()
}
func TearDown() {
tdb.DropTables()
tdb.Close()
os.Remove(dbpath)
}
func TestMain(m *testing.M) {
SetUp()
exitVal := m.Run()
TearDown()
os.Exit(exitVal)
}
func TestSite(t *testing.T) {
items := []Site{
Site{Name: "CNN", Url: "www.cnn.com"},
Site{Name: "BBC", Url: "www.bbc.com"},
}
for _, item := range items {
tdb.AddSite(item)
}
result, _ := tdb.GetSites()
t.Log(result)
if len(result) != 2 {
t.Errorf("Found %v Site records %v, want %v",
len(result), len(items))
}
for i, res := range result {
if res.Name != items[i].Name {
t.Errorf("Site record %v has name %v, want %v",
res.Id, res.Name, items[i].Name)
}
}
record, _ := tdb.GetSiteById(1)
t.Log(record)
if record.Name != items[0].Name {
t.Errorf("Site record %v has name %v, want %v",
record.Id, record.Name, items[0].Name)
}
_, err := tdb.GetSiteById(12345)
if _, ok := err.(NotFound); !ok {
t.Errorf("Expected NotFound error for id 12345")
}
err = tdb.DeleteSite(12345)
if _, ok := err.(NotFound); !ok {
t.Errorf("Expected NotFound error for id 12345")
}
tdb.DeleteSite(1)
_, err = tdb.GetSiteById(1)
if _, ok := err.(NotFound); !ok {
t.Errorf("Expected NotFound error for id 12345")
}
}
func TestEvent(t *testing.T) {
items := []Event{
Event{
Title: "some event1",
Author: "author1",
Url: "www.news.com/some-event1",
Summary: "summary1",
Published: "2017"},
Event{
Title: "some event2",
Author: "author2",
Url: "www.news.com/some-event2",
Summary: "summary2",
Published: "2017"},
}
for _, item := range items {
tdb.AddEvent(item)
}
result, _ := tdb.GetEvents()
t.Log(result)
if len(result) != 2 {
t.Errorf("Found %v Event records %v, want %v",
len(result), len(items))
}
if result[1].Title != items[0].Title {
t.Errorf("Event record %v has title %v, want %v",
result[1].Id, result[1].Title, items[0].Title)
}
if result[0].Title != items[1].Title {
t.Errorf("Event record %v has title %v, want %v",
result[0].Id, result[0].Title, items[1].Title)
}
record, _ := tdb.GetEventById(1)
t.Log(record)
if record.Title != items[0].Title {
t.Errorf("Event record %v has title %v, want %v",
record.Id, record.Title, items[0].Title)
}
_, err := tdb.GetEventById(12345)
if _, ok := err.(NotFound); !ok {
t.Errorf("Expected NotFound error for id 12345")
}
err = tdb.DeleteEvent(12345)
if _, ok := err.(NotFound); !ok {
t.Errorf("Expected NotFound error for id 12345")
}
tdb.DeleteEvent(1)
_, err = tdb.GetEventById(1)
if _, ok := err.(NotFound); !ok {
t.Errorf("Expected NotFound error for id 12345")
}
}
terminews-1.2.2/db/error.go 0000664 0000000 0000000 00000001547 14022715460 0015606 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package db
type NotFound string
func (e NotFound) Error() string {
return string(e)
}
terminews-1.2.2/db/event.go 0000664 0000000 0000000 00000006216 14022715460 0015574 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package db
import (
"database/sql"
"fmt"
"net/url"
_ "github.com/mattn/go-sqlite3"
)
type Event struct {
Id int
Title string
Author string
Url string
Summary string
Published string
}
func GetEventSql() string {
return `
CREATE TABLE IF NOT EXISTS event(
Id INTEGER NOT NULL PRIMARY KEY ASC,
Title TEXT,
Author TEXT,
Url TEXT,
Summary TEXT,
Published TEXT
);`
}
func (tdb *TDB) GetEvents() ([]Event, error) {
sql_readall := `
SELECT Id, Title, Author, Url, Summary, Published FROM event
ORDER BY id DESC
`
rows, err := tdb.Query(sql_readall)
defer rows.Close()
if err != nil {
return nil, err
}
var result []Event
for rows.Next() {
e := Event{}
if err := rows.Scan(&e.Id, &e.Title, &e.Author, &e.Url, &e.Summary, &e.Published); err != nil {
return nil, err
}
result = append(result, e)
}
return result, nil
}
func (tdb *TDB) GetEventById(id int) (Event, error) {
sql_readone := `SELECT Id, Title, Author, Url, Summary, Published FROM event WHERE id = ?`
stmt, err := tdb.Prepare(sql_readone)
defer stmt.Close()
if err != nil {
return Event{}, err
}
var e Event
if err = stmt.QueryRow(id).Scan(&e.Id, &e.Title, &e.Author, &e.Url, &e.Summary, &e.Published); err != nil {
if err == sql.ErrNoRows {
return Event{}, NotFound(fmt.Sprintf("Event not found for id: %v", id))
}
return Event{}, err
}
return e, nil
}
func (tdb *TDB) AddEvent(e Event) error {
sql_additem := `
INSERT OR REPLACE INTO event(
Title,
Author,
Url,
Summary,
Published
) values(?, ?, ?, ?, ?)
`
stmt, err := tdb.Prepare(sql_additem)
defer stmt.Close()
if err != nil {
return err
}
if _, err = stmt.Exec(e.Title, e.Author, e.Url, e.Summary, e.Published); err != nil {
return err
}
return nil
}
func (tdb *TDB) DeleteEvent(id int) error {
if _, err := tdb.GetEventById(id); err != nil {
return NotFound(fmt.Sprintf("Event not found for id: %v", id))
}
sql_delete := `DELETE FROM event WHERE id = ?`
stmt, err := tdb.Prepare(sql_delete)
defer stmt.Close()
if err != nil {
return err
}
if _, err = stmt.Exec(id); err != nil {
return err
}
return nil
}
func (e Event) String() string {
return string(e.Title)
}
func (e Event) Host() string {
u, err := url.Parse(e.Url)
if err != nil {
return ""
}
return u.Hostname()
}
terminews-1.2.2/db/site.go 0000664 0000000 0000000 00000006260 14022715460 0015416 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package db
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
type Site struct {
Id int
Name string
Url string
}
func GetSiteSql() string {
return `
CREATE TABLE IF NOT EXISTS site(
Id INTEGER NOT NULL PRIMARY KEY ASC,
Name TEXT,
Url TEXT,
CreatedAt DATETIME
);`
}
func (tdb *TDB) GetSites() ([]Site, error) {
sql_readall := `
SELECT Id, Name, Url FROM site
ORDER BY datetime(CreatedAt) ASC
`
rows, err := tdb.Query(sql_readall)
defer rows.Close()
if err != nil {
return nil, err
}
var records []Site
for rows.Next() {
rr := Site{}
if err := rows.Scan(&rr.Id, &rr.Name, &rr.Url); err != nil {
return nil, err
}
records = append(records, rr)
}
return records, nil
}
func (tdb *TDB) GetSiteById(id int) (Site, error) {
sql_readone := `SELECT Id, Name, Url FROM site WHERE id = ?`
stmt, err := tdb.Prepare(sql_readone)
defer stmt.Close()
if err != nil {
return Site{}, err
}
var rr Site
if err = stmt.QueryRow(id).Scan(&rr.Id, &rr.Name, &rr.Url); err != nil {
if err == sql.ErrNoRows {
return Site{}, NotFound(fmt.Sprintf("Site not found for id: %v", id))
}
return Site{}, err
}
return rr, nil
}
func (tdb *TDB) GetSiteByUrl(url string) (Site, error) {
sql_readone := `SELECT Id, Name, Url FROM site WHERE Url = ?`
stmt, err := tdb.Prepare(sql_readone)
defer stmt.Close()
if err != nil {
return Site{}, err
}
var rr Site
if err = stmt.QueryRow(url).Scan(&rr.Id, &rr.Name, &rr.Url); err != nil {
if err == sql.ErrNoRows {
return Site{}, NotFound(fmt.Sprintf("Site not found for url: %v", url))
}
return Site{}, err
}
return rr, nil
}
func (tdb *TDB) AddSite(rr Site) error {
sql_additem := `
INSERT OR REPLACE INTO site(
Name,
Url,
CreatedAt
) values(?, ?, CURRENT_TIMESTAMP)
`
stmt, err := tdb.Prepare(sql_additem)
defer stmt.Close()
if err != nil {
return err
}
if _, err = stmt.Exec(rr.Name, rr.Url); err != nil {
return err
}
return nil
}
func (tdb *TDB) DeleteSite(id int) error {
if _, err := tdb.GetSiteById(id); err != nil {
return NotFound(fmt.Sprintf("Site not found for id: %v", id))
}
sql_delete := `DELETE FROM site WHERE id = ?`
stmt, err := tdb.Prepare(sql_delete)
defer stmt.Close()
if err != nil {
return err
}
if _, err = stmt.Exec(id); err != nil {
return err
}
return nil
}
func (rr Site) String() string {
return rr.Name
}
terminews-1.2.2/go.mod 0000664 0000000 0000000 00000000524 14022715460 0014641 0 ustar 00root root 0000000 0000000 module github.com/antavelos/terminews
go 1.14
require (
github.com/advancedlogic/GoOse v0.0.0-20200830213114-1225d531e0ad
github.com/fatih/color v1.10.0
github.com/jroimartin/gocui v0.4.0
github.com/mattn/go-sqlite3 v1.14.6
github.com/mmcdole/gofeed v1.1.0
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 // indirect
)
terminews-1.2.2/go.sum 0000664 0000000 0000000 00000017212 14022715460 0014670 0 ustar 00root root 0000000 0000000 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/advancedlogic/GoOse v0.0.0-20200830213114-1225d531e0ad h1:gyzOmx++wVkSj5kLzYtvNN2ooeJGTFTtV37t5Do4sdM=
github.com/advancedlogic/GoOse v0.0.0-20200830213114-1225d531e0ad/go.mod h1:f3HCSN1fBWjcpGtXyM119MJgeQl838v6so/PQOqvE1w=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e h1:s05JG2GwtJMHaPcXDpo4V35TFgyYZzNsmBlSkHPEbeg=
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 h1:u8AQ9bPa9oC+8/A/jlWouakhIvkFfuxgIIRjiy8av7I=
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs=
github.com/go-resty/resty/v2 v2.0.0 h1:9Nq/U+V4xsoDnDa/iTrABDWUCuk3Ne92XFHPe6dKWUc=
github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mmcdole/gofeed v1.1.0 h1:T2WrGLVJRV04PY2qwhEJLHCt9JiCtBhb6SmC8ZvJH08=
github.com/mmcdole/gofeed v1.1.0/go.mod h1:PPiVwgDXLlz2N83KB4TrIim2lyYM5Zn7ZWH9Pi4oHUk=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 h1:Rl8NelBe+n7SuLbJyw13ho7CGWUt2BjGGKIoreCWQ/c=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4JDUd43CApkmCeTSQlWjtTtABrU2qsgbuP0BI=
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
terminews-1.2.2/main.go 0000664 0000000 0000000 00000017376 14022715460 0015023 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package main
import (
"fmt"
"log"
"os"
"os/user"
"path"
"github.com/antavelos/terminews/db"
"github.com/fatih/color"
c "github.com/jroimartin/gocui"
)
const (
SITES_VIEW = "rssreaders"
NEWS_VIEW = "news"
SUMMARY_VIEW = "summary"
PROMPT_VIEW = "prompt"
CONTENT_VIEW = "content"
HELP_VIEW = "help"
appVersion = "1.2.1"
)
var (
tdb *db.TDB
SitesList *List
NewsList *List
ContentList *List
Summary *c.View
CurrentContent []string
CurrentBookmarks []db.Event
curW int
curH int
Bold *color.Color
)
// relSize calculates the sizes of the sites view width
// and the news view height in relation to the current terminal size
func relSize(g *c.Gui) (int, int) {
tw, th := g.Size()
return (tw * 3) / 10, (th * 70) / 100
}
// The layout handler calculates all sizes depending
// on the current terminal size.
func layout(g *c.Gui) error {
// Get the current terminal size.
tw, th := g.Size()
// Get the relative size of the views
rw, rh := relSize(g)
_, err := g.SetView(SITES_VIEW, 0, 0, rw, th-1)
if err != nil {
log.Fatal("Cannot update sites view", err)
}
_, err = g.SetView(NEWS_VIEW, rw+1, 0, tw-1, rh)
if err != nil {
log.Fatal("Cannot update news view", err)
}
_, err = g.SetView(SUMMARY_VIEW, rw+1, rh+1, tw-1, th-1)
if err != nil {
log.Fatal("Cannot update Summary view.", err)
}
UpdateSummary()
if _, err = g.View(PROMPT_VIEW); err == nil {
_, err = g.SetView(PROMPT_VIEW, tw/6, (th/2)-1, (tw*5)/6, (th/2)+1)
if err != nil && err != c.ErrUnknownView {
return err
}
}
if _, err = g.View(CONTENT_VIEW); err == nil {
_, err = g.SetView(CONTENT_VIEW, tw/8, th/8, (tw*7)/8, (th*7)/8)
if err != nil && err != c.ErrUnknownView {
return err
}
}
if _, err = g.View(HELP_VIEW); err == nil {
_, err = g.SetView(HELP_VIEW, tw/6, th/5, (tw*5)/6, (th*4)/5)
if err != nil && err != c.ErrUnknownView {
return err
}
}
if curW != tw || curH != th {
SitesList.ResetPages()
SitesList.Draw()
NewsList.ResetPages()
NewsList.Draw()
if ContentList != nil {
ContentList.Reset()
UpdateContent(g, CurrentContent)
}
curW = tw
curH = th
}
return nil
}
// getappDir creates if not exists the app directory where the sqlite db file
// as well as the log file will be stored. In case of failure the current dir
// will be used.
func getAppDir() (string, error) {
usr, _ := user.Current()
dir := path.Join(usr.HomeDir, ".terminews")
if _, err := os.Stat(dir); err != nil {
if os.IsNotExist(err) {
if oserr := os.Mkdir(dir, 0700); oserr != nil {
return "", oserr
}
} else {
return "", err
}
}
return dir, nil
}
func main() {
// If app is called with --version then display version flag & exit
if len(os.Args) == 2 {
if os.Args[1] == "--version" {
fmt.Println(appVersion)
return
}
}
var v *c.View
var err error
Bold = color.New(color.Bold)
appDir, err := getAppDir()
if err != nil {
panic("Could not set up app directory.")
}
// Setup logging
logfile := path.Join(appDir, "terminews.log")
f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0700)
if err != nil {
log.Fatal("Failed to open logfile", err)
}
defer f.Close()
log.SetOutput(f)
// Init DB
if tdb, err = db.InitDB(appDir); err != nil {
log.Fatal("Failed to initialize DB", err)
}
defer tdb.Close()
// Create a new GUI.
g, err := c.NewGui(c.OutputNormal)
if err != nil {
log.Fatal("Failed to initialize GUI", err)
}
defer g.Close()
// some basic configuration
g.SelFgColor = c.ColorGreen | c.AttrBold
g.BgColor = c.ColorDefault
g.Highlight = true
// setup the layout
g.SetManagerFunc(layout)
// the current actual size of the terminal
curW, curH = g.Size()
// rw is the relative width of the sites view
// rh is the relative height of the news view
rw, rh := relSize(g)
// Setup the initial layout
// Sites List
v, err = g.SetView(SITES_VIEW, 0, 0, rw, curH-1)
if err != nil && err != c.ErrUnknownView {
log.Fatal("Failed to create sites list:", err)
}
SitesList = CreateList(v, true)
SitesList.Focus(g)
// it loads the existing sites if any at the beginning
g.Update(func(g *c.Gui) error {
if err := LoadSites(); err != nil {
log.Fatal("Error while loading sites", err)
}
log.Print("Loaded initial sites")
return nil
})
// News list
v, err = g.SetView(NEWS_VIEW, rw+1, 0, curW-1, rh)
if err != nil && err != c.ErrUnknownView {
log.Fatal(" Failed to create news list:", err)
}
NewsList = CreateList(v, true)
NewsList.SetTitle("No news yet...")
// Summary view
Summary, err = g.SetView(SUMMARY_VIEW, rw+1, rh+1, curW-1, curH-1)
if err != nil && err != c.ErrUnknownView {
log.Fatal("Failed to create Summary view:", err)
}
Summary.Title = " Summary "
Summary.Wrap = true
// preload thebookmarks
CurrentBookmarks, err = tdb.GetEvents()
if err != nil {
log.Println("Error on load bookmarks", err)
}
// setup the keybindings of the app
if err = g.SetKeybinding("", c.KeyCtrlN, c.ModNone, AddSite); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyDelete, c.ModNone, DeleteEntry); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding(NEWS_VIEW, c.KeyCtrlB, c.ModNone, AddBookmark); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyCtrlC, c.ModNone, Quit); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyCtrlB, c.ModAlt, LoadBookmarks); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyTab, c.ModNone, SwitchView); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyArrowUp, c.ModNone, ListUp); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyArrowDown, c.ModNone, ListDown); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyPgup, c.ModNone, ListPgUp); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyPgdn, c.ModNone, ListPgDown); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyEnter, c.ModNone, OnEnter); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyCtrlQ, c.ModNone, RemoveTopView); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyCtrlF, c.ModNone, Find); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding(NEWS_VIEW, c.KeyCtrlO, c.ModNone, LoadContent); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding(NEWS_VIEW, c.KeyCtrlO, c.ModAlt, OpenBrowser); err != nil {
log.Fatal("Failed to set keybindings")
}
if err = g.SetKeybinding("", c.KeyCtrlH, c.ModNone, Help); err != nil {
log.Fatal("Failed to set keybindings")
}
// run the mainloop
if err = g.MainLoop(); err != nil && err != c.ErrQuit {
log.Println("terminews exited unexpectedly: ", err)
}
log.Println("Exiting")
}
terminews-1.2.2/rss.go 0000664 0000000 0000000 00000003446 14022715460 0014677 0 ustar 00root root 0000000 0000000 /*
Terminews is a terminal based (TUI) RSS feed manager.
Copyright (C) 2017 Alexandros Ntavelos, a[dot]ntavelos[at]gmail[dot]com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package main
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/antavelos/terminews/db"
"github.com/mmcdole/gofeed"
)
func CheckUrl(url string) (*gofeed.Feed, error) {
fp := gofeed.NewParser()
return fp.ParseURL(url)
}
func DownloadEvents(url string) ([]db.Event, error) {
feed, err := CheckUrl(url)
if err != nil {
return nil, errors.New(fmt.Sprintf("Failed to retrieve news from: '%v'", url))
}
var events []db.Event
for _, item := range feed.Items {
e := db.Event{}
e.Title = item.Title
if item.Author != nil {
e.Author = item.Author.Name
} else {
e.Author = "Unknown author"
}
e.Url = item.Link
if len(item.Description) > 0 {
e.Summary = trim(item.Description)
} else {
e.Summary = "No summary available"
}
e.Published = item.Published
events = append(events, e)
}
return events, nil
}
func trim(desc string) string {
var re = regexp.MustCompile(`(<.*?>)`)
// remove html
desc = re.ReplaceAllString(desc, ``)
// remove spaces
desc = strings.TrimSpace(desc)
return desc
}
terminews-1.2.2/screenshot.png 0000664 0000000 0000000 00012131534 14022715460 0016427 0 ustar 00root root 0000000 0000000 PNG
IHDR
b sBITO IDATx^Wu7ԛիmٲ$pŀ15i%@
O
ń Hb1.`IdKlYVlT^ߝ=rn}cȌ3;wٍ.n=1Q~,T6jŦa.9Q %A4&065F[E8m8"E$RAf
jA$Tί$A]H
&p
nq<X
{"ԍtQ|HBss
&6ŃxU".GO#nH7[ o"q;?C*>,M;5
` ZiL e`jQvt5F O.W-GXXop8~Gx(/avA ^FTh&0Y` ~PA+d?\JʔV21G%$5 gtv&}>8?x V͓3ӑ)KA[RUJ[F bE(0ʀ!;eeWBW/e:МTBig@b\El>DMiATOrrzٯ;TMhYXUAֹ"4]IK:cM#@EEؗpZ@Ȋ HA,n $ilJ\"°4;{OI )W)RWOE{__U &29&hTΨl,ZTs ŁW PcY11|d#GS
rZāӔ"838x(\kLf-H~O[JU&B3]T%f>N5Ƹ~B`=oYS%*y z^)j] O2OӗË鲧4D?Bm {Q`Pk55$|0ɐ8ś
!iԢML;.
3N|zt xO@J0
g9rԠ",`r1zJŮXGϻ\u44߃h% k4V>-v/I6x]ׄflpTx.PgLXtBsG*MeFOxJ
P
:aJ[0CmⲄ@n [j:)0/p2N1Zei6$rN9Akbd-P! v& $X-L;cYH:IkJP-+J{|ubIPjRʢT#PsX?gA۫bj$ODR8 An1L]T'N 5FIKAy h\Ù"t (IRrv!UK8)I 0=h8jtjϠX|mәsAq)$}mԾOU@hDiٱk&~82&d!s,Q1Tj
)S0ŧ`,*NN֕ČH j^msiTr.\IM9J$#2gǼn^ԤWmcBv8%+"|cMaР࠻uudU[8KItrEf]yu_0p~EX8 (5r]tSֈV[DCGh=96
d ?@EP=
vf<^s4cD=q1m5bkse3X86P!'Hj&R4F'n5{ώ_
[94w0SSg0fW`||Ey1ȫou,1q +=#ZZ5k&(l U ~K"EFDK(AcD")RkX\R;YJ֙;#+E-PDxbĬJ^u>dn<
xLa:#P/>3yj¾V;OzΪ$HiUUd`@6N7," չaS,|JP
њ?cyg?SlmF$W1G6-X@G Ph)Tr`ƀ[ 4(]u&SDTN9dEky)>$7p5q-rA,vecM&d
d
d
ƷJ[sfטq.S S S S S S S S S S S S S S_}Y\)uy]5Ty!E;`W2>Wz*`;躸IV"lxGR.kkma_GL[NX ܬ_%@vTkzѽ%X5bo1M.¡^בGvd7I aA%S Sk7}-VS:
5eZHHnlhA%AP)K-:U /~2H.P(XWTTJ
*zM1zV5c/UJ$([ǪTv7X><3
:NMYA9g;_4ڙ,
1> Nv:vETkvE%vx3Gsf]t C!{ dG@SN!)UbwRAWulų&c|