Verified Commit 404e6f2c authored by rakshazi's avatar rakshazi
Browse files

Init

parents
stages:
- test
- release
release:
stage: release
image: docker:stable
services:
- docker:dind
only:
refs:
- tags
variables:
DOCKER_REGISTRY: $CI_REGISTRY
DOCKER_USERNAME: $CI_REGISTRY_USER
DOCKER_PASSWORD: $CI_REGISTRY_PASSWORD
GIT_DEPTH: 0
script:
- docker run --rm --privileged -v $PWD:/app -w /app -v /var/run/docker.sock:/var/run/docker.sock \
-e DOCKER_USERNAME -e DOCKER_PASSWORD -e DOCKER_REGISTRY -e GITLAB_TOKEN \
goreleaser/goreleaser release --rm-dist
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
dockers:
- goos: linux
goarch: amd64
image_templates:
- 'registry.gitlab.com/rakshazi/mitufe:{{ .Tag }}'
- 'registry.gitlab.com/rakshazi/mitufe:latest'
dockerfile: Dockerfile
build_flag_templates:
- --label=org.opencontainers.image.title={{ .ProjectName }}
- --label=org.opencontainers.image.description={{ .ProjectName }}
- --label=org.opencontainers.image.url=https://gitlab.com/rakshazi/mitufe
- --label=org.opencontainers.image.source=https://gitlab.com/rakshazi/mitufe
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=MIT
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
nfpms:
- maintainer: Rakshazi
description: CLI/TUI rss reader, client for https://miniflux.app/, offline-first, highly customizable
homepage: https://gitlab.com/rakshazi/mitufe
license: MIT
formats:
- deb
- rpm
- apk
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
FROM golang:alpine AS builder
ENV CGO_ENABLED 0
ENV GOOS linux
RUN apk --no-cache add git ca-certificates tzdata && update-ca-certificates && \
adduser -D -g '' mitufe && \
mkdir -p /home/mitufe/.config/mitufe
WORKDIR /mitufe
COPY . .
RUN go build -ldflags="-w -s" -a -installsuffix cgo -o mitufe
FROM scratch
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /mitufe/mitufe /bin/mitufe
COPY --from=builder /home /home
USER mitufe
ENTRYPOINT ["/bin/mitufe"]
Copyright (c) 2021 Rakshazi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# mitufe
[![GoDoc](https://img.shields.io/badge/godoc-reference-informational.svg?style=for-the-badge)](https://pkg.go.dev/gitlab.com/rakshazi/mitufe)[![Download package](https://img.shields.io/badge/download-package-success.svg?style=for-the-badge)](https://gitlab.com/rakshazi/mitufe/-/releases)
[![Download docker](https://img.shields.io/badge/download-docker-blue.svg?style=for-the-badge)](https://gitlab.com/rakshazi/mitufe/container_registry)
[![coverage report](https://gitlab.com/rakshazi/mitufe/badges/master/coverage.svg)](https://gitlab.com/rakshazi/mitufe/-/commits/master)
[![Go Report Card](https://goreportcard.com/badge/gitlab.com/rakshazi/mitufe)](https://goreportcard.com/report/gitlab.com/rakshazi/mitufe)
[![Liberapay](https://img.shields.io/badge/donate-liberapay-yellow.svg?style=for-the-badge)](https://liberapay.com/rakshazi)
CLI/TUI rss reader, client for [Miniflux](https://miniflux.app/), offline-first, highly customizable
## Documentation
### Install
#### GNU/Linux distributives' packages
Available on [releases](https://gitlab.com/rakshazi/mitufe/-/releases) page
#### Docker
```bash
docker run -it --rm registry.gitlab.com/rakshazi/mitufe -h
```
#### Go
```bash
go install gitlab.com/rakshazi/mitufe@latest
mitufe -h
```
> **NOTE**: configuration and local db saved into `/home/mitufe/.config/mitufe` directory,
> don't forget to use it as volume
### Usage
```bash
Usage of mitufe:
--sync
Only sync, do not show TUI, just update db and exit
--date-format string
Date format, allowed values can be found here: https://golang.org/pkg/time/#pkg-constants (default "02 Jan 06 15:04")
--sort-direction string
Sort direction, allowed values: asc (newest first), desc (oldest first) (default "asc")
--sort-order string
Field name used for sorting, check miniflux docs for list of available fields (default "published_at")
-p string
Miniflux user password, only if you want to use login/password auth, used only once to generate config
-s string
Miniflux server URL, example: https://miniflux.app, used only once to generate config
-t string
Miniflux user API token, only if you want to use token auth, used only once to generate config
-u string
Miniflux user login, only if you want to use login/password auth, used only once to generate config
```
#### keyboard hotkeys
this app uses vim-like controls
**main view**:
* `j`, `down arrow` - Move down.
* `k`, `up arrow` - Move up.
* `g`, `home` - Move to the top.
* `G`, `end` - Move to the bottom.
* `T` - Toggle entries, show unread or read entries
* `Ctrl+j` - Next feed
* `Ctrl+k` - Previous feed
* `Ctrl+g` - First feed
* `q`, `ESC`, `Ctrl+C` - Close app
**article view**:
* `l` - Next article.
* `j`, `down arrow` - Move down.
* `k`, `up arrow` - Move up.
* `g`, `home` - Move to the top.
* `G`, `end` - Move to the bottom.
* `Ctrl-F`, `page down` - Move down by one page.
* `Ctrl-B`, `page up` - Move up by one page.
* `q`, `ESC` - Close article view.
Check the documentation - [Configuration](./docs/config.md)
package api
import (
"gitlab.com/rakshazi/mitufe/config"
miniflux "miniflux.app/client"
)
// Client - API client interface
type Client interface {
Me() (*miniflux.User, error)
Feeds() (miniflux.Feeds, error)
Entries(*miniflux.Filter) (*miniflux.EntryResultSet, error)
UpdateEntries([]int64, string) error
}
// New miniflux client
func New(cfg *config.Config) Client {
if len(cfg.Token) > 0 {
return miniflux.New(cfg.Server, cfg.Token)
}
return miniflux.New(cfg.Server, cfg.Login, cfg.Password)
}
package api
import (
"testing"
"gitlab.com/rakshazi/mitufe/config"
"miniflux.app/client"
)
func TestNew(t *testing.T) {
tests := []struct {
Name string
Config *config.Config
}{
{"token", &config.Config{Server: "http://not.exist", Token: "test"}},
{"login-password", &config.Config{Server: "http://not.exist", Login: "test", Password: "test"}},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
api := New(test.Config)
if _, ok := api.(*client.Client); !ok {
t.Fail()
}
})
}
}
package config
import (
"encoding/json"
"io/ioutil"
"os"
)
// ConfigFile - config file name
const ConfigFile string = "/config.json"
// New Config instance
func New(app, server, login, password, token, sortOrder, sortDirection, dateFormat string) (*Config, error) {
path, err := GetPath(app)
if err != nil {
return nil, err
}
if _, err := os.Stat(path); os.IsNotExist(err) {
err := os.Mkdir(path, 0700)
if err != nil {
return nil, err
}
}
path += ConfigFile
// If new config vars - save them
if server != "" {
config := &Config{
Server: server,
Login: login,
Password: password,
Token: token,
Sort: Sort{
Order: sortOrder,
Direction: sortDirection,
},
Colors: defaultColors,
Hotkeys: defaultHotkeys,
Elements: defaultElements,
DateFormat: dateFormat,
}
err := Write(path, config)
return config, err
}
// If no new vars - read existing config
return Read(path)
}
// Read config file
func Read(path string) (*Config, error) {
var config *Config = &Config{Sort: defaultSort, Colors: defaultColors, Hotkeys: defaultHotkeys, Elements: defaultElements}
if _, err := os.Stat(path); os.IsNotExist(err) {
return config, nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return config, err
}
json.Unmarshal(data, &config)
return config, nil
}
// Write config to file
func Write(path string, config *Config) error {
jsonString, err := json.Marshal(config)
if err != nil {
return err
}
return ioutil.WriteFile(path, jsonString, 0660)
}
// GetPath to config dir
func GetPath(app string) (string, error) {
path, err := os.UserConfigDir()
if err != nil {
return path, err
}
path += "/" + app
return path, nil
}
package config
import (
"encoding/json"
"io/ioutil"
"os"
"reflect"
"runtime"
"testing"
"time"
"gitlab.com/rakshazi/mitufe/logger"
)
func TestMain(m *testing.M) {
cfgPath, _ := GetPath("test")
logPath, _ := logger.GetPath("test")
for _, path := range []string{cfgPath, logPath} {
if _, err := os.Stat(path); os.IsNotExist(err) {
os.Mkdir(path, 0700)
}
}
defer os.Remove(cfgPath + "/local.db")
defer os.Remove(logPath + time.Now().Format("/2006-01-02.log"))
os.Exit(m.Run())
}
func TestNew(t *testing.T) {
os.Setenv("XDG_CONFIG_HOME", "/tmp")
defer os.RemoveAll("/tmp/mitufe")
defer os.Setenv("XDG_CONFIG_HOME", os.Getenv("HOME")+"/.config")
config, err := New("test", "http://not.exist", "login", "password", "", "published_at", "asc", "02 Jan 06 15:04")
if err != nil {
t.Error(err)
}
if config.Server != "http://not.exist" {
t.Fail()
}
config2, err := New("test", "", "", "", "", "published_at", "asc", "02 Jan 06 15:04")
if err != nil {
t.Error(err)
}
if config2.Server != "http://not.exist" {
t.Fail()
}
}
func TestGetPath(t *testing.T) {
// Partial copy of os.UserConfigDir() implementation
var dir string
switch runtime.GOOS {
case "windows":
dir = os.Getenv("AppData")
case "darwin", "ios":
dir = os.Getenv("HOME")
dir += "/Library/Application Support"
case "plan9":
dir = os.Getenv("home")
dir += "/lib"
default: // Unix
dir = os.Getenv("XDG_CONFIG_HOME")
if dir == "" {
dir = os.Getenv("HOME")
dir += "/.config"
}
}
// Actual test
expected := dir + "/test"
path, err := GetPath("test")
if err != nil {
t.Error(err)
}
if path != expected {
t.Errorf("Expected: %s, but got: %s", expected, path)
}
}
func TestWrite(t *testing.T) {
file, err := ioutil.TempFile("", "config.json")
if err != nil {
t.Error(err)
}
defer os.Remove(file.Name())
defer file.Close()
config := &Config{
Server: "http://not.exist",
Token: "test",
}
configFromFile := &Config{}
err = Write(file.Name(), config)
if err != nil {
t.Error(err)
}
data, err := ioutil.ReadAll(file)
if err != nil {
t.Error(err)
}
if err = json.Unmarshal(data, &configFromFile); err != nil {
t.Error(err)
}
if !reflect.DeepEqual(config, configFromFile) {
t.Fail()
}
}
func TestRead(t *testing.T) {
file, err := ioutil.TempFile("", "config.json")
if err != nil {
t.Error(err)
}
defer os.Remove(file.Name())
defer file.Close()
config := &Config{
Server: "http://not.exist",
Token: "test",
Colors: defaultColors,
Elements: defaultElements,
Hotkeys: defaultHotkeys,
Sort: defaultSort,
}
jsonString, err := json.Marshal(config)
if err != nil {
t.Error(err)
}
if err := ioutil.WriteFile(file.Name(), jsonString, 0660); err != nil {
t.Error(err)
}
configFromFile, err := Read(file.Name())
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(config, configFromFile) {
t.Fail()
}
}
package config
// default hotkeys
var defaultHotkeys Hotkeys = Hotkeys{
Up: "k",
Down: "j",
Top: "g",
Bottom: "G",
Toggle: "T",
Close: "q",
NextArticle: "l",
OpenInBrowser: "o",
}
// default elements config
var defaultElements Elements = Elements{
Sidebar: true,
Statusbar: true,
Hotkeybar: true,
}
// default sort options
var defaultSort Sort = Sort{
Order: "published_at",
Direction: "asc",
}
// default colors
var defaultColors Colors = Colors{
SidebarBackground: "black",
SidebarText: "white",
MainBackground: "black",
MainText: "white",
ArticleBackground: "black",
ArticleText: "white",
StatusbarBackground: "black",
StatusbarText: "white",
HotkeybarBackground: "darkgray",
HotkeybarText: "black",
}
package config
// Config
type Config struct {
Server string `json:"server,omitempty"`
Login string `json:"login,omitempty"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
Sort Sort `json:"sort,omitempty"`
Hotkeys Hotkeys `json:"hotkeys,omitempty"`
Colors Colors `json:"colors,omitempty"`
Elements Elements `json:"elements,omitempty"`
DateFormat string `json:"date_format,omitempty"`
}
// Hotkeys config
type Hotkeys struct {
Up string `json:'up,omitempty"`
Down string `json:"down,omitempty"`
Top string `json:"top,omitempty"`
Bottom string `json:"bottom,omitempty"`
Toggle string `json:"toggle,omitempty"`
Close string `json:"close,omitempty"`
NextArticle string `json:"next_article,omitempty"`
OpenInBrowser string `json:"open_in_browser,omitempty"`
}
// Elements - list of enabled/disabled TUI elements
type Elements struct {
Sidebar bool `json:"sidebar,omitempty"`
Statusbar bool `json:"statusbar,omitempty"`
Hotkeybar bool `json:"hotkeybar,omitempty"`
}
// Sort config
type Sort struct {
Order string `json:"order,omitempty"`
Direction string `json:"direction,omitempty"`
}
// Colors config
type Colors struct {
SidebarBackground string `json:"sidebar_background,omitempty"`
SidebarText string `json:"sidebar_text,omitempty"`
MainBackground string `json:"main_background,omitempty"`
MainText string `json:"main_text,omitempty"`
ArticleBackground string `json:"article_background,omitempty"`
ArticleText string `json:"article_text,omitempty"`
StatusbarBackground string `json:"statusbar_background,omitempty"`
StatusbarText string `json:"statusbar_text,omitempty"`
HotkeybarBackground string `json:"hotkeybar_background,omitempty"`
HotkeybarText string `json:"hotkeybar_text,omitempty"`
}
# Configuration
## Path
Config saved to user's config dir, for Linux it is `~/.config/mitufe/`
## Structure
```json
{
"server": "https://miniflux-server.site",
"login": "user_login",
"password": "user_password",
"token": "API token",
"sort": {
"order": "field_name",
"direction": "desc"
},
"colors": {
"sidebar_background": "black",
"sidebar_text": "white",
"main_background": "black",
"main_text": "white",
"article_background": "black",
"article_text": "white",
"statusbar_background": "black",
"statusbar_text": "white",
"hotkeybar_background": "black",
"hotkeybar_text": "white"
},
"hotkeys": {
"up": "k",
"down": "j",
"top": "g",
"bottom": "G",
"toggle": "T",
"close": "q",
"next_article": "l",
"open_in_browser": "o"
},
"elements": {
"sidebar": true,
"statusbar": true,
"hotkeybar": true
},
"date_format": "golang date format"
}
```
## Fields
### server
URL of miniflux server, just host with http/https scheme
### token
Miniflux API token, **preferred** auth way.
> **NOTE**: if you set token, you don't need to provide login and password
### login
Miniflux user login