add config, clean, and reset flags

pull/25/head
Miguel Mota 5 years ago
parent 9b0670f11b
commit 22d551b3b4

@ -0,0 +1,25 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.2] - 2018-12-30
### Fixed
- Paginate CoinMarketCap V1 API responses due to their backward-incompatible update
### Added
- `-clean` flag to clean cache
- `-reset` flag to clean cache and delete config
- `-config` flag to use a different specified config file
## [1.1.1] - 2018-12-26
### Changed
- Use go modules instead of dep
## [1.1.0] - 2018-12-25
### Added
- Basic portfolio functionality
- `P` keyboard shortcut to toggle portfolio view
- `e` keyboard shortcut to edit portfolio holdings
- `[portfolio]` TOML config holdings list

@ -39,6 +39,15 @@ test:
cointop/test:
go run main.go -test
cointop/version:
go run main.go -version
cointop/clean:
go run main.go -clean
cointop/reset:
go run main.go -reset
snap/clean:
snapcraft clean
rm -f cointop_*.snap

@ -74,6 +74,17 @@ Now you can run cointop:
cointop
```
### Binary (all platforms)
You can download the binary from the [releases](https://github.com/miguelmota/cointop/releases) page
```bash
# replace x.x.x with the latest version
wget https://github.com/miguelmota/cointop/releases/download/x.x.x/cointop_x.x.x_linux_amd64.tar.gz
tar -xvzf cointop_x.x.x_linux_amd64.tar.gz cointop
./cointop
```
### Homebrew (macOS)
cointop is available via [Homebrew](https://formulae.brew.sh/formula/cointop) for macOS:
@ -302,7 +313,7 @@ Key|Action
<kbd>m</kbd>|Sort table by *[m]arket cap*
<kbd>M</kbd> (Shift+m)|Go to middle of visible table window (vim inspired)
<kbd>n</kbd>|Sort table by *[n]ame*
<kbd>o</kbd>|[o]pen link to highlighted coin on [CoinMarketCap](https://coinmarketcap.com/)
<kbd>o</kbd>|[o]pen link to highlighted coin (visits the API's coin page)
<kbd>p</kbd>|Sort table by *[p]rice*
<kbd>P</kbd> (Shift+p)|Toggle show portfolio
<kbd>r</kbd>|Sort table by *[r]ank*
@ -468,16 +479,24 @@ Frequently asked questions:
- Q: Where is the data from?
- A: The data is from [Coin Market Cap](https://coinmarketcap.com/).
- A: Currently, the data is from [CoinMarketCap](https://coinmarketcap.com/).
- Q: What coins does this support?
- A: This supports any coin listed on [Coin Market Cap](https://coinmarketcap.com/).
- A: This supports any coin supported by the API being used to fetch coin information.
- Q: Will you be supporting more coin API's in the future?
- A: Yes supporting more coin API's is planned.
- Q: How often is the data polled?
- A: Data gets polled once every minute by default. You can press <kbd>Ctrl</kbd>+<kbd>r</kbd> to force refresh.
- Q: I ran cointop for the first time and don't see any data?
- A: Running cointop for the first time will fetch the data and populate the cache which may take a few seconds.
- Q: I installed cointop without errors but the command is not found.
- A: Make sure your `GOPATH` and `PATH` is set correctly.
@ -619,7 +638,7 @@ Frequently asked questions:
- Q: The data isn't refreshing!
- A: The CoinMarketCap API has rate limits, so make sure to keep manual refreshes to a minimum. If you've hit the rate limit then wait about half an hour to be able to fetch the data again. Keep in mind that CoinMarketCap updates prices every 5 minutes so constant refreshes aren't necessary.
- A: The coin APIs have rate limits, so make sure to keep manual refreshes to a minimum. If you've hit the rate limit then wait about half an hour to be able to fetch the data again. Keep in mind that some coin APIs, such as CoinMarketCap, update prices every 5 minutes so constant refreshes aren't necessary.
- Q: How do I quit the application?
@ -641,6 +660,10 @@ Frequently asked questions:
- A: In `~/.cointop/config`, set `defaultView = "default"`
- Q: How can use a different config file other than the default?
- A: Run `cointop -config="/path/to/config/file"` to use the specified file as the config.
- Q: I'm getting the error `open /dev/tty: no such device or address`.
-A: Usually this error occurs when cointop is running as a daemon or slave which means that there is no terminal allocated, so `/dev/tty` doesn't exist for that process. Try running it with the following environment variables:
@ -649,9 +672,21 @@ Frequently asked questions:
DEV_IN=/dev/stdout DEV_OUT=/dev/stdout cointop
```
- Q: I can only view the first page, why isn't the pagination is working?
- A: Sometimes the coin APIs will make updates and break things. If you see this problem please [submit an issue](https://github.com/miguelmota/cointop/issues/new).
- Q: How can I delete the cache?
- A: Run `cointop -clean` to delete the cache files. Cointop will generate new cache files after fetching data.
- Q: How can I reset cointop?
- A: Run `cointop -reset` to delete the config files and cache. Cointop will generate a new config when starting up.
- Q: What is the size of the binary?
- A: The executable is only ~2MB in size.
- A: The executable binary is ~6MB in size. Packed with [UPX](https://upx.github.io/) it's ~2.5MB
## Development

@ -0,0 +1,38 @@
package cmd
import (
"flag"
"fmt"
"github.com/miguelmota/cointop/cointop"
)
// Run ...
func Run() {
var v, ver, test, clean, reset bool
var config string
flag.BoolVar(&v, "v", false, "Version")
flag.BoolVar(&ver, "version", false, "Version")
flag.BoolVar(&test, "test", false, "Run test")
flag.BoolVar(&clean, "clean", false, "Clean cache")
flag.BoolVar(&reset, "reset", false, "Reset config")
flag.StringVar(&config, "config", "", "Config filepath")
flag.Parse()
if v || ver {
fmt.Printf("cointop v%s", cointop.Version())
} else if test {
doTest()
} else if clean {
cointop.Clean()
} else if reset {
cointop.Reset()
} else {
cointop.NewCointop(&cointop.Config{
ConfigFilepath: config,
}).Run()
}
}
func doTest() {
cointop.NewCointop(nil).Exit()
}

@ -2,6 +2,7 @@ package cointop
import (
"fmt"
"io/ioutil"
"log"
"os"
"strings"
@ -56,6 +57,7 @@ type Cointop struct {
actionsmap map[string]bool
shortcutkeys map[string]string
config config // toml config
configFilepath string
searchfield *gocui.View
searchfieldviewname string
searchfieldvisible bool
@ -93,12 +95,25 @@ type portfolio struct {
Entries map[string]*portfolioEntry
}
// New initializes cointop
func New() *Cointop {
// Config config options
type Config struct {
ConfigFilepath string
}
// NewCointop initializes cointop
func NewCointop(config *Config) *Cointop {
var debug bool
if os.Getenv("DEBUG") != "" {
debug = true
}
configFilepath := "~/.cointop/config"
if config != nil {
if config.ConfigFilepath != "" {
configFilepath = config.ConfigFilepath
}
}
ct := &Cointop{
api: api.NewCMC(),
refreshticker: time.NewTicker(1 * time.Minute),
@ -114,6 +129,7 @@ func New() *Cointop {
favorites: map[string]bool{},
cache: cache.New(1*time.Minute, 2*time.Minute),
debug: debug,
configFilepath: configFilepath,
marketbarviewname: "market",
chartviewname: "chart",
chartranges: []string{
@ -231,3 +247,43 @@ func (ct *Cointop) Run() {
log.Fatalf("main loop: %v", err)
}
}
// Clean ...
func Clean() {
tmpPath := "/tmp"
if _, err := os.Stat(tmpPath); !os.IsNotExist(err) {
files, err := ioutil.ReadDir(tmpPath)
if err != nil {
log.Fatal(err)
}
for _, f := range files {
if strings.HasPrefix(f.Name(), "fcache.") {
file := fmt.Sprintf("%s/%s", tmpPath, f.Name())
fmt.Printf("removing %s\n", file)
if err := os.Remove(file); err != nil {
log.Fatal(err)
}
}
}
}
fmt.Println("cointop cache has been cleaned")
}
// Reset ...
func Reset() {
Clean()
homedir := userHomeDir()
// default config path
configPath := fmt.Sprintf("%s%s", homedir, "/.cointop")
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
fmt.Printf("removing %s\n", configPath)
if err := os.RemoveAll(configPath); err != nil {
log.Fatal(err)
}
}
fmt.Println("cointop has been reset")
}

@ -2,7 +2,6 @@ package cointop
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strings"
@ -21,11 +20,7 @@ type config struct {
}
func (ct *Cointop) setupConfig() error {
err := ct.makeConfigDir()
if err != nil {
return err
}
err = ct.makeConfigFile()
err := ct.createConfigIfNotExists()
if err != nil {
return err
}
@ -56,13 +51,26 @@ func (ct *Cointop) setupConfig() error {
return nil
}
func (ct *Cointop) createConfigIfNotExists() error {
err := ct.makeConfigDir()
if err != nil {
return err
}
err = ct.makeConfigFile()
if err != nil {
return err
}
return nil
}
func (ct *Cointop) configDirPath() string {
homedir := userHomeDir()
return fmt.Sprintf("%s%s", homedir, "/.cointop")
path := normalizePath(ct.configFilepath)
parts := strings.Split(path, "/")
return strings.Join(parts[0:len(parts)-1], "/")
}
func (ct *Cointop) configPath() string {
return fmt.Sprintf("%v%v", ct.configDirPath(), "/config")
return normalizePath(ct.configFilepath)
}
func (ct *Cointop) makeConfigDir() error {

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/gob"
"os"
"path/filepath"
"runtime"
"strings"
@ -41,6 +42,15 @@ func userHomeDir() string {
return os.Getenv("HOME")
}
func normalizePath(path string) string {
// expand tilde
if strings.HasPrefix(path, "~/") {
path = filepath.Join(userHomeDir(), path[2:])
}
return path
}
func (ct *Cointop) slugify(s string) string {
s = strings.ToLower(s)
return s

@ -1,7 +1,7 @@
package cointop
// TODO: make dynamic based on git tag
const version = "1.1.1"
const version = "1.1.2"
func (ct *Cointop) version() string {
return version

@ -1,27 +1,9 @@
package main
import (
"flag"
"fmt"
"github.com/miguelmota/cointop/cointop"
"github.com/miguelmota/cointop/cmd"
)
func main() {
var v, ver, test bool
flag.BoolVar(&v, "v", false, "Version")
flag.BoolVar(&ver, "version", false, "Version")
flag.BoolVar(&test, "test", false, "Run test")
flag.Parse()
if v || ver {
fmt.Printf("cointop v%s", cointop.Version())
} else if test {
doTest()
} else {
cointop.New().Run()
}
}
func doTest() {
cointop.New().Exit()
cmd.Run()
}

@ -13,3 +13,8 @@ func NewCMC() Interface {
func NewCC() {
// TODO
}
// NewCG new CoinGecko API
func NewCG() {
// TODO
}

@ -0,0 +1,3 @@
package coingecko
// TODO

@ -6,6 +6,7 @@ import (
"strconv"
"strings"
"sync"
"time"
apitypes "github.com/miguelmota/cointop/pkg/api/types"
cmc "github.com/miguelmota/cointop/pkg/cmc"
@ -34,7 +35,7 @@ func (s *Service) Ping() error {
return nil
}
func getLimitedCoinData(convert string, offset int) (map[string]apitypes.Coin, error) {
func getLimitedCoinDataV2(convert string, offset int) (map[string]apitypes.Coin, error) {
ret := make(map[string]apitypes.Coin)
max := 100
coins, err := cmc.Tickers(&cmc.TickersOptions{
@ -58,27 +59,54 @@ func getLimitedCoinData(convert string, offset int) (map[string]apitypes.Coin, e
PercentChange1H: v.Quotes[convert].PercentChange1H,
PercentChange24H: v.Quotes[convert].PercentChange24H,
PercentChange7D: v.Quotes[convert].PercentChange7D,
Volume24H: v.Quotes[convert].Volume24H,
Volume24H: formatVolume(v.Quotes[convert].Volume24H),
LastUpdated: strconv.Itoa(v.LastUpdated),
}
}
return ret, nil
}
// GetAllCoinData get all coin data
func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, error) {
func getLimitedCoinData(convert string, offset int) (map[string]apitypes.Coin, error) {
ret := make(map[string]apitypes.Coin)
coins, err := cmc.V1Tickers(0, convert)
max := 100
coins, err := cmc.Tickers(&cmc.TickersOptions{
Convert: convert,
Start: max * offset,
Limit: max,
})
if err != nil {
return ret, err
}
for _, v := range coins {
price := v.Quotes[convert].Price
pricestr := fmt.Sprintf("%.2f", price)
if convert == "ETH" || convert == "BTC" || price < 1 {
pricestr = fmt.Sprintf("%.5f", price)
price := formatPrice(v.Quotes[convert].Price, convert)
ret[v.Name] = apitypes.Coin{
ID: strings.ToLower(v.Name),
Name: v.Name,
Symbol: v.Symbol,
Rank: v.Rank,
AvailableSupply: v.CirculatingSupply,
TotalSupply: v.TotalSupply,
MarketCap: v.Quotes[convert].MarketCap,
Price: price,
PercentChange1H: v.Quotes[convert].PercentChange1H,
PercentChange24H: v.Quotes[convert].PercentChange24H,
PercentChange7D: v.Quotes[convert].PercentChange7D,
Volume24H: formatVolume(v.Quotes[convert].Volume24H),
LastUpdated: strconv.Itoa(v.LastUpdated),
}
price, _ = strconv.ParseFloat(pricestr, 64)
}
return ret, nil
}
// GetAllCoinData1 get all coin data
func (s *Service) GetAllCoinData1(convert string) (map[string]apitypes.Coin, error) {
ret := make(map[string]apitypes.Coin)
coins, err := cmc.V1Tickers(0, convert)
if err != nil {
return ret, err
}
for _, v := range coins {
price := formatPrice(v.Quotes[convert].Price, convert)
ret[v.Name] = apitypes.Coin{
ID: strings.ToLower(v.Name),
Name: v.Name,
@ -91,7 +119,7 @@ func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, erro
PercentChange1H: v.PercentChange1H,
PercentChange24H: v.PercentChange24H,
PercentChange7D: v.PercentChange7D,
Volume24H: v.Quotes[convert].Volume24H,
Volume24H: formatVolume(v.Quotes[convert].Volume24H),
LastUpdated: strconv.Itoa(v.LastUpdated),
}
}
@ -105,6 +133,31 @@ func (s *Service) GetAllCoinDataV2(convert string) (map[string]apitypes.Coin, er
ret := make(map[string]apitypes.Coin)
var mutex sync.Mutex
for i := 0; i < 5; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
coins, err := getLimitedCoinDataV2(convert, j)
if err != nil {
return
}
mutex.Lock()
for k, v := range coins {
ret[k] = v
}
mutex.Unlock()
}(i)
}
wg.Wait()
return ret, nil
}
// GetAllCoinData gets all coin data. Need to paginate through all pages
func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, error) {
var wg sync.WaitGroup
ret := make(map[string]apitypes.Coin)
var mutex sync.Mutex
for i := 0; i < 17; i++ {
time.Sleep(500 * time.Millisecond)
wg.Add(1)
go func(j int) {
defer wg.Done()
@ -177,3 +230,16 @@ func (s *Service) GetGlobalMarketData(convert string) (apitypes.GlobalMarketData
}
return ret, nil
}
func formatPrice(price float64, convert string) float64 {
pricestr := fmt.Sprintf("%.2f", price)
if convert == "ETH" || convert == "BTC" || price < 1 {
pricestr = fmt.Sprintf("%.5f", price)
}
price, _ = strconv.ParseFloat(pricestr, 64)
return price
}
func formatVolume(volume float64) float64 {
return float64(int64(volume))
}

Loading…
Cancel
Save