From 6e473588dafca397b001dc0a2f0218983f21621c Mon Sep 17 00:00:00 2001 From: Magnus Persson Date: Tue, 27 Oct 2020 21:10:38 +0100 Subject: [PATCH] Fixes #18, Fixes #11, Fixes #10 - Background fetch - parallell feed update - search functionality - ask to quit --- Makefile | 4 +- README.md | 2 + gorss.conf | 2 + internal/config.go | 1 + internal/controller.go | 64 +++++++++++++++-------- internal/rss.go | 37 ++++++++----- internal/version.go | 1 + internal/window.go | 115 ++++++++++++++++++++++++++++++++++++++--- 8 files changed, 183 insertions(+), 43 deletions(-) diff --git a/Makefile b/Makefile index 6090f96..1db5853 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,8 @@ run: build release: @mkdir release @mkdir dist - @CC=x86_64-linux-musl-gcc CXX=x86_64-linux-musl-g++ GOARCH=amd64 GOOS=linux CGO_ENABLED=1 go build -ldflags "-linkmode external -extldflags -static -s -w -X $(shell go list)/internal.Version=${VERSION}" -o ./release/gorss_linux ./cmd/gorss/... - @go build -ldflags "-s -w -X $(shell go list)/internal.Version=${VERSION}" -o ./release/gorss_osx ./cmd/gorss/... + @GOARCH=amd64 GOOS=linux go build -ldflags "-s -w -X $(shell go list)/internal.Version=${VERSION}" -o ./release/gorss_linux ./cmd/gorss/... + @GOARCH=amd64 GOOS=darwin go build -ldflags "-s -w -X $(shell go list)/internal.Version=${VERSION}" -o ./release/gorss_osx ./cmd/gorss/... @cp gorss.conf dist/ @cp themes/default.theme dist/ @cp -r themes dist/ diff --git a/README.md b/README.md index e061708..2587f93 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ to use with the argument `-db` to the binary. - Mark articles as read - Mark all as read/unread - Undo last read (mark it as unread) +- Search titles ## Configuration Example (Default config) It's possible to specify configuration file as a flag, default is `gorss.conf`. @@ -110,6 +111,7 @@ and name fields. (See the example below for supported options). "keySwitchWindows": "Tab", "keyQuit": "Esc", "keyUndoLastRead": "u", + "keySearchPromt": "/", "customCommands": [ { "key": "j", diff --git a/gorss.conf b/gorss.conf index 3540d7d..fe0927e 100644 --- a/gorss.conf +++ b/gorss.conf @@ -3,6 +3,7 @@ "voxel", "gorss", "doit", + "devel", "lallassu" ], "OPMLFile": "../example_ompl.xml", @@ -43,6 +44,7 @@ "keySwitchWindows": "Tab", "keyQuit": "Esc", "keyUndoLastRead": "u", + "keySearchPromt": "/", "customCommands": [ { "key": "j", diff --git a/internal/config.go b/internal/config.go index 8796274..ef95b68 100644 --- a/internal/config.go +++ b/internal/config.go @@ -44,6 +44,7 @@ type Config struct { KeySwitchWindows string `json:"keySwitchWindows"` KeyQuit string `json:"keyQuit"` KeyUndoLastRead string `json:"keyUndoLastRead"` + KeySearchPromt string `json:"keySearchPromt"` CustomCommands []Command `json:"customCommands"` } diff --git a/internal/controller.go b/internal/controller.go index 9cf8430..c808ec8 100644 --- a/internal/controller.go +++ b/internal/controller.go @@ -18,20 +18,21 @@ import ( // Controller handles the logic and keep everything together type Controller struct { - rss *RSS - db *DB - win *Window - activeFeed string - linksToOpen []string - quit chan int - articles []Article - aLock sync.Mutex - conf Config - theme Theme - isUpdated bool - prevArticle *Article - undoArticle *Article - lastUpdate time.Time + rss *RSS + db *DB + win *Window + activeFeed string + linksToOpen []string + quit chan int + articles []Article + aLock sync.Mutex + conf Config + theme Theme + isUpdated bool + prevArticle *Article + undoArticle *Article + lastUpdate time.Time + searchResults int } // Init initiates the controller with database handles etc. @@ -102,7 +103,7 @@ func (c *Controller) GetConfigKeys() map[string]string { // UpdateLoop updates the feeds and windows func (c *Controller) UpdateLoop() { c.GetArticlesFromDB() - c.UpdateFeeds() // Start by updating feeds. + go c.UpdateFeeds() // Start by updating feeds. c.ShowFeeds() go func() { updateWin := time.NewTicker(time.Duration(30) * time.Second) @@ -116,9 +117,11 @@ func (c *Controller) UpdateLoop() { } c.ShowFeeds() case <-updateFeeds.C: - c.UpdateFeeds() - c.db.CleanupDB() - c.win.StatusUpdate() + go func() { + c.UpdateFeeds() + c.db.CleanupDB() + c.win.StatusUpdate() + }() case <-c.quit: c.Quit() return @@ -242,6 +245,7 @@ func (c *Controller) ShowFeeds() { } } c.win.AddToFeeds(fmt.Sprintf("[%s]Highlight", c.theme.Highlights), "", hc, total, &Article{feed: "highlight"}) + c.win.AddToFeeds(fmt.Sprintf("[%s]Search Results", c.theme.Highlights), "", c.searchResults, c.searchResults, &Article{feed: "result"}) type feed struct { count int @@ -295,6 +299,8 @@ func (c *Controller) ShowArticles(feed string) { if feed == "" { feed = "highlight" + } else if feed == "result" { + c.searchResults = 0 } c.activeFeed = feed @@ -304,6 +310,19 @@ func (c *Controller) ShowArticles(feed string) { if !a.highlight { continue } + } else if feed == "result" { + match := false + for _, f := range strings.Fields(c.win.currSearch) { + // Insensitive search + if strings.Contains(strings.ToLower(a.title), strings.ToLower(f)) { + match = true + c.searchResults++ + break + } + } + if !match { + continue + } } else if feed == "allarticles" { // pass - take all articles } else if feed == "unread" { @@ -334,6 +353,7 @@ func (c *Controller) ShowArticles(feed string) { c.isUpdated = false c.win.articles.ScrollToBeginning() + c.ShowFeeds() } // GetArticleForSelection returns the article instance for the selected article @@ -409,7 +429,6 @@ func (c *Controller) SelectArticle(row, col int) { c.db.MarkRead(c.prevArticle) c.prevArticle.read = true } - } // Input handles keystrokes @@ -420,8 +439,11 @@ func (c *Controller) Input(e *tcell.EventKey) *tcell.EventKey { } switch keyName { + case c.conf.KeySearchPromt: + c.win.Search() + case c.conf.KeyQuit: - c.quit <- 1 + c.win.AskQuit() case c.conf.KeySwitchWindows: c.win.SwitchFocus() @@ -452,7 +474,7 @@ func (c *Controller) Input(e *tcell.EventKey) *tcell.EventKey { // Remove the linkmarker icon r, _ := c.win.articles.GetSelection() cell := c.win.articles.GetCell(r, 1) - cell.SetText(fmt.Sprintf("%s", c.theme.UnreadMarker)) + cell.SetText(c.theme.UnreadMarker) } if c.activeFeed != "unread" { c.ShowArticles(c.activeFeed) diff --git a/internal/rss.go b/internal/rss.go index 123dc60..de5597d 100644 --- a/internal/rss.go +++ b/internal/rss.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "sync" "github.com/gilliek/go-opml/opml" "github.com/mmcdole/gofeed" @@ -69,20 +70,32 @@ func (r *RSS) Update() { displayName string feed *gofeed.Feed }{} + + var mu sync.Mutex + + var wg sync.WaitGroup + for _, f := range r.c.conf.Feeds { - feed, err := r.FetchURL(fp, f.URL) - if err != nil { - log.Printf("error fetching url: %s, err: %v", f.URL, err) - continue - } - r.feeds = append(r.feeds, struct { - displayName string - feed *gofeed.Feed - }{ - f.Name, - feed, - }) + wg.Add(1) + go func(f Feed) { + feed, err := r.FetchURL(fp, f.URL) + if err != nil { + log.Printf("error fetching url: %s, err: %v", f.URL, err) + } else { + mu.Lock() + r.feeds = append(r.feeds, struct { + displayName string + feed *gofeed.Feed + }{ + f.Name, + feed, + }) + mu.Unlock() + } + wg.Done() + }(f) } + wg.Wait() } // FetchURL fetches the feed URL and also fakes the user-agent to be able diff --git a/internal/version.go b/internal/version.go index 3a3f219..a37e960 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,3 +1,4 @@ package internal +// Version is version of the application var Version string diff --git a/internal/window.go b/internal/window.go index 27302d9..eb1f64f 100644 --- a/internal/window.go +++ b/internal/window.go @@ -33,6 +33,8 @@ type Window struct { showHelp bool nArticles int nFeeds int + askQuit bool + currSearch string } const ( @@ -223,8 +225,87 @@ func (w *Window) UpdateStatusTicker() { }() } +// Search asks the user to input search query +func (w *Window) Search() { + w.askQuit = true + w.flexStatus.RemoveItem(w.status) + w.currSearch = "" + + inputField := tview.NewInputField(). + SetLabel("find: "). + SetFieldWidth(20). + SetFieldBackgroundColor(tcell.ColorBlack) + + capt := func(e *tcell.EventKey) *tcell.EventKey { + keyName := string(e.Name()) + if strings.Contains(keyName, "Rune") { + keyName = string(e.Rune()) + } + + if strings.EqualFold(keyName, "esc") { + w.flexStatus.RemoveItem(inputField) + w.flexStatus.AddItem(w.status, 1, 1, false) + w.app.SetInputCapture(w.c.Input) + w.app.SetFocus(w.articles) + } + + if strings.EqualFold(keyName, "enter") { + w.flexStatus.RemoveItem(inputField) + w.flexStatus.AddItem(w.status, 1, 1, false) + w.app.SetInputCapture(w.c.Input) + w.app.SetFocus(w.articles) + + w.feeds.Select(2, 0) + w.articles.Select(0, 3) + + } else { + w.currSearch += keyName + } + + return e + } + w.flexStatus.AddItem(inputField, 1, 0, false) + w.app.SetFocus(inputField) + w.app.SetInputCapture(capt) +} + +// AskQuit asks the user to quit or not. +func (w *Window) AskQuit() { + w.askQuit = true + w.flexStatus.RemoveItem(w.status) + + inputField := tview.NewInputField(). + SetLabel("Quit [Y/n]? "). + SetFieldWidth(5). + SetFieldBackgroundColor(tcell.ColorBlack) + + x := func(e *tcell.EventKey) *tcell.EventKey { + keyName := string(e.Name()) + if strings.Contains(keyName, "Rune") { + keyName = string(e.Rune()) + } + + if strings.EqualFold(keyName, "y") || strings.EqualFold(keyName, "enter") { + w.c.quit <- 1 + } + + w.flexStatus.RemoveItem(inputField) + w.flexStatus.AddItem(w.status, 1, 1, false) + w.app.SetInputCapture(w.c.Input) + w.app.SetFocus(w.articles) + + return e + } + w.flexStatus.AddItem(inputField, 1, 0, false) + w.app.SetFocus(inputField) + w.app.SetInputCapture(x) +} + // StatusUpdate updates the status window with updated information func (w *Window) StatusUpdate() { + if w.askQuit { + return + } // Update time c := w.status.GetCell(0, 0) c.SetText( @@ -557,12 +638,12 @@ func (w *Window) AddToArticles(a *Article, markedWeb bool) { tc.SetReference(a) w.articles.SetCell(w.nArticles, 3, tc) - if a.highlight { + if w.c.activeFeed == "result" { hTitle := "" fields := strings.Fields(a.title) for _, f := range fields { found := false - for _, h := range w.c.conf.Highlights { + for _, h := range strings.Fields(w.currSearch) { if strings.Contains(strings.ToLower(f), strings.ToLower(h)) { found = true } @@ -575,8 +656,26 @@ func (w *Window) AddToArticles(a *Article, markedWeb bool) { } tc.SetText(hTitle) } else { - tc.SetText(a.title) - + if a.highlight { + hTitle := "" + fields := strings.Fields(a.title) + for _, f := range fields { + found := false + for _, h := range w.c.conf.Highlights { + if strings.Contains(strings.ToLower(f), strings.ToLower(h)) { + found = true + } + } + if found { + hTitle += fmt.Sprintf("[%s]"+f+" [%s]", w.c.theme.Highlights, w.c.theme.Title) + } else { + hTitle += f + " " + } + } + tc.SetText(hTitle) + } else { + tc.SetText(a.title) + } } str := time.Since(a.published).Round(time.Minute).String() @@ -637,10 +736,10 @@ func (w *Window) AddPreview(a *Article) { // GetTime returns the timestring formatted as (%h%m < 24 hours < %d) func GetTime(ts string) string { - d_rex := regexp.MustCompile(`(\d+)h`) - d_res := d_rex.FindStringSubmatch(ts) - if len(d_res) > 0 { - if i, err := strconv.Atoi(d_res[1]); err == nil { + dDrex := regexp.MustCompile(`(\d+)h`) + dRes := dDrex.FindStringSubmatch(ts) + if len(dRes) > 0 { + if i, err := strconv.Atoi(dRes[1]); err == nil { if i > 23 { days := i / 24 return strconv.Itoa(days) + "d"