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"
|
- "1.14"
|
||||||
before_script:
|
before_script:
|
||||||
- make build
|
- make build
|
||||||
|
- make lint
|
||||||
- make test
|
- make test
|
||||||
deploy:
|
deploy:
|
||||||
- provider: script
|
- provider: script
|
||||||
|
4
Makefile
4
Makefile
@@ -8,3 +8,7 @@ build: ## Builds the binary
|
|||||||
test: ## Run unit tests
|
test: ## Run unit tests
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
.PHONY: test
|
.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
|
# srv
|
||||||
|
|
||||||
|
[](https://travis-ci.org/davegallant/srv)
|
||||||
|
[](https://goreportcard.com/report/github.com/davegallant/srv)
|
||||||
|
|
||||||
View RSS feeds from the terminal.
|
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
|
## 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.
|
If a configuration is not provided, a default configuration is generated.
|
||||||
|
|
||||||
- `feeds` is a list of RSS/Atom feeds to be loaded in srv.
|
- `feeds` is a list of RSS/Atom feeds to be loaded in srv.
|
||||||
- `externalViewer` defines an application to override the default web browser (optional).
|
- `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
|
## navigate
|
||||||
cp ./config-example.yaml ~/.config/srv/config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## control
|
|
||||||
|
|
||||||
Key mappings are statically defined for the time being.
|
Key mappings are statically defined for the time being.
|
||||||
|
|
||||||
- `TAB` switches between Feeds and Items.
|
| Key | Description |
|
||||||
- `UP/DOWN` navigates feeds and items`
|
|:---------:| --------------------------------------------------------------------- |
|
||||||
- `ENTER` either selects a feed or opens a feed item in an external application.
|
| `TAB` | switches between Feeds and Items. |
|
||||||
- `F5` refresh list of feeds
|
| `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
|
## build
|
||||||
|
|
||||||
@@ -39,3 +57,9 @@ make build
|
|||||||
```shell
|
```shell
|
||||||
make test
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -8,15 +8,14 @@ import (
|
|||||||
|
|
||||||
// TestLoadConfiguration tests loading the example config
|
// TestLoadConfiguration tests loading the example config
|
||||||
func TestLoadConfiguration(t *testing.T) {
|
func TestLoadConfiguration(t *testing.T) {
|
||||||
exampleConfig := LoadConfiguration("../config-example.yaml")
|
exampleConfig, err := LoadConfiguration("../config-example.yml")
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
expectedFeeds := []string{
|
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://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(
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,20 +12,15 @@ import (
|
|||||||
|
|
||||||
type RSS struct {
|
type RSS struct {
|
||||||
Feeds []*gofeed.Feed
|
Feeds []*gofeed.Feed
|
||||||
c *Controller
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RSS) New(c *Controller) {
|
|
||||||
r.c = c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update fetches all articles for all feeds
|
// Update fetches all articles for all feeds
|
||||||
func (r *RSS) Update() {
|
func (r *RSS) Update(feeds []string) {
|
||||||
fp := gofeed.NewParser()
|
fp := gofeed.NewParser()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var mux sync.Mutex
|
var mux sync.Mutex
|
||||||
r.Feeds = []*gofeed.Feed{}
|
r.Feeds = []*gofeed.Feed{}
|
||||||
for _, f := range r.c.Config.Feeds {
|
for _, f := range feeds {
|
||||||
f := f
|
f := f
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
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 (
|
require (
|
||||||
github.com/EDDYCJY/fake-useragent v0.2.0
|
github.com/EDDYCJY/fake-useragent v0.2.0
|
||||||
github.com/PuerkitoBio/goquery v1.5.1 // indirect
|
github.com/jroimartin/gocui v0.5.0
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f
|
||||||
github.com/jroimartin/gocui v0.4.0
|
github.com/juju/testing v0.0.0-20200608005635-e4eedbc6f7aa // indirect
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.8 // indirect
|
github.com/mmcdole/gofeed v1.1.3
|
||||||
github.com/mmcdole/gofeed v1.0.0-beta2
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
|
github.com/spf13/afero v1.6.0
|
||||||
github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be // indirect
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/stretchr/testify v1.5.1
|
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
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-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gopkg.in/yaml.v2 v2.2.8
|
|
||||||
)
|
)
|
||||||
|
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 h1:Jcnkk2bgXmDpX0z+ELlUErTkoLb/mxFBNd2YdcpvJBs=
|
||||||
github.com/EDDYCJY/fake-useragent v0.2.0/go.mod h1:5wn3zzlDxhKW6NYknushqinPcAqZcAPHy8lLczCdJdc=
|
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 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
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 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
|
github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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/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/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mmcdole/gofeed v1.0.0-beta2 h1:CjQ0ADhAwNSb08zknAkGOEYqr8zfZKfrzgk9BxpWP2E=
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/mmcdole/gofeed v1.0.0-beta2/go.mod h1:/BF9JneEL2/flujm8XHoxUcghdTV6vvb3xx/vKyChFU=
|
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 h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
|
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
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-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-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-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 h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
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-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/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.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.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=
|
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 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-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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
|
package main
|
||||||
|
|
||||||
import "github.com/davegallant/srv/cmd"
|
import cui "github.com/davegallant/srv/cui"
|
||||||
|
|
||||||
func main() {
|
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