Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
17f0bc2f32 | ||
|
012c2de078 | ||
|
c7a575de70 | ||
|
d737967924 | ||
|
313902115b | ||
|
92a0ba369f | ||
|
3cf7ec32af | ||
|
5b3b9b998c | ||
|
15ccbc1e12 | ||
|
ceb3300227 | ||
|
45320c8265 | ||
|
15f667f75d | ||
|
e5d67e701b | ||
|
36eae6f980 | ||
|
46fb6eebe3 | ||
|
32969675c7 | ||
|
f5e9deb941 | ||
|
37b9fbfa29 | ||
|
d41de41c49 | ||
|
9eb31f4e08 | ||
|
780f26245c | ||
|
c9ba058ae6 | ||
|
d9474be233 | ||
|
2ec9bca107 | ||
|
61f9c2d3ce | ||
|
820255a18d | ||
|
afbfa53ae6 | ||
|
85bf323fe8 | ||
|
5dc140bcaa | ||
|
97dc2c571b | ||
|
71b8746c51 | ||
|
90a1c0e588 | ||
|
295dae27fd | ||
|
a13d212968 | ||
|
69139ee7f6 | ||
|
073db44b81 | ||
|
49ea48976d |
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal 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
27
.github/workflows/codeql-analysis.yml
vendored
Normal 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
52
.github/workflows/reviewdog.yml
vendored
Normal 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
18
.github/workflows/test.yml
vendored
Normal 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 ./...
|
@@ -4,6 +4,7 @@ go:
|
||||
- "1.14"
|
||||
before_script:
|
||||
- make build
|
||||
- make lint
|
||||
- make test
|
||||
deploy:
|
||||
- provider: script
|
||||
|
4
Makefile
4
Makefile
@@ -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
|
||||
|
48
README.md
48
README.md
@@ -1,32 +1,50 @@
|
||||
# srv
|
||||
|
||||
[](https://travis-ci.org/davegallant/srv)
|
||||
[](https://goreportcard.com/report/github.com/davegallant/srv)
|
||||
|
||||
View RSS feeds from the terminal.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## 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
|
||||
```
|
||||
|
218
cmd/start.go
218
cmd/start.go
@@ -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)
|
||||
}
|
||||
}
|
@@ -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
11
config-example.yml
Normal 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
118
config/config.go
Normal 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
|
||||
}
|
@@ -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
33
controller/controller.go
Normal 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
11
cui/colours.go
Normal 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
32
cui/keybindings.go
Normal 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
183
cui/main.go
Normal 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
39
cui/scroll.go
Normal 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
|
||||
}
|
@@ -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
13
file/file.go
Normal 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
43
file/file_test.go
Normal 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
22
go.mod
@@ -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
89
go.sum
@@ -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
6
install.sh
Executable 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
|
@@ -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
|
||||
}
|
@@ -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()
|
||||
}
|
4
main.go
4
main.go
@@ -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
23
utils/html.go
Normal 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
26
utils/html_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user