37 Commits

Author SHA1 Message Date
dependabot[bot]
17f0bc2f32 Bump github.com/stretchr/testify from 1.7.0 to 1.8.1 (#51)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.8.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-02 18:12:03 -05:00
dependabot[bot]
012c2de078 Bump github.com/jroimartin/gocui from 0.4.0 to 0.5.0 (#36)
Bumps [github.com/jroimartin/gocui](https://github.com/jroimartin/gocui) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/jroimartin/gocui/releases)
- [Commits](https://github.com/jroimartin/gocui/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: github.com/jroimartin/gocui
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-16 06:53:15 -04:00
dependabot[bot]
c7a575de70 Bump github.com/mmcdole/gofeed from 1.1.1 to 1.1.3 (#35)
Bumps [github.com/mmcdole/gofeed](https://github.com/mmcdole/gofeed) from 1.1.1 to 1.1.3.
- [Release notes](https://github.com/mmcdole/gofeed/releases)
- [Commits](https://github.com/mmcdole/gofeed/compare/v1.1.1...v1.1.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 07:42:50 -04:00
dependabot[bot]
d737967924 Bump github.com/mmcdole/gofeed from 1.1.0 to 1.1.1 (#34)
Bumps [github.com/mmcdole/gofeed](https://github.com/mmcdole/gofeed) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/mmcdole/gofeed/releases)
- [Commits](https://github.com/mmcdole/gofeed/compare/v1.1.0...v1.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-09 07:21:25 -04:00
dependabot[bot]
313902115b Bump github.com/spf13/afero from 1.5.1 to 1.6.0 (#33)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.5.1...v1.6.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 15:08:45 -04:00
dependabot[bot]
92a0ba369f Bump github.com/stretchr/testify from 1.6.1 to 1.7.0 (#32)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.6.1...v1.7.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-13 17:49:37 -05:00
dependabot-preview[bot]
3cf7ec32af Create Dependabot config file (#31)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-12-31 10:42:34 -05:00
dependabot-preview[bot]
5b3b9b998c Bump github.com/spf13/afero from 1.5.0 to 1.5.1 (#30)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.5.0 to 1.5.1.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.5.0...v1.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-12-10 23:46:31 -05:00
dependabot-preview[bot]
15ccbc1e12 Bump github.com/spf13/afero from 1.4.1 to 1.5.0 (#29)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.4.1 to 1.5.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.4.1...v1.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-12-09 21:21:00 -05:00
dependabot-preview[bot]
ceb3300227 Bump gopkg.in/yaml.v2 from 2.3.0 to 2.4.0 (#28)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.3.0...v2.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-11-26 08:00:30 -05:00
Shay Elmualem
45320c8265 Add test cases (#26)
* Add tests for the Exists function

* Add tests for the StripHTMLTags function
2020-10-07 09:13:57 -04:00
Dave Gallant
15f667f75d Add actions/setup-go@v2 (#27)
* Run tests for Ubuntu and macOS
2020-10-07 09:07:10 -04:00
dependabot-preview[bot]
e5d67e701b Bump github.com/spf13/afero from 1.4.0 to 1.4.1 (#25)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.4.0...v1.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-10-06 09:15:33 -04:00
dependabot-preview[bot]
36eae6f980 Bump github.com/spf13/afero from 1.3.5 to 1.4.0 (#24)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.3.5 to 1.4.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.3.5...v1.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-15 07:39:27 -04:00
dependabot-preview[bot]
46fb6eebe3 Bump github.com/mmcdole/gofeed from 1.0.0 to 1.1.0 (#23)
Bumps [github.com/mmcdole/gofeed](https://github.com/mmcdole/gofeed) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/mmcdole/gofeed/releases)
- [Commits](https://github.com/mmcdole/gofeed/compare/v1.0.0...v1.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-07 09:07:22 -04:00
dependabot-preview[bot]
32969675c7 Bump github.com/spf13/afero from 1.3.4 to 1.3.5 (#22)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.3.4...v1.3.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-03 08:38:56 -04:00
dependabot-preview[bot]
f5e9deb941 Bump github.com/spf13/afero from 1.3.3 to 1.3.4 (#21)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.3.3...v1.3.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-10 21:48:49 -04:00
dependabot-preview[bot]
37b9fbfa29 Bump github.com/spf13/afero from 1.3.2 to 1.3.3 (#20)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.3.2 to 1.3.3.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.3.2...v1.3.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-03 10:51:04 -04:00
Dave Gallant
d41de41c49 Display error in feed description if external viewer is not found (#19) 2020-07-20 22:56:09 -04:00
dependabot-preview[bot]
9eb31f4e08 Bump github.com/spf13/afero from 1.3.1 to 1.3.2 (#18)
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.3.1...v1.3.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-14 22:00:58 -04:00
Dave Gallant
780f26245c Fix refresh and add install script (#17)
* Fix refresh bug

* Add install script
2020-07-07 23:07:09 -04:00
Dave Gallant
c9ba058ae6 Remove config-example.yaml 2020-07-06 13:26:27 -04:00
Dave Gallant
d9474be233 Add and enforce golangci-lint (#16) 2020-07-05 22:44:05 -04:00
Dave Gallant
2ec9bca107 Add codeql analysis (#15) 2020-07-04 17:21:25 -04:00
Dave Gallant
61f9c2d3ce Simplify scroll (#14) 2020-07-04 16:51:29 -04:00
Dave Gallant
820255a18d Fix scroll bug (#13) 2020-07-04 01:05:23 -04:00
Dave Gallant
afbfa53ae6 Add feed description view (#12)
with colors!
2020-07-03 23:25:48 -04:00
dependabot-preview[bot]
85bf323fe8 Bump github.com/stretchr/testify from 1.5.1 to 1.6.1 (#11)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.5.1 to 1.6.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.5.1...v1.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Dave Gallant <davegallant@gmail.com>
2020-06-23 15:01:27 -04:00
dependabot-preview[bot]
5dc140bcaa Bump gopkg.in/yaml.v2 from 2.2.8 to 2.3.0 (#10)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.8 to 2.3.0.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.8...v2.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-23 14:56:17 -04:00
dependabot-preview[bot]
97dc2c571b Bump github.com/mmcdole/gofeed from 1.0.0-beta2 to 1.0.0 (#9)
Bumps [github.com/mmcdole/gofeed](https://github.com/mmcdole/gofeed) from 1.0.0-beta2 to 1.0.0.
- [Release notes](https://github.com/mmcdole/gofeed/releases)
- [Commits](https://github.com/mmcdole/gofeed/compare/v1.0.0-beta2...v1.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-23 14:51:58 -04:00
Dave Gallant
71b8746c51 Add instructions on how to install 2020-04-12 18:04:23 -04:00
Dave Gallant
90a1c0e588 Add Go report card 2020-04-10 10:58:47 -04:00
Dave Gallant
295dae27fd Reset origin on feed load 2020-04-08 00:50:44 -04:00
Dave Gallant
a13d212968 Determine when to apply autoscroll
When shrinking the window, scrolling was limited to only the current
view buffer. This should implement auto-scrolling. It's a little bit
janky, but should be sufficient for now.
2020-04-08 00:21:18 -04:00
Dave Gallant
69139ee7f6 Add reviewdog golangci-lint 2020-04-05 19:33:36 -04:00
Dave Gallant
073db44b81 Allow for optional external viewer args
In order to pass in options like `--new-window` to an external viewer like
`firefox`, optional args can now be defined in configuration. See
config-example.yaml for an example.
2020-04-04 23:08:43 -04:00
Dave Gallant
49ea48976d Re-structure packages
Overhaul of package structure and other cleanup.
2020-04-04 20:19:46 -04:00
28 changed files with 773 additions and 394 deletions

8
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "10:00"
open-pull-requests-limit: 10

27
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 4 * * 1'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 2
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

52
.github/workflows/reviewdog.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: reviewdog
on: [pull_request]
jobs:
# NOTE: golangci-lint doesn't report multiple errors on the same line from
# different linters and just report one of the errors?
golangci-lint:
name: runner / golangci-lint
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: golangci-lint
uses: docker://reviewdog/action-golangci-lint:v1 # Pre-built image
# uses: reviewdog/action-golangci-lint@v1 # Build with Dockerfile
# uses: docker://reviewdog/action-golangci-lint:v1.0.2 # Can use specific version.
# uses: reviewdog/action-golangci-lint@v1.0.2 # Can use specific version.
with:
github_token: ${{ secrets.github_token }}
# Can pass --config flag to change golangci-lint behavior and target
# directory.
golangci_lint_flags: "--config=.github/.golangci.yml ./testdata"
# Use golint via golangci-lint binary with "warning" level.
golint:
name: runner / golint
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: golint
uses: reviewdog/action-golangci-lint@v1
with:
github_token: ${{ secrets.github_token }}
golangci_lint_flags: "--disable-all -E golint"
tool_name: golint # Change reporter name.
level: warning # GitHub Status Check won't become failure with this level.
# You can add more and more supported linters with different config.
errcheck:
name: runner / errcheck
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: errcheck
uses: reviewdog/action-golangci-lint@v1
with:
github_token: ${{ secrets.github_token }}
golangci_lint_flags: "--disable-all -E errcheck"
tool_name: errcheck
level: info

18
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.14.x, 1.15.x]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: go test ./...

View File

@@ -4,6 +4,7 @@ go:
- "1.14"
before_script:
- make build
- make lint
- make test
deploy:
- provider: script

View File

@@ -8,3 +8,7 @@ build: ## Builds the binary
test: ## Run unit tests
go test -v ./...
.PHONY: test
lint: ## Run lint
go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0
golangci-lint run

View File

@@ -1,32 +1,50 @@
# srv
[![Build Status](https://travis-ci.org/davegallant/srv.svg?branch=master)](https://travis-ci.org/davegallant/srv)
[![Go Report Card](https://goreportcard.com/badge/github.com/davegallant/srv)](https://goreportcard.com/report/github.com/davegallant/srv)
View RSS feeds from the terminal.
![image](https://user-images.githubusercontent.com/4519234/77839285-49077c00-7149-11ea-80ad-76efda38615e.png)
![image](https://user-images.githubusercontent.com/4519234/86504202-b861bd00-bd83-11ea-8a8e-4f28e38a71ce.png)
## install
### via releases
```shell
curl -fsSL https://raw.githubusercontent.com/davegallant/srv/master/install.sh | bash
```
### via go
```shell
go get github.com/davegallant/srv
```
## configure
srv reads configuration from `~/.config/srv/config.yaml`
srv reads configuration from `~/.config/srv/config.yml`
If a configuration is not provided, a default configuration is generated.
- `feeds` is a list of RSS/Atom feeds to be loaded in srv.
- `externalViewer` defines an application to override the default web browser (optional).
An example config can be copied:
An example config can be found [here](./config-example.yml).
```shell
cp ./config-example.yaml ~/.config/srv/config.yaml
```
## control
## navigate
Key mappings are statically defined for the time being.
- `TAB` switches between Feeds and Items.
- `UP/DOWN` navigates feeds and items`
- `ENTER` either selects a feed or opens a feed item in an external application.
- `F5` refresh list of feeds
| Key | Description |
|:---------:| --------------------------------------------------------------------- |
| `TAB` | switches between Feeds and Items. |
| `UP/DOWN` | navigates feeds and items` |
| `ENTER` | either selects a feed or opens a feed item in an external application.|
| `CTRL+R` | refresh list of feeds |
| `CTRL+C` | quit |
## build
@@ -39,3 +57,9 @@ make build
```shell
make test
```
## lint
```shell
make lint
```

View File

@@ -1,218 +0,0 @@
package cmd
import (
"fmt"
"log"
"os/exec"
"os/user"
"github.com/davegallant/srv/internal"
"github.com/jroimartin/gocui"
)
// Controller can access Feeds and Config
var Controller *internal.Controller
var (
viewArr = []string{"feeds", "Items"}
active = 0
currentFeed = 0 // TODO: move to Controller
)
func cursorDown(g *gocui.Gui, v *gocui.View) error {
if v != nil {
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy+1); err != nil {
v.SetCursor(cx, cy-1)
}
}
return nil
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
if v != nil {
ox, oy := v.Origin()
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
if err := v.SetOrigin(ox, oy-1); err != nil {
return err
}
}
}
return nil
}
// openFeed opens all items in the feed
func openFeed(g *gocui.Gui, v *gocui.View) error {
_, cy := v.Cursor()
currentFeed = cy
feed := Controller.Rss.Feeds[currentFeed]
ov, _ := g.View("Items")
ov.Clear()
for _, item := range feed.Items {
fmt.Fprintln(ov, "-", item.Title)
}
nextView(g, ov)
return nil
}
// openItem opens the feed in an external browser
func openItem(g *gocui.Gui, v *gocui.View) error {
_, cy := v.Cursor()
item := Controller.Rss.Feeds[currentFeed].Items[cy]
viewer := Controller.Config.ExternalViewer
err := exec.Command(viewer, item.Link).Start()
if err != nil {
log.Fatal(err)
}
return nil
}
func showLoading(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("loading", maxX/2-4, maxY/2-1, maxX/2+4, maxY/2+1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
fmt.Fprintln(v, "Loading")
}
return nil
}
func hideLoading(g *gocui.Gui) error {
if err := g.DeleteView("loading"); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
return nil
}
func refreshFeeds(g *gocui.Gui, v *gocui.View) error {
showLoading(g)
Controller.Rss.Update()
//hideLoading(g)
return nil
}
// statically map all of the keys
func keybindings(g *gocui.Gui) error {
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
return err
}
if err := g.SetKeybinding("feeds", gocui.KeyEnter, gocui.ModNone, openFeed); err != nil {
return err
}
if err := g.SetKeybinding("Items", gocui.KeyEnter, gocui.ModNone, openItem); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyF5, gocui.ModNone, refreshFeeds); err != nil {
return err
}
return nil
}
func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
if _, err := g.SetCurrentView(name); err != nil {
return nil, err
}
return g.SetViewOnTop(name)
}
func nextView(g *gocui.Gui, v *gocui.View) error {
nextIndex := (active + 1) % len(viewArr)
name := viewArr[nextIndex]
_, err := g.View("Items")
if err != nil {
return err
}
if _, err := setCurrentViewOnTop(g, name); err != nil {
return err
}
if nextIndex == 0 || nextIndex == 3 {
g.Cursor = true
} else {
g.Cursor = false
}
active = nextIndex
return nil
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("feeds", 0, 0, maxX-1, maxY/4-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Feeds"
v.Highlight = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
if _, err = setCurrentViewOnTop(g, "feeds"); err != nil {
return err
}
for _, f := range Controller.Rss.Feeds {
fmt.Fprintln(v, "-", f.Title)
}
}
if v, err := g.SetView("Items", 0, maxY/4, maxX-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Items"
v.Highlight = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
// Start initializes the application
func Start() {
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
configPath := usr.HomeDir + "/.config/srv/config.yaml"
Controller = &internal.Controller{}
Controller.Init(configPath)
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetManagerFunc(layout)
if err := keybindings(g); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}

View File

@@ -1,11 +0,0 @@
---
feeds:
- https://news.ycombinator.com/rss
- https://www.reddit.com/r/golang/.rss
- https://www.reddit.com/r/linux/.rss
- https://www.zdnet.com/topic/security/rss.xml
- https://aws.amazon.com/blogs/security/feed/
- https://www.archlinux.org/feeds/news/
# Optionally define an an external application viewer
#externalViewer: firefox

11
config-example.yml Normal file
View File

@@ -0,0 +1,11 @@
---
feeds:
- https://aws.amazon.com/blogs/security/feed/
- https://www.phoronix.com/rss.php
- https://www.zdnet.com/topic/security/rss.xml
# Optionally define an application to view the feeds
#externalViewer: firefox
# Optionally define args for the external viewer
#externalViewerArgs:
#- --new-window

118
config/config.go Normal file
View File

@@ -0,0 +1,118 @@
package config
import (
"github.com/juju/errors"
"io/ioutil"
"log"
"os"
"os/user"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/davegallant/srv/file"
"github.com/spf13/afero"
"gopkg.in/yaml.v2"
)
// ConfigPath defines where the configuration is stored
const ConfigPath = ".config/srv/config.yml"
// Configuration stores the global config
type Configuration struct {
Feeds []string `yaml:"feeds"`
ExternalViewer string `yaml:"externalViewer,omitempty"`
ExternalViewerArgs []string `yaml:"externalViewerArgs,omitempty"`
}
// DefaultConfiguration can be used if a config is missing
var DefaultConfiguration = Configuration{
Feeds: []string{
"https://aws.amazon.com/blogs/security/feed/",
"https://www.phoronix.com/rss.php",
"https://www.zdnet.com/topic/security/rss.xml",
},
}
// GetUGetUGetUserConfigPath returns the full configuration path for the current user
func GetUserConfigPath() (string, error) {
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
createConfigPath := []string{usr.HomeDir}
createConfigPath = append(createConfigPath, strings.Split(ConfigPath, "/")...)
return path.Join(createConfigPath...), nil
}
// DetermineExternalViewer checks the OS to decide the default viewer
func DetermineExternalViewer() (string, error) {
switch os := runtime.GOOS; os {
case "linux":
return "xdg-open", nil
case "darwin":
return "open", nil
}
return "", errors.New("Unable to determine a default external viewer")
}
// EnsureConfigDirExists ensures directory exists with correct permissions
func EnsureConfigDirExists(d string) error {
var AppFs = afero.NewOsFs()
return AppFs.MkdirAll(d, 0700)
}
// LoadConfiguration loads a configuration from a file
func LoadConfiguration(f string) (Configuration, error) {
var config Configuration
if !file.Exists(f) {
err := WriteConfig(DefaultConfiguration, f)
return DefaultConfiguration, errors.Annotate(err, "failed to load configuration")
}
data, err := ioutil.ReadFile(f)
if err != nil {
log.Println(err)
os.Exit(1)
}
if config.ExternalViewer == "" {
config.ExternalViewer, err = DetermineExternalViewer()
if err != nil {
log.Println(err)
os.Exit(1)
}
}
err = yaml.Unmarshal(data, &config)
if err != nil {
log.Panicln(err)
}
return config, nil
}
// WriteConfig writes a config to disk
func WriteConfig(config Configuration, f string) error {
d := filepath.Dir(f)
err := EnsureConfigDirExists(d)
if err != nil {
return errors.Annotatef(err, "Unable to to create config directory '%s'", d)
}
c, err := yaml.Marshal(&config)
if err != nil {
return errors.Annotatef(err, "Unable to marshal config '%s'", f)
}
err = ioutil.WriteFile(f, c, 0600)
if err != nil {
return errors.Annotatef(err, "Unable to write default config: '%s'", f)
}
return nil
}

View File

@@ -1,4 +1,4 @@
package internal
package config
import (
"testing"
@@ -8,15 +8,14 @@ import (
// TestLoadConfiguration tests loading the example config
func TestLoadConfiguration(t *testing.T) {
exampleConfig := LoadConfiguration("../config-example.yaml")
exampleConfig, err := LoadConfiguration("../config-example.yml")
assert.NoError(t, err)
expectedFeeds := []string{
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/golang/.rss",
"https://www.reddit.com/r/linux/.rss",
"https://www.zdnet.com/topic/security/rss.xml",
"https://aws.amazon.com/blogs/security/feed/",
"https://www.archlinux.org/feeds/news/",
"https://www.phoronix.com/rss.php",
"https://www.zdnet.com/topic/security/rss.xml",
}
assert.Equal(

33
controller/controller.go Normal file
View File

@@ -0,0 +1,33 @@
package controller
import (
"log"
config "github.com/davegallant/srv/config"
feeds "github.com/davegallant/srv/feeds"
)
// Controller keeps everything together
type Controller struct {
Config config.Configuration
Rss *feeds.RSS
CurrentFeed int
}
// Init initiates the controller
func (c *Controller) Init() {
configPath, err := config.GetUserConfigPath()
if err != nil {
log.Fatal("Unable to locate user's config path")
}
c.Config, err = config.LoadConfiguration(configPath)
if err != nil {
log.Fatalf("Unable to load configuration: %s", err)
}
c.Rss = &feeds.RSS{}
c.Rss.Update(c.Config.Feeds)
c.CurrentFeed = 0
}

11
cui/colours.go Normal file
View File

@@ -0,0 +1,11 @@
package cui
import "github.com/jroimartin/gocui"
const (
feedItemSelectionBgColor = gocui.ColorCyan
feedItemSelectionFgColor = gocui.ColorBlack
feedNameSelectionBgColor = gocui.ColorWhite
feedNameSelectionFgColor = gocui.ColorBlack
descriptionFgColor = gocui.ColorCyan
)

32
cui/keybindings.go Normal file
View File

@@ -0,0 +1,32 @@
package cui
import "github.com/jroimartin/gocui"
// Map keys to actions
func keybindings(g *gocui.Gui) error {
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlQ, gocui.ModNone, quit); err != nil {
return err
}
if err := g.SetKeybinding("feeds", gocui.KeyEnter, gocui.ModNone, openFeed); err != nil {
return err
}
if err := g.SetKeybinding("Items", gocui.KeyEnter, gocui.ModNone, openItem); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, refreshFeeds); err != nil {
return err
}
return nil
}

183
cui/main.go Normal file
View File

@@ -0,0 +1,183 @@
package cui
import (
"fmt"
"log"
"os/exec"
"github.com/davegallant/srv/controller"
"github.com/davegallant/srv/utils"
"github.com/jroimartin/gocui"
"github.com/mmcdole/gofeed"
)
// Controller can access internal state
var Controller *controller.Controller
var (
viewArr = []string{"feeds", "Items"}
active = 0
)
// openFeed opens all items in the feed
func openFeed(g *gocui.Gui, v *gocui.View) error {
_, oy := v.Origin()
feed := Controller.Rss.Feeds[oy]
Controller.CurrentFeed = oy
ov, _ := g.View("Items")
ov.Clear()
if err := ov.SetOrigin(0, 0); err != nil {
log.Fatal(err)
}
for _, item := range feed.Items {
fmt.Fprintln(ov, "-", item.Title)
}
err := nextView(g, ov)
if err != nil {
log.Printf("Unable to get next view: %s", err)
}
displayDescription(g, ov)
return nil
}
func getCurrentFeedItem(v *gocui.View) *gofeed.Item {
_, oy := v.Origin()
return Controller.Rss.Feeds[Controller.CurrentFeed].Items[oy]
}
// displayDescription displays feed description if it exists
func displayDescription(g *gocui.Gui, v *gocui.View) {
item := getCurrentFeedItem(v)
description := utils.StripHTMLTags(item.Description)
setDescription(g, v, description)
}
// setDescription displays text in the bottom panel
func setDescription(g *gocui.Gui, v *gocui.View, description string) {
ov, _ := g.View("Description")
ov.Clear()
fmt.Fprintln(ov, description)
}
// openItem opens the feed in an external browser
func openItem(g *gocui.Gui, v *gocui.View) error {
item := getCurrentFeedItem(v)
err := exec.Command(
Controller.Config.ExternalViewer,
append(Controller.Config.ExternalViewerArgs, item.Link)...).Start()
if err != nil {
setDescription(g, v, err.Error())
}
return nil
}
func refreshFeeds(g *gocui.Gui, v *gocui.View) error {
g.Close()
Start()
return nil
}
func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
if _, err := g.SetCurrentView(name); err != nil {
return nil, err
}
return g.SetViewOnTop(name)
}
func nextView(g *gocui.Gui, v *gocui.View) error {
nextIndex := (active + 1) % len(viewArr)
name := viewArr[nextIndex]
_, err := g.View("Items")
if err != nil {
return err
}
if _, err := setCurrentViewOnTop(g, name); err != nil {
return err
}
if nextIndex == 0 || nextIndex == 3 {
g.Cursor = true
} else {
g.Cursor = false
}
active = nextIndex
return nil
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("feeds", 0, 0, maxX-1, maxY/3-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Highlight = true
v.SelBgColor = feedNameSelectionBgColor
v.SelFgColor = feedNameSelectionFgColor
v.Title = "Feeds"
if _, err = setCurrentViewOnTop(g, "feeds"); err != nil {
return err
}
for _, f := range Controller.Rss.Feeds {
fmt.Fprintln(v, "-", f.Title)
}
}
if v, err := g.SetView("Items", 0, maxY/3, maxX-1, maxY-(maxY/5)-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Highlight = true
v.SelBgColor = feedItemSelectionBgColor
v.SelFgColor = feedItemSelectionFgColor
v.Title = "Items"
}
if v, err := g.SetView("Description", 0, maxY-maxY/5, maxX-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.SelBgColor = feedItemSelectionBgColor
v.SelFgColor = feedItemSelectionFgColor
v.Title = "Description"
v.FgColor = descriptionFgColor
v.Wrap = true
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
// Start initializes the application
func Start() {
Controller = &controller.Controller{}
Controller.Init()
g, err := gocui.NewGui(gocui.Output256)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetManagerFunc(layout)
if err := keybindings(g); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}

39
cui/scroll.go Normal file
View File

@@ -0,0 +1,39 @@
package cui
import (
"github.com/jroimartin/gocui"
)
func scroll(v *gocui.View, direction int) error {
if v != nil {
ox, oy := v.Origin()
if oy+direction >= len(v.BufferLines())-1 {
// hit bottom
return nil
}
if oy+direction < 0 {
// hit top
return nil
}
if err := v.SetOrigin(ox, oy+direction); err != nil {
return err
}
}
return nil
}
func cursorDown(g *gocui.Gui, v *gocui.View) error {
err := scroll(v, 1)
if g.CurrentView().Title == "Items" {
displayDescription(g, v)
}
return err
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
err := scroll(v, -1)
if g.CurrentView().Title == "Items" {
displayDescription(g, v)
}
return err
}

View File

@@ -1,4 +1,4 @@
package internal
package feeds
import (
"fmt"
@@ -12,20 +12,15 @@ import (
type RSS struct {
Feeds []*gofeed.Feed
c *Controller
}
func (r *RSS) New(c *Controller) {
r.c = c
}
// Update fetches all articles for all feeds
func (r *RSS) Update() {
func (r *RSS) Update(feeds []string) {
fp := gofeed.NewParser()
var wg sync.WaitGroup
var mux sync.Mutex
r.Feeds = []*gofeed.Feed{}
for _, f := range r.c.Config.Feeds {
for _, f := range feeds {
f := f
wg.Add(1)
go func() {

13
file/file.go Normal file
View File

@@ -0,0 +1,13 @@
// Package file contains filesystem functions
package file
import (
"github.com/spf13/afero"
)
// Exists returns true if a file exists
func Exists(filename string) bool {
var AppFs = afero.NewOsFs()
_, err := AppFs.Stat(filename)
return err == nil
}

43
file/file_test.go Normal file
View File

@@ -0,0 +1,43 @@
package file
import (
"fmt"
"testing"
"time"
"github.com/spf13/afero"
)
var FakeFs = afero.NewOsFs()
func TestExists(t *testing.T) {
t.Run("expects file to exist", func(t *testing.T) {
input, _ := afero.TempFile(FakeFs, ".", "")
defer removeFile(input.Name(), t)
got := Exists(input.Name())
expect := true
if got != expect {
t.Errorf("Expected file %s to exist.", input.Name())
}
})
t.Run("expects file to not exist", func(t *testing.T) {
input := fmt.Sprintf("test_file_%d", time.Now().UnixNano())
got := Exists(input)
expect := false
if got != expect {
t.Errorf("Expected file %s to not exist", input)
}
})
}
func removeFile(name string, t *testing.T) {
err := FakeFs.Remove(name)
if err != nil {
t.Errorf("Did not cleanly delete file %s", name)
}
}

22
go.mod
View File

@@ -4,17 +4,15 @@ go 1.14
require (
github.com/EDDYCJY/fake-useragent v0.2.0
github.com/PuerkitoBio/goquery v1.5.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jroimartin/gocui v0.4.0
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-runewidth v0.0.8 // indirect
github.com/mmcdole/gofeed v1.0.0-beta2
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be // indirect
github.com/stretchr/testify v1.5.1
github.com/jroimartin/gocui v0.5.0
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f
github.com/juju/testing v0.0.0-20200608005635-e4eedbc6f7aa // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mmcdole/gofeed v1.1.3
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/spf13/afero v1.6.0
github.com/stretchr/testify v1.8.1
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
golang.org/x/text v0.3.2 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.4.0
)

89
go.sum
View File

@@ -1,49 +1,96 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/EDDYCJY/fake-useragent v0.2.0 h1:Jcnkk2bgXmDpX0z+ELlUErTkoLb/mxFBNd2YdcpvJBs=
github.com/EDDYCJY/fake-useragent v0.2.0/go.mod h1:5wn3zzlDxhKW6NYknushqinPcAqZcAPHy8lLczCdJdc=
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/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
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/juju/clock v0.0.0-20180524022203-d293bb356ca4/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM=
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9 h1:Y+lzErDTURqeXqlqYi4YBYbDd7ycU74gW1ADt57/bgY=
github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/retry v0.0.0-20160928201858-1998d01ba1c3/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/testing v0.0.0-20200608005635-e4eedbc6f7aa h1:v1ZEHRVaUgTIkxzYaT78fJ+3bV3vjxj9jfNJcYzi9pY=
github.com/juju/testing v0.0.0-20200608005635-e4eedbc6f7aa/go.mod h1:hpGvhGHPVbNBraRLZEhoQwFLMrjK8PSlO4D3nDjKYXo=
github.com/juju/utils v0.0.0-20180808125547-9dfc6dbfb02b/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mmcdole/gofeed v1.0.0-beta2 h1:CjQ0ADhAwNSb08zknAkGOEYqr8zfZKfrzgk9BxpWP2E=
github.com/mmcdole/gofeed v1.0.0-beta2/go.mod h1:/BF9JneEL2/flujm8XHoxUcghdTV6vvb3xx/vKyChFU=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
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/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be h1:yzmWtPyxEUIKdZg4RcPq64MfS8NA6A5fNOJgYhpR9EQ=
github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4 h1:hILp2hNrRnYjZpmIbx70psAHbBSEcQ1NIzDcUbJ1b6g=
gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

6
install.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
VERSION='0.1.2'
sudo curl --progress-bar \
-L "https://github.com/davegallant/srv/releases/download/v${VERSION}/srv_${VERSION}_$(uname -s)_x86_64.tar.gz" | \
sudo tar -C /usr/bin --overwrite -xvzf - srv

View File

@@ -1,85 +0,0 @@
package internal
import (
"errors"
"io/ioutil"
"log"
"os"
"runtime"
"gopkg.in/yaml.v2"
)
// Configuration stores the global config
type Configuration struct {
Feeds []string `yaml:"feeds"`
ExternalViewer string `yaml:"externalViewer,omitempty"`
}
// DefaultConfiguration can be used if a config is missing
var DefaultConfiguration = Configuration{
Feeds: []string{
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/golang/.rss",
"https://www.zdnet.com/topic/security/rss.xml",
},
}
// Determines the default viewer
func DetermineExternalViewer() (string, error) {
switch os := runtime.GOOS; os {
case "linux":
return "xdg-open", nil
case "darwin":
return "open", nil
}
return "", errors.New("Unable to determine a default external viewer")
}
// LoadConfiguration takes a filename (configuration) and loads it.
func LoadConfiguration(file string) Configuration {
var config Configuration
// If the configuration file does not exist,
// write a default config
_, err := os.Stat(file)
if os.IsNotExist(err) {
WriteConfig(DefaultConfiguration, file)
}
data, err := ioutil.ReadFile(file)
if err != nil {
log.Println(err)
os.Exit(1)
}
if config.ExternalViewer == "" {
config.ExternalViewer, err = DetermineExternalViewer()
if err != nil {
log.Println(err)
os.Exit(1)
}
}
err = yaml.Unmarshal(data, &config)
if err != nil {
log.Panicln(err)
}
return config
}
// WriteConfig writes a config to disk
func WriteConfig(config Configuration, file string) error {
c, err := yaml.Marshal(&config)
if err != nil {
log.Fatalf("Unable to marshal default config: %v", err)
}
err = ioutil.WriteFile(file, c, 0644)
if err != nil {
log.Fatalf("Unable to write default config: %v", err)
}
return nil
}

View File

@@ -1,18 +0,0 @@
package internal
import "time"
// Controller keeps everything together
type Controller struct {
Config Configuration
lastUpdate time.Time
Rss *RSS
}
// Init initiates the controller with config
func (c *Controller) Init(config string) {
c.Config = LoadConfiguration(config)
c.Rss = &RSS{}
c.Rss.New(c)
c.Rss.Update()
}

View File

@@ -1,9 +1,9 @@
package main
import "github.com/davegallant/srv/cmd"
import cui "github.com/davegallant/srv/cui"
func main() {
cmd.Start()
cui.Start()
}

23
utils/html.go Normal file
View File

@@ -0,0 +1,23 @@
package utils
import (
"regexp"
"sort"
"strings"
)
// StripHTMLTags uses regex to strip all html elements
func StripHTMLTags(s string) string {
const pattern = `(<\/?[a-zA-A]+?[^>]*\/?>)*`
r := regexp.MustCompile(pattern)
groups := r.FindAllString(s, -1)
sort.Slice(groups, func(i, j int) bool {
return len(groups[i]) > len(groups[j])
})
for _, group := range groups {
if strings.TrimSpace(group) != "" {
s = strings.ReplaceAll(s, group, "")
}
}
return s
}

26
utils/html_test.go Normal file
View File

@@ -0,0 +1,26 @@
package utils
import "testing"
func TestStripHTMLTags(t *testing.T) {
testCases := []struct {
htmlInput string
expect string
}{
{htmlInput: "<html><body<h1>Hello World!</h1></body></html>",
expect: "Hello World!"},
{htmlInput: "<h1>Hello World!</h1>",
expect: "Hello World!"},
{htmlInput: "<h1 style='color: #5e9ca0';>Hello World!</h1>",
expect: "Hello World!"},
{htmlInput: "<td><img style='margin: 1px 15px;' src='images/smiley.png' alt='laughing' width='40' height='16' /><strong>Hello World!</strong></td>", expect: "Hello World!"},
}
for _, tc := range testCases {
got := StripHTMLTags(tc.htmlInput)
expect := tc.expect
if got != expect {
t.Errorf("Expected %s, got %s", expect, got)
}
}
}