Compare commits

..

No commits in common. 'master' and 'v1.6.5' have entirely different histories.

@ -1,40 +0,0 @@
version: 2.1
orbs:
base: dmx-io/base@2.0.88
jobs:
build_and_push:
working_directory: /app
docker:
- image: docker:17.09.0-ce-git
steps:
- checkout
- setup_remote_docker
- run:
name: Install dependencies
command: |
apk update
apk upgrade
apk add --no-cache make
- run:
name: Build application Docker image
command: |
make docker-build
- deploy:
name: Push Docker image to Docker Hub
command: |
make docker-login-ci
make docker-tag-ci
make docker-push-ci
workflows:
main:
jobs:
- build_and_push:
filters:
branches:
only:
- master
- develop
ignore: /.*/

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.github.miguelmota.Cointop.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>Apache-2.0</project_license>
<name>Cointop</name>
<summary>Terminal based application for tracking cryptocurrencies</summary>
<description>
<p>
Cointop is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time.
</p>
</description>
<screenshots>
<screenshot type="default">
<image>https://user-images.githubusercontent.com/168240/39569578-7ce9f3b6-4e7a-11e8-82a9-8a18b91b1bd5.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://cointop.sh/</url>
<releases>
<release version="1.4.5" date="2020-02-18"/>
</releases>
<content_rating type="oars-1.0">
<content_attribute id="violence-cartoon">none</content_attribute>
<content_attribute id="violence-fantasy">none</content_attribute>
<content_attribute id="violence-realistic">none</content_attribute>
<content_attribute id="violence-bloodshed">none</content_attribute>
<content_attribute id="violence-sexual">none</content_attribute>
<content_attribute id="drugs-alcohol">none</content_attribute>
<content_attribute id="drugs-narcotics">none</content_attribute>
<content_attribute id="drugs-tobacco">none</content_attribute>
<content_attribute id="sex-nudity">none</content_attribute>
<content_attribute id="sex-themes">none</content_attribute>
<content_attribute id="language-profanity">none</content_attribute>
<content_attribute id="language-humor">none</content_attribute>
<content_attribute id="language-discrimination">none</content_attribute>
<content_attribute id="social-chat">none</content_attribute>
<content_attribute id="social-info">none</content_attribute>
<content_attribute id="social-audio">none</content_attribute>
<content_attribute id="social-location">none</content_attribute>
<content_attribute id="social-contacts">none</content_attribute>
<content_attribute id="money-purchasing">none</content_attribute>
<content_attribute id="money-gambling">none</content_attribute>
</content_rating>
<categories>
<category>Utility</category>
</categories>
</component>

@ -0,0 +1,6 @@
[Desktop Entry]
Name=Cointop
Exec=cointop
Type=Application
Icon=com.github.miguelmota.Cointop
Terminal=true

@ -0,0 +1,69 @@
{
"app-id": "com.github.miguelmota.Cointop",
"runtime": "org.freedesktop.Platform",
"runtime-version": "19.08",
"sdk": "org.freedesktop.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.golang"
],
"command": "cointop",
"finish-args": [
"--share=network"
],
"cleanup": [
"/go",
"/bin/scripts"
],
"modules": [
{
"name": "scripts",
"sources": [
{
"type": "script",
"commands": [
". /usr/lib/sdk/golang/enable.sh; export GOPATH=/app/go"
],
"dest-filename": "my_enable.sh"
},
{
"type": "script",
"commands": [
". /app/bin/scripts/my_enable.sh; cd /app/go/src/$1; GO111MODULE=off go build -o x"
],
"dest-filename": "build.sh"
}
],
"buildsystem": "simple",
"build-commands": [
"mkdir -p /app/go/{src,pkg,bin}",
"install -d /app/bin/scripts",
"cp *.sh /app/bin/scripts/"
]
},
{
"name": "cointop",
"buildsystem": "simple",
"build-commands": [
"cp -rpv go/* /app/go/",
"/app/bin/scripts/build.sh github.com/miguelmota/cointop",
"install -D /app/go/src/github.com/miguelmota/cointop/x /app/bin/cointop",
"mkdir -p /app/share/icons/hicolor/64x64/apps",
"mkdir -p /app/share/icons/hicolor/128x128/apps",
"mkdir -p /app/share/applications",
"mkdir -p /app/share/metainfo",
"cp /app/go/src/github.com/miguelmota/cointop/assets/icon_64x64.png /app/share/icons/hicolor/64x64/apps/com.github.miguelmota.Cointop.png",
"cp /app/go/src/github.com/miguelmota/cointop/assets/icon_128x128.png /app/share/icons/hicolor/128x128/apps/com.github.miguelmota.Cointop.png",
"cp /app/go/src/github.com/miguelmota/cointop/.flathub/com.github.miguelmota.Cointop.appdata.xml /app/share/metainfo/com.github.miguelmota.Cointop.appdata.xml",
"cp /app/go/src/github.com/miguelmota/cointop/.flathub/com.github.miguelmota.Cointop.desktop /app/share/applications/com.github.miguelmota.Cointop.desktop"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/miguelmota/cointop/archive/1.4.3.tar.gz",
"sha256": "842b11703e4e8c825c7d0e3d167c018f44b555d9bde467a1f57be4aba7ba8606",
"dest": "go/src/github.com/miguelmota/cointop"
}
]
}
]
}

@ -1,28 +0,0 @@
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

6
.gitignore vendored

@ -47,6 +47,9 @@ wasm
.config
.bz2
# flatpak
.flatpak-builder
build-dir
#repo
# do not ignore .flathub
# do not ignore .rpm
@ -57,11 +60,10 @@ wasm
.appimage_workspace
todo.txt
.vscode
docs/public
deploy_docs.sh
package-lock.json
# Local Netlify folder
.netlify
.netlify

@ -6,4 +6,4 @@ builds:
goarch:
- amd64
ldflags:
- -X github.com/cointop-sh/cointop/cointop.version={{.Env.VERSION}}
- -X github.com/miguelmota/cointop/cointop.version={{.Env.VERSION}}

@ -8,7 +8,7 @@ Release: 6%{?dist}
Summary: Interactive terminal based UI application for tracking cryptocurrencies
License: Apache-2.0
URL: https://cointop.sh
Source0: https://github.com/cointop-sh/%{cointop}/archive/v%{version}.tar.gz
Source0: https://github.com/miguelmota/%{cointop}/archive/v%{version}.tar.gz
BuildRequires: gcc
BuildRequires: golang >= 1.14
@ -20,11 +20,11 @@ cointop is a fast and lightweight interactive terminal based UI application for
%setup -q -n %{name}-%{version}
%build
mkdir -p ./_build/src/github.com/cointop-sh
ln -s $(pwd) ./_build/src/github.com/cointop-sh/%{name}
mkdir -p ./_build/src/github.com/miguelmota
ln -s $(pwd) ./_build/src/github.com/miguelmota/%{name}
export GOPATH=$(pwd)/_build:%{gopath}
GO111MODULE=off go build -ldflags="-linkmode=external -compressdwarf=false -X github.com/cointop-sh/cointop/cointop.version=%{version}" -o x .
GO111MODULE=off go build -ldflags="-linkmode=external -compressdwarf=false -X github.com/miguelmota/cointop/cointop.version=%{version}" -o x .
%install
install -d %{buildroot}%{_bindir}

@ -4,132 +4,55 @@ 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.6.10] - 2021-11-06
### Added
- Search by symbol or name
- Purchase price option for portfolio entries
- Mouse support for column sorting and menu options
### Changed
- `0` keybinding to go to first row of first page
### Fixed
- Coin sorting
- Editable shortcuts
- Duplicate portfolio entries
## [1.6.9] - 2021-10-12
### Added
- Chart x-axis date labels
- Configurable favorite character
- Configurable chart width
- Save chart height
### Changed
- Renamed organization `miguelmota``cointop-sh`
### Fixed
- Global chart currency
- Chart resampling and interpolation
- Chart time periods
- Use preferred cache directory
- Currency symbol width
## [1.6.8] - 2021-09-13
### Fixed
- Hide holdings amount when using command hide flag
## [1.6.7] - 2021-09-13
### Added
- Toggle hide portfolio balances keybinding
- Evaluate expression in portfolio value edit field
- Add 1Y% change column
## [1.6.6] - 2021-08-22
### Added
- Default chart range config
### Fixed
- Duplicate coin portfolio entries
- Increase decimals places shown for small values
- Filecache locking
## [1.6.5] - 2021-04-25
### Added
- Chart fullscreen toggle keybinding
- 24% change to holdings command
- Read environment variables for config
## [1.6.4] - 2021-04-25
### Added
- Preferred cache directory
- Read host numeric monetary locale
- Column filter for holdings command
- SSH server user config type
### Fixed
- Config file path
- String rune count
## [1.6.3] - 2021-03-10
### Added
- Max pages flag
- SSH server connection max timeout
### Fixed
- Negative holdings balance input
- Coins and portfolio row selection
- Table scroll
## [1.6.2] - 2021-02-12
### Added
- Config option to keep row focus on sort
## [1.6.1] - 2021-02-12
### Added
- Multiple coin support in price command
### Fixed
- Chart data interpolation
- CoinMarketCap graph data endpoint
## [1.6.0] - 2021-02-12
### Added
- Configurable table columns
- Basic price alerts
- Multiple coin support in price command
## [1.6.0] - 2021-02-12
### Fixed
- Coin chart lookup
- Dynamic column widths
## [1.5.5] - 2020-11-15
### Added
- Currency convesion option to holdings command
- Sort by percent holdings shortcut
- Configurable table columns
- Basic price alerts
## [1.5.5] - 2020-11-15
### Fixed
- Termux cache directory
- Open command on Windows
## [1.5.4] - 2020-08-24
### Added
- Colorschemes directory flag
- Currency convesion option to holdings command
- Sort by percent holdings shortcut
## [1.5.4] - 2020-08-24
### Fixed
- Rank order for low market cap coins
### Added
- Colorschemes directory flag
## [1.5.3] - 2020-08-14
### Fixed
- Build error
## [1.5.2] - 2020-08-13
### Fixed
- `XDG_CONFIG_HOME` config path
### Added
- Holdings command with sorting and filter options
- Bitcoin dominance command
### Fixed
- `XDG_CONFIG_HOME` config path
## [1.5.1] - 2020-08-05
### Fixed
- Version typo
@ -155,24 +78,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Increase number of page results from CoinGecko
## [1.4.5] - 2020-02-18
### Added
- VND currency conversion
### Fixed
- Convert to chosen currency for market data
### Added
- VND currency conversion
## [1.4.4] - 2019-12-31
### Fixed
- Flathub app release version
## [1.4.3] - 2019-12-29
### Added
- Tab keybinding
### Fixed
- Chart update bug fixes
- Marketbar currency bug fixes
### Added
- Tab keybinding
## [1.4.2] - 2019-12-29
### Fixed
- Fix keybinding issue on FreeBSD
@ -240,25 +163,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Release archive to contain latest source code
## [1.1.4] - 2019-04-21
### Added
- Config option to use CoinMarketCap Pro V1 API KEY
### Changed
- CoinMarketCap legacy V2 API to Pro V1 API
### Added
- Config option to use CoinMarketCap Pro V1 API KEY
## [1.1.3] - 2019-02-25
### Fixed
- Vendor dependencies
## [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
### Fixed
- Paginate CoinMarketCap V1 API responses due to their backward-incompatible update
## [1.1.1] - 2018-12-26
### Changed
- Use go modules instead of dep

@ -1,46 +1,18 @@
FROM golang:alpine AS build
ARG VERSION
RUN wget \
--output-document "/cointop-$VERSION.tar.gz" \
"https://github.com/cointop-sh/cointop/archive/refs/tags/$VERSION.tar.gz" \
&& wget \
--output-document "/cointop-colors-master.tar.gz" \
"https://github.com/cointop-sh/colors/archive/master.tar.gz" \
&& mkdir --parents \
"$GOPATH/src/github.com/cointop-sh/cointop" \
"/usr/local/share/cointop/colors" \
&& tar \
--directory "$GOPATH/src/github.com/cointop-sh/cointop" \
--extract \
--file "/cointop-$VERSION.tar.gz" \
--strip-components 1 \
&& tar \
--directory /usr/local/share/cointop/colors \
--extract \
--file /cointop-colors-master.tar.gz \
--strip-components 1 \
&& rm \
"/cointop-$VERSION.tar.gz" \
/cointop-colors-master.tar.gz \
&& cd "$GOPATH/src/github.com/cointop-sh/cointop" \
&& CGO_ENABLED=0 go install -ldflags "-s -w -X 'github.com/cointop-sh/cointop/cointop.version=$VERSION'" \
&& cd "$GOPATH" \
&& rm -r src/github.com \
&& apk add --no-cache upx \
&& upx --lzma /go/bin/cointop \
&& apk del upx
FROM golang:1.15 as build
FROM busybox
RUN mkdir /app
WORKDIR /app
ARG VERSION
ARG MAINTAINER
COPY --from=build /etc/ssl/certs /etc/ssl/certs
COPY --from=build /go/bin/cointop /usr/local/bin/cointop
COPY --from=build /usr/local/share /usr/local/share
ENV \
COINTOP_COLORS_DIR=/usr/local/share/cointop/colors \
XDG_CONFIG_HOME=/config
EXPOSE 2222
LABEL \
maintainer="$MAINTAINER" \
version="$VERSION"
ENTRYPOINT ["cointop"]
COPY . ./
RUN go build -ldflags=-s -ldflags=-w -ldflags=-X=github.com/miguelmota/cointop/cointop.version=$VERSION -o main .
ADD https://github.com/cointop-sh/colors/archive/master.tar.gz ./
RUN tar zxf master.tar.gz --exclude images
FROM busybox:glibc
RUN mkdir -p /etc/ssl
COPY --from=build /etc/ssl/certs/ /etc/ssl/certs
COPY --from=build /app/main /bin/cointop
COPY --from=build /app/colors-master /root/.config/cointop/colors
ENTRYPOINT ["/bin/cointop"]
CMD []

@ -1,8 +1,6 @@
VERSION = $$(git describe --abbrev=0 --tags)
COMMIT_TAG = $$(git tag --points-at HEAD)
VERSION_DATE = $$(git log -1 --pretty='%ad' --date=format:'%Y-%m-%d' $(VERSION))
COMMIT_REV = $$(git rev-list -n 1 $(VERSION))
MAINTAINER = "Miguel Mota"
all: build
@ -31,18 +29,18 @@ debug:
.PHONY: build
build:
go build -ldflags "-X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/cointop main.go
go build -ldflags "-X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop main.go
# http://macappstore.org/upx
build-mac: clean-mac
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/macos/cointop && upx bin/macos/cointop
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/macos/cointop && upx bin/macos/cointop
build-linux: clean-linux
env GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/linux/cointop && upx bin/linux/cointop
env GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/linux/cointop && upx bin/linux/cointop
build-multiple: clean
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/cointop64 && upx bin/cointop64 && \
env GOARCH=386 go build -ldflags "-s -w -X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/cointop32 && upx bin/cointop32
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop64 && upx bin/cointop64 && \
env GOARCH=386 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop32 && upx bin/cointop32
install: build
sudo mv bin/cointop /usr/local/bin
@ -97,7 +95,7 @@ snap-clean:
snap-stage:
# https://github.com/elopio/go/issues/2
mv go.mod go.mod~ ;GO111MODULE=off GOFLAGS="-ldflags=-s -ldflags=-w -ldflags=-X=github.com/cointop-sh/cointop/cointop.version=$(VERSION)" snapcraft stage; mv go.mod~ go.mod
mv go.mod go.mod~ ;GO111MODULE=off GOFLAGS="-ldflags=-s -ldflags=-w -ldflags=-X=github.com/miguelmota/cointop/cointop.version=$(VERSION)" snapcraft stage; mv go.mod~ go.mod
snap-install:
sudo apt install snapd
@ -177,7 +175,7 @@ rpm-dirs:
chmod -R a+rwx ~/rpmbuild
rpm-download:
wget https://github.com/cointop-sh/cointop/archive/$(VERSION).tar.gz -O ~/rpmbuild/SOURCES/$(VERSION).tar.gz
wget https://github.com/miguelmota/cointop/archive/$(VERSION).tar.gz -O ~/rpmbuild/SOURCES/$(VERSION).tar.gz
copr-install-cli:
sudo dnf install -y copr-cli
@ -211,7 +209,7 @@ brew-test:
brew test cointop.rb
brew-tap:
brew tap cointop/cointop https://github.com/cointop-sh/cointop
brew tap cointop/cointop https://github.com/miguelmota/cointop
brew-untap:
brew untap cointop/cointop
@ -229,43 +227,22 @@ release:
rm -rf dist
VERSION=$(VERSION) goreleaser
docker-login:
docker login
docker-login-ci:
docker login -u $(DOCKER_USER) -p $(DOCKER_PASS)
docker-build:
docker build --build-arg VERSION=$(VERSION) --build-arg MAINTAINER=$(MAINTAINER) -t cointop/cointop .
docker-tag:
docker tag cointop/cointop:latest cointop/cointop:$(VERSION)
docker-tag-ci:
docker tag cointop/cointop:latest cointop/cointop:$(CIRCLE_SHA1)
docker tag cointop/cointop:latest cointop/cointop:$(CIRCLE_BRANCH)
test $(COMMIT_TAG) && docker tag cointop/cointop:latest cointop/cointop:$(COMMIT_TAG); true
docker build --build-arg VERSION=$(VERSION) -t cointop/cointop .
docker-run:
docker run -it cointop/cointop
docker-push:
docker push cointop/cointop:$(VERSION)
docker push cointop/cointop:latest
docker-push-ci:
docker push cointop/cointop:$(CIRCLE_SHA1)
docker push cointop/cointop:$(CIRCLE_BRANCH)
test $(COMMIT_TAG) && docker push cointop/cointop:$(COMMIT_TAG); true
test $(CIRCLE_BRANCH) == "master" && docker push cointop/cointop:latest; true
docker-build-and-push: docker-build docker-tag docker-push
docker-build-and-push: docker-build docker-push
docker-run-ssh:
docker run -p 2222:22 -v ~/.ssh/demo:/keys -v ~/.cache/cointop:/tmp/cointop_config --entrypoint cointop -it cointop/cointop server -k /keys/id_rsa
ssh-server:
go run cmd/cointop/cointop.go server -p 2222 -k ~/.ssh/demo/id_rsa
go run cmd/cointop/cointop.go server -p 2222
ssh-client:
ssh localhost -p 2222

@ -10,14 +10,14 @@
> Coin tracking for hackers
[![License](http://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/cointop-sh/cointop/master/LICENSE)
[![Build Status](https://travis-ci.org/cointop-sh/cointop.svg?branch=master)](https://travis-ci.org/cointop-sh/cointop)
[![Go Report Card](https://goreportcard.com/badge/github.com/cointop-sh/cointop?)](https://goreportcard.com/report/github.com/cointop-sh/cointop)
[![GoDoc](https://godoc.org/github.com/cointop-sh/cointop?status.svg)](https://godoc.org/github.com/cointop-sh/cointop)
[![License](http://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/miguelmota/cointop/master/LICENSE)
[![Build Status](https://travis-ci.org/miguelmota/cointop.svg?branch=master)](https://travis-ci.org/miguelmota/cointop)
[![Go Report Card](https://goreportcard.com/badge/github.com/miguelmota/cointop?)](https://goreportcard.com/report/github.com/miguelmota/cointop)
[![GoDoc](https://godoc.org/github.com/miguelmota/cointop?status.svg)](https://godoc.org/github.com/miguelmota/cointop)
[![Mentioned in Awesome Terminals](https://awesome.re/mentioned-badge.svg)](https://github.com/k4m4/terminals-are-sexy)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing)
[`cointop`](https://github.com/cointop-sh/cointop) is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time.
[`cointop`](https://github.com/miguelmota/cointop) is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time.
The interface is inspired by [`htop`](https://en.wikipedia.org/wiki/Htop) and shortcut keys are inspired by [`vim`](https://en.wikipedia.org/wiki/Vim_(text_editor)).
@ -25,8 +25,6 @@ The interface is inspired by [`htop`](https://en.wikipedia.org/wiki/Htop) and sh
## Demo
This connects to an instance of Cointop using SSH:
```bash
ssh cointop.sh
```
@ -37,18 +35,16 @@ In action
## Table of Contents
Documentation has been moved to [docs.cointop.sh](https://docs.cointop.sh/)
- [Features](#features)
- [Documentation](https://docs.cointop.sh/)
- [Install](https://docs.cointop.sh/install)
- [Update](https://docs.cointop.sh/update)
- [Getting started](https://docs.cointop.sh/getting-started)
- [Shortcuts](https://docs.cointop.sh/shortcuts)
- [Colorschemes](https://docs.cointop.sh/colorschemes)
- [Config](https://docs.cointop.sh/config)
- [SSH server](https://docs.cointop.sh/ssh)
- [FAQ](https://docs.cointop.sh/faq)
- [Documentation](#documentation)
- [Install](#install)
- [Update](#update)
- [Getting started](#getting-started)
- [Shortcuts](#shortcuts)
- [Colorschemes](#colorschemes)
- [Config](#config)
- [SSH server](#ssh-server)
- [FAQ](#faq)
- [Contributing](#contributing)
- [Social](#social)
- [Mentioned in](#mentioned-in)
@ -57,26 +53,63 @@ Documentation has been moved to [docs.cointop.sh](https://docs.cointop.sh/)
## Features
- **Shortcut keys**: Vim-inspired shortcut keys, custom key bindings configuration
- **Colorschemes**: Custom colorscheme configuration, 256-color and 24-bit support
- **Favorites**: Save and view favorite coins
- **Portfolio**: Portfolio tracking of holdings, view profit & loss
- **Charts**: Charts for coin price history and global market graphs
- **Search**: Fuzzy searching for finding coins
- **Conversion**: Currency conversion
- **Price Alerts**: Price alerts with desktop notifications
- **Multiple APIs**: Supports multiple coin data APIs; CoinGecko and CoinMarketCap
- **Mouse**: Mouse support
- **Offline**: Offline cache
- **Fast**: Fast sort shortcuts, pagination, chart date range change, auto-refresh
- **Lightweight**: It's very lightweight; can be left running indefinitely
- Quick sort shortcuts
- Custom key bindings configuration
- Vim inspired shortcut keys
- Fast pagination
- Charts for coins and global market graphs
- Quick chart date range change
- Fuzzy searching for finding coins
- Currency conversion
- Save and view favorite coins
- Portfolio tracking of holdings
- 256-color support
- Custom colorschemes
- Help menu
- Offline cache
- Supports multiple coin stat APIs
- Auto-refresh
- Works on macOS, Linux, and Windows
- It's very lightweight; can be left running indefinitely
## Documentation
Documentation has been moved to [docs.cointop.sh](https://docs.cointop.sh/)
Some helpful documentation links are provided below.
## Install
See [docs.cointop.sh/install](https://docs.cointop.sh/install)
## Update
See [docs.cointop.sh/update](https://docs.cointop.sh/update)
## Shortcuts
See [docs.cointop.sh/shortcuts](https://docs.cointop.sh/shortcuts)
## Colorschemes
See [docs.cointop.sh/colorschemes](https://docs.cointop.sh/colorschemes)
## Config
See [docs.cointop.sh/config](https://docs.cointop.sh/config)
## SSH Server
See [docs.cointop.sh/ssh](https://docs.cointop.sh/ssh)
## FAQ
See [docs.cointop.sh/faq](https://docs.cointop.sh/faq)
## Contributing
See [docs.cointop.sh/contributing](https://docs.cointop.sh/contributing)
_Many thanks to [Simon Roberts](https://github.com/lyricnz), [Alexis Hildebrandt](https://github.com/afh), and all the [contributors](https://github.com/cointop-sh/cointop/graphs/contributors) that made cointop better._
## Social
- Follow on twitter [@cointop](https://twitter.com/cointop)
@ -94,12 +127,10 @@ Cointop has been mentioned in:
[![BTC Tip Jar](https://img.shields.io/badge/BTC-tip-yellow.svg?logo=bitcoin&style=flat)](https://www.blockchain.com/btc/address/3KdMW53vUMLPEC33xhHAUx4EFtvmXQF8Kf) `3KdMW53vUMLPEC33xhHAUx4EFtvmXQF8Kf`
[![ETH Tip Jar](https://img.shields.io/badge/ETH-tip-blue.svg?logo=ethereum&style=flat)](https://etherscan.io/address/0xC014b8F6F43f467922E93De62C9216F0538E0F8f) `0xC014b8F6F43f467922E93De62C9216F0538E0F8f`
[![ETH Tip Jar](https://img.shields.io/badge/ETH-tip-blue.svg?logo=ethereum&style=flat)](https://etherscan.io/address/0x0072cdd7c3d9963ba69506ECf50e16E963B35bb1) `0x0072cdd7c3d9963ba69506ECf50e16E963B35bb1`
Thank you for tips! 🙏
## License
Released under the [Apache 2.0](./LICENSE) license.
© [Miguel Mota](https://github.com/miguelmota)

@ -1,7 +1,7 @@
package main
import (
cmd "github.com/cointop-sh/cointop/cmd/commands"
cmd "github.com/miguelmota/cointop/cmd/commands"
)
func main() {

@ -1,30 +1,22 @@
package cmd
import (
"fmt"
"os"
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/miguelmota/cointop/pkg/filecache"
"github.com/spf13/cobra"
)
// CleanCmd will wipe the cache only
// CleanCmd ...
func CleanCmd() *cobra.Command {
config := os.Getenv("COINTOP_CONFIG")
cacheDir := os.Getenv("COINTOP_CACHE_DIR")
cacheDir := filecache.DefaultCacheDir
cleanCmd := &cobra.Command{
Use: "clean",
Short: "Clear the cache",
Long: `The clean command clears the cache`,
RunE: func(cmd *cobra.Command, args []string) error {
ct, err := cointop.NewCointop(&cointop.Config{
ConfigFilepath: config,
})
if err != nil {
return err
}
return ct.Clean(&cointop.CleanConfig{
// NOTE: if clean command, clean but don't run cointop
return cointop.Clean(&cointop.CleanConfig{
Log: true,
CacheDir: cacheDir,
})
@ -32,7 +24,6 @@ func CleanCmd() *cobra.Command {
}
cleanCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory")
cleanCmd.Flags().StringVarP(&config, "config", "c", config, fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
return cleanCmd
}

@ -1,7 +1,7 @@
package cmd
import (
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/spf13/cobra"
)

@ -3,7 +3,7 @@ package cmd
import (
"fmt"
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/spf13/cobra"
)
@ -17,12 +17,11 @@ func HoldingsCmd() *cobra.Command {
var config string
var sortBy string
var sortDesc bool
var format = "table"
var format string = "table"
var humanReadable bool
var filter []string
var cols []string
var convert string
var hideBalances bool
holdingsCmd := &cobra.Command{
Use: "holdings",
@ -69,7 +68,6 @@ func HoldingsCmd() *cobra.Command {
Cols: cols,
Convert: convert,
NoHeader: noHeader,
HideBalances: hideBalances,
})
},
}
@ -80,7 +78,6 @@ func HoldingsCmd() *cobra.Command {
holdingsCmd.Flags().BoolVarP(&noCache, "no-cache", "", noCache, "No cache")
holdingsCmd.Flags().BoolVarP(&humanReadable, "human", "h", humanReadable, "Human readable output")
holdingsCmd.Flags().BoolVarP(&noHeader, "no-header", "", noHeader, "Don't display header columns")
holdingsCmd.Flags().BoolVarP(&hideBalances, "hide-balances", "", hideBalances, "Hide portfolio balances. Useful for when sharing screen or taking screenshotss")
holdingsCmd.Flags().StringVarP(&config, "config", "c", "", fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
holdingsCmd.Flags().StringVarP(&sortBy, "sort-by", "s", sortBy, `Sort by column. Options are "name", "symbol", "price", "holdings", "balance", "24h"`)
holdingsCmd.Flags().BoolVarP(&sortDesc, "sort-desc", "d", sortDesc, "Sort in descending order")

@ -3,7 +3,7 @@ package cmd
import (
"errors"
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/spf13/cobra"
)

@ -1,30 +1,22 @@
package cmd
import (
"fmt"
"os"
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/miguelmota/cointop/pkg/filecache"
"github.com/spf13/cobra"
)
// ResetCmd will wipe cache and config file
// ResetCmd ...
func ResetCmd() *cobra.Command {
config := os.Getenv("COINTOP_CONFIG")
cacheDir := os.Getenv("COINTOP_CACHE_DIR")
cacheDir := filecache.DefaultCacheDir
resetCmd := &cobra.Command{
Use: "reset",
Short: "Resets the config and clear the cache",
Long: `The reset command resets the config and clears the cache`,
RunE: func(cmd *cobra.Command, args []string) error {
ct, err := cointop.NewCointop(&cointop.Config{
ConfigFilepath: config,
})
if err != nil {
return err
}
return ct.Reset(&cointop.ResetConfig{
// NOTE: if reset command, reset but don't run cointop
return cointop.Reset(&cointop.ResetConfig{
Log: true,
CacheDir: cacheDir,
})
@ -32,7 +24,6 @@ func ResetCmd() *cobra.Command {
}
resetCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory")
resetCmd.Flags().StringVarP(&config, "config", "c", config, fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
return resetCmd
}

@ -5,7 +5,7 @@ import (
"os"
"strconv"
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/spf13/cobra"
)
@ -20,7 +20,6 @@ func RootCmd() *cobra.Command {
hideChart := getEnvBool("COINTOP_HIDE_CHART")
hideTable := getEnvBool("COINTOP_HIDE_TABLE")
hideStatusbar := getEnvBool("COINTOP_HIDE_STATUSBAR")
hidePortfolioBalances := getEnvBool("COINTOP_HIDE_PORTFOLIO_BALANCES")
onlyTable := getEnvBool("COINTOP_ONLY_TABLE")
onlyChart := getEnvBool("COINTOP_ONLY_CHART")
silent := getEnvBool("COINTOP_SILENT")
@ -31,8 +30,6 @@ func RootCmd() *cobra.Command {
config := os.Getenv("COINTOP_CONFIG")
apiChoice := os.Getenv("COINTOP_API")
cmcAPIKey := os.Getenv("CMC_PRO_API_KEY")
coingeckoAPIKey := os.Getenv("COINGECKO_API_KEY")
coingeckoProAPIKey := os.Getenv("COINGECKO_PRO_API_KEY")
perPage := cointop.DefaultPerPage
maxPages := cointop.DefaultMaxPages
@ -62,27 +59,21 @@ See git.io/cointop for more info.`,
return nil
}
// wipe before starting program
if reset || clean {
ct, err := cointop.NewCointop(&cointop.Config{
CacheDir: cacheDir,
ConfigFilepath: config,
})
if err != nil {
// NOTE: if reset flag enabled, reset and run cointop
if reset {
if err := cointop.Reset(&cointop.ResetConfig{
Log: !silent,
}); err != nil {
return err
}
if reset {
if err := ct.Reset(&cointop.ResetConfig{
Log: !silent,
}); err != nil {
return err
}
} else if clean {
if err := ct.Clean(&cointop.CleanConfig{
Log: !silent,
}); err != nil {
return err
}
}
// NOTE: if clean flag enabled, clean and run cointop
if clean {
if err := cointop.Clean(&cointop.CleanConfig{
Log: !silent,
}); err != nil {
return err
}
}
@ -99,25 +90,22 @@ See git.io/cointop for more info.`,
}
ct, err := cointop.NewCointop(&cointop.Config{
CacheDir: cacheDir,
ColorsDir: colorsDir,
NoCache: noCache,
ConfigFilepath: config,
CoinMarketCapAPIKey: cmcAPIKey,
CoinGeckoAPIKey: coingeckoAPIKey,
CoinGeckoProAPIKey: coingeckoProAPIKey,
APIChoice: apiChoice,
Colorscheme: colorscheme,
HideMarketbar: hideMarketbar,
HideChart: hideChart,
HideTable: hideTable,
HideStatusbar: hideStatusbar,
OnlyTable: onlyTable,
OnlyChart: onlyChart,
RefreshRate: refreshRateP,
PerPage: perPage,
MaxPages: maxPages,
HidePortfolioBalances: hidePortfolioBalances,
CacheDir: cacheDir,
ColorsDir: colorsDir,
NoCache: noCache,
ConfigFilepath: config,
CoinMarketCapAPIKey: cmcAPIKey,
APIChoice: apiChoice,
Colorscheme: colorscheme,
HideMarketbar: hideMarketbar,
HideChart: hideChart,
HideTable: hideTable,
HideStatusbar: hideStatusbar,
OnlyTable: onlyTable,
OnlyChart: onlyChart,
RefreshRate: refreshRateP,
PerPage: perPage,
MaxPages: maxPages,
})
if err != nil {
return err
@ -135,7 +123,6 @@ See git.io/cointop for more info.`,
rootCmd.Flags().BoolVarP(&hideChart, "hide-chart", "", hideChart, "Hide the chart view")
rootCmd.Flags().BoolVarP(&hideTable, "hide-table", "", hideTable, "Hide the table view")
rootCmd.Flags().BoolVarP(&hideStatusbar, "hide-statusbar", "", hideStatusbar, "Hide the bottom statusbar")
rootCmd.Flags().BoolVarP(&hidePortfolioBalances, "hide-portfolio-balances", "", hidePortfolioBalances, "Hide portfolio balances. Useful for when sharing screen or taking screenshots")
rootCmd.Flags().BoolVarP(&onlyTable, "only-table", "", onlyTable, "Show only the table. Hides the chart and top and bottom bars")
rootCmd.Flags().BoolVarP(&onlyChart, "only-chart", "", onlyChart, "Show only the chart. Hides the table and top and bottom bars")
rootCmd.Flags().BoolVarP(&silent, "silent", "s", silent, "Silence log ouput")
@ -144,9 +131,7 @@ See git.io/cointop for more info.`,
rootCmd.Flags().UintVarP(&perPage, "per-page", "", perPage, "Per page")
rootCmd.Flags().UintVarP(&maxPages, "max-pages", "", maxPages, "Max number of pages")
rootCmd.Flags().StringVarP(&config, "config", "c", config, fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
rootCmd.Flags().StringVarP(&cmcAPIKey, "coinmarketcap-api-key", "", cmcAPIKey, "Set the CoinMarketCap Pro API key")
rootCmd.Flags().StringVarP(&coingeckoAPIKey, "coingecko-api-key", "", coingeckoAPIKey, "Set the CoinGecko Demo API key")
rootCmd.Flags().StringVarP(&coingeckoProAPIKey, "coingecko-pro-api-key", "", coingeckoProAPIKey, "Set the CoinGecko Pro API key")
rootCmd.Flags().StringVarP(&cmcAPIKey, "coinmarketcap-api-key", "", cmcAPIKey, "Set the CoinMarketCap API key")
rootCmd.Flags().StringVarP(&apiChoice, "api", "", apiChoice, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"")
rootCmd.Flags().StringVarP(&colorscheme, "colorscheme", "", colorscheme, fmt.Sprintf("Colorscheme to use (default \"cointop\").\n%s", cointop.ColorschemeHelpString()))
rootCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, fmt.Sprintf("Cache directory (default %s)", cointop.DefaultCacheDir))

@ -4,25 +4,23 @@ package cmd
import (
"fmt"
"os"
"strings"
"time"
cssh "github.com/cointop-sh/cointop/pkg/ssh"
cssh "github.com/miguelmota/cointop/pkg/ssh"
"github.com/spf13/cobra"
)
// ServerCmd ...
func ServerCmd() *cobra.Command {
port := uint(22)
address := "0.0.0.0"
idleTimeout := uint(0)
maxTimeout := uint(0)
maxSessions := uint(0)
executableBinary := "cointop"
hostKeyFile := cssh.DefaultHostKeyFile
userConfigType := cssh.UserConfigTypePublicKey
colorsDir := os.Getenv("COINTOP_COLORS_DIR")
var port uint = 22
var address string = "0.0.0.0"
var idleTimeout uint = 0
var maxTimeout uint = 0
var maxSessions uint = 0
var executableBinary string = "cointop"
var hostKeyFile string = cssh.DefaultHostKeyFile
var userConfigType string = cssh.UserConfigTypePublicKey
serverCmd := &cobra.Command{
Use: "server",
@ -38,7 +36,6 @@ func ServerCmd() *cobra.Command {
ExecutableBinary: executableBinary,
HostKeyFile: hostKeyFile,
UserConfigType: userConfigType,
ColorsDir: colorsDir,
})
fmt.Printf("Running SSH server on port %v\n", port)

@ -1,7 +1,7 @@
package cmd
import (
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/spf13/cobra"
)

@ -1,7 +1,7 @@
package cmd
import (
"github.com/cointop-sh/cointop/cointop"
"github.com/miguelmota/cointop/cointop"
"github.com/spf13/cobra"
)

@ -4,7 +4,6 @@ package cointop
func ActionsMap() map[string]bool {
return map[string]bool{
"first_page": true,
"move_to_first_page_first_row": true,
"help": true,
"toggle_show_help": true,
"close_help": true,
@ -29,7 +28,6 @@ func ActionsMap() map[string]bool {
"sort_column_24h_volume": true,
"sort_column_7d_change": true,
"sort_column_30d_change": true,
"sort_column_1y_change": true,
"sort_column_asc": true,
"sort_column_available_supply": true,
"sort_column_desc": true,
@ -57,21 +55,6 @@ func ActionsMap() map[string]bool {
"toggle_show_portfolio": true,
"enlarge_chart": true,
"shorten_chart": true,
"toggle_chart_fullscreen": true,
"scroll_right": true,
"show_portfolio_edit_menu": true,
"sort_column_percent_holdings": true,
"toggle_portfolio_balances": true,
"scroll_left": true,
"save": true,
"toggle_table_fullscreen": true,
"toggle_price_alerts": true,
"move_down_or_next_page": true,
"show_price_alert_add_menu": true,
"sort_column_balance": true,
"sort_column_cost": true,
"sort_column_pnl": true,
"sort_column_pnl_percent": true,
}
}

@ -10,17 +10,5 @@ func (ct *Cointop) SetActiveView(v string) error {
} else if v == ct.Views.Table.Name() {
ct.g.SetViewOnTop(ct.Views.Statusbar.Name())
}
// TODO: better way to map/unmap shortcut key actions based on active view
if v == ct.Views.Table.Name() {
if err := ct.SetKeybindingAction("/", "open_search"); err != nil {
return err
}
} else {
// deletes binding to allow using "/" key on input fields
if err := ct.DeleteKeybinding("/"); err != nil {
return err
}
}
return nil
}

@ -4,33 +4,17 @@ import (
"fmt"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
// CacheKey returns cached value given key
func (ct *Cointop) CacheKey(key string) string {
ct.debuglog("CacheKey()")
return strings.ToLower(fmt.Sprintf("%s_%s", ct.apiChoice, key))
}
// CompositeCacheKey returns a CacheKey for a coin (or globaldata)
func (ct *Cointop) CompositeCacheKey(symbol string, name string, convert string, chartRange string) string {
keyname := symbol
if name != "" {
keyname += "-" + name
}
if convert != "" {
keyname += "_" + convert
}
if chartRange != "" {
keyname += "_" + strings.Replace(chartRange, " ", "", -1) // "All Time" contains space
}
return ct.CacheKey(keyname)
}
// CacheAllCoinsSlugMap writes the coins map to the memory and disk cache
func (ct *Cointop) CacheAllCoinsSlugMap() {
log.Debug("CacheAllCoinsSlugMap()")
ct.debuglog("CacheAllCoinsSlugMap()")
allCoinsSlugMap := make(map[string]*Coin)
ct.State.allCoinsSlugMap.Range(func(key, value interface{}) bool {
allCoinsSlugMap[key.(string)] = value.(*Coin)

@ -2,30 +2,23 @@ package cointop
import (
"fmt"
"math"
"sort"
"strings"
"sync"
"time"
"github.com/cointop-sh/cointop/pkg/chartplot"
"github.com/cointop-sh/cointop/pkg/timedata"
"github.com/cointop-sh/cointop/pkg/timeutil"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/chartplot"
"github.com/miguelmota/cointop/pkg/timeutil"
"github.com/miguelmota/cointop/pkg/ui"
)
// PriceData is the time-series data for a Coin used when building a Portfolio view for chart
type PriceData struct {
coin *Coin
data [][]float64
}
// ChartView is structure for chart view
type ChartView = ui.View
// NewChartView returns a new chart view
func NewChartView() *ChartView {
return ui.NewView("chart")
var view *ChartView = ui.NewView("chart")
return view
}
var chartLock sync.Mutex
@ -49,23 +42,23 @@ func ChartRanges() []string {
// ChartRangesMap returns map of chart range time ranges
func ChartRangesMap() map[string]time.Duration {
return map[string]time.Duration{
"All Time": 10 * 365 * 24 * time.Hour,
"YTD": 1 * time.Second, // this will be calculated
"1Y": 365 * 24 * time.Hour,
"6M": 365 / 2 * 24 * time.Hour,
"3M": 365 / 4 * 24 * time.Hour,
"1M": 365 / 12 * 24 * time.Hour,
"7D": 24 * 7 * time.Hour,
"3D": 24 * 3 * time.Hour,
"24H": 24 * time.Hour,
"6H": 6 * time.Hour,
"1H": 1 * time.Hour,
"All Time": time.Duration(24 * 7 * 4 * 12 * 5 * time.Hour),
"YTD": time.Duration(1 * time.Second), // this will be calculated
"1Y": time.Duration(24 * 7 * 4 * 12 * time.Hour),
"6M": time.Duration(24 * 7 * 4 * 6 * time.Hour),
"3M": time.Duration(24 * 7 * 4 * 3 * time.Hour),
"1M": time.Duration(24 * 7 * 4 * time.Hour),
"7D": time.Duration(24 * 7 * time.Hour),
"3D": time.Duration(24 * 3 * time.Hour),
"24H": time.Duration(24 * time.Hour),
"6H": time.Duration(6 * time.Hour),
"1H": time.Duration(1 * time.Hour),
}
}
// UpdateChart updates the chart view
func (ct *Cointop) UpdateChart() error {
log.Debug("UpdateChart()")
ct.debuglog("UpdateChart()")
chartLock.Lock()
defer chartLock.Unlock()
@ -102,9 +95,9 @@ func (ct *Cointop) UpdateChart() error {
return nil
}
// ChartPoints calculates the chart points
// ChartPoints calculates the the chart points
func (ct *Cointop) ChartPoints(symbol string, name string) error {
log.Debug("ChartPoints()")
ct.debuglog("ChartPoints()")
maxX := ct.ChartWidth()
chartPointsLock.Lock()
@ -118,7 +111,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange]
if ct.State.selectedChartRange == "YTD" {
ytd := time.Now().Unix() - timeutil.BeginningOfYear().Unix()
ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix())
rangeseconds = time.Duration(ytd) * time.Second
}
@ -127,29 +120,32 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
start := nowseconds - int64(rangeseconds.Seconds())
end := nowseconds
var cacheData [][]float64
var data []float64
keyname := symbol
if keyname == "" {
keyname = "globaldata"
}
cachekey := ct.CompositeCacheKey(keyname, name, ct.State.currencyConversion, ct.State.selectedChartRange)
cachekey := ct.CacheKey(fmt.Sprintf("%s_%s", keyname, strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
cached, found := ct.cache.Get(cachekey)
if found {
// cache hit
cacheData, _ = cached.([][]float64)
log.Debug("ChartPoints() soft cache hit")
data, _ = cached.([]float64)
ct.debuglog("ct.ChartPoints() soft cache hit")
}
if len(cacheData) == 0 {
if len(data) == 0 {
if symbol == "" {
convert := ct.State.currencyConversion
graphData, err := ct.api.GetGlobalMarketGraphData(convert, start, end)
if err != nil {
return nil
}
cacheData = graphData.MarketCapByAvailableSupply
for i := range graphData.MarketCapByAvailableSupply {
price := graphData.MarketCapByAvailableSupply[i][1]
data = append(data, price)
}
} else {
convert := ct.State.currencyConversion
graphData, err := ct.api.GetCoinGraphData(convert, symbol, name, start, end)
@ -160,39 +156,21 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
sort.Slice(sorted[:], func(i, j int) bool {
return sorted[i][0] < sorted[j][0]
})
cacheData = sorted
for i := range sorted {
price := sorted[i][1]
data = append(data, price)
}
}
ct.cache.Set(cachekey, cacheData, 10*time.Second)
ct.cache.Set(cachekey, data, 10*time.Second)
if ct.filecache != nil {
go func() {
ct.filecache.Set(cachekey, cacheData, 24*time.Hour)
ct.filecache.Set(cachekey, data, 24*time.Hour)
}()
}
}
var labels []string
var data []float64
timeQuantum := timedata.CalculateTimeQuantum(cacheData) // will be 0 if <2 points
if timeQuantum > 0 {
// Resample cachedata
newStart := cacheData[0][0] // use the first data point
newEnd := time.Unix(end, 0).Add(-timeQuantum)
timeData := timedata.ResampleTimeSeriesData(cacheData, newStart, float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX))
labels = timedata.BuildTimeSeriesLabels(timeData)
// Extract just the values from the data
for i := range timeData {
value := timeData[i][1]
if math.IsNaN(value) {
value = 0.0
}
data = append(data, value)
}
}
chart.SetData(data)
chart.SetDataLabels(labels)
ct.State.chartPoints = chart.GetChartPoints(maxX)
return nil
@ -200,7 +178,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
// PortfolioChart renders the portfolio chart
func (ct *Cointop) PortfolioChart() error {
log.Debug("PortfolioChart()")
ct.debuglog("PortfolioChart()")
maxX := ct.ChartWidth()
chartPointsLock.Lock()
defer chartPointsLock.Unlock()
@ -211,11 +189,9 @@ func (ct *Cointop) PortfolioChart() error {
chart := chartplot.NewChartPlot()
chart.SetHeight(ct.State.chartHeight)
convert := ct.State.currencyConversion // cache here
selectedChartRange := ct.State.selectedChartRange // cache here
rangeseconds := ct.chartRangesMap[selectedChartRange]
if selectedChartRange == "YTD" {
ytd := time.Now().Unix() - timeutil.BeginningOfYear().Unix()
rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange]
if ct.State.selectedChartRange == "YTD" {
ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix())
rangeseconds = time.Duration(ytd) * time.Second
}
@ -224,7 +200,7 @@ func (ct *Cointop) PortfolioChart() error {
start := nowseconds - int64(rangeseconds.Seconds())
end := nowseconds
var allCacheData []PriceData
var data []float64
portfolio := ct.GetPortfolioSlice()
chartname := ct.SelectedCoinName()
for _, p := range portfolio {
@ -239,98 +215,55 @@ func (ct *Cointop) PortfolioChart() error {
continue
}
var cacheData [][]float64 // [][time,value]
cachekey := ct.CompositeCacheKey(p.Symbol, p.Name, convert, selectedChartRange)
var graphData []float64
cachekey := strings.ToLower(fmt.Sprintf("%s_%s", p.Symbol, strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
cached, found := ct.cache.Get(cachekey)
if found {
// cache hit
cacheData, _ = cached.([][]float64)
log.Debug("PortfolioChart() soft cache hit")
graphData, _ = cached.([]float64)
ct.debuglog("soft cache hit")
} else {
if ct.filecache != nil {
ct.filecache.Get(cachekey, &cacheData)
ct.filecache.Get(cachekey, &graphData)
}
if len(cacheData) == 0 {
if len(graphData) == 0 {
time.Sleep(2 * time.Second)
convert := ct.State.currencyConversion
apiGraphData, err := ct.api.GetCoinGraphData(convert, p.Symbol, p.Name, start, end)
if err != nil {
return err
}
cacheData = apiGraphData.Price
sort.Slice(cacheData[:], func(i, j int) bool {
return cacheData[i][0] < cacheData[j][0]
sorted := apiGraphData.Price
sort.Slice(sorted[:], func(i, j int) bool {
return sorted[i][0] < sorted[j][0]
})
for i := range sorted {
price := sorted[i][1]
graphData = append(graphData, price)
}
}
ct.cache.Set(cachekey, cacheData, 10*time.Second)
ct.cache.Set(cachekey, graphData, 10*time.Second)
if ct.filecache != nil {
go func() {
ct.filecache.Set(cachekey, cacheData, 24*time.Hour)
ct.filecache.Set(cachekey, graphData, 24*time.Hour)
}()
}
}
allCacheData = append(allCacheData, PriceData{p, cacheData})
}
// Use the gap between price samples to adjust start/end in by one interval
var timeQuantum time.Duration
for _, cacheData := range allCacheData {
timeQuantum = timedata.CalculateTimeQuantum(cacheData.data)
if timeQuantum != 0 {
break // use the first one
}
}
// If there is data, resample and sum
var data []float64
var labels []string
if timeQuantum > 0 {
newStart := time.Unix(start, 0).Add(timeQuantum)
newEnd := time.Unix(end, 0).Add(-timeQuantum)
// Resample and sum data
for i, cacheData := range allCacheData {
coinData := timedata.ResampleTimeSeriesData(cacheData.data, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX))
if i == 0 {
labels = timedata.BuildTimeSeriesLabels(coinData)
}
// sum (excluding NaN)
for i := range coinData {
price := coinData[i][1]
if math.IsNaN(price) {
price = 0.0
}
sum := cacheData.coin.Holdings * price
if i < len(data) {
data[i] += sum
} else {
data = append(data, sum)
}
}
}
// Scale Portfolio Balances to hide value
if ct.State.hidePortfolioBalances {
scalePrice := 0.0
for _, price := range data {
if price > scalePrice {
scalePrice = price
}
}
if scalePrice > 0.0 {
for i, price := range data {
data[i] = 100 * price / scalePrice
}
for i := range graphData {
price := graphData[i]
sum := p.Holdings * price
if len(data)-1 >= i {
data[i] += sum
}
data = append(data, sum)
}
}
chart.SetData(data)
chart.SetDataLabels(labels)
ct.State.chartPoints = chart.GetChartPoints(maxX)
return nil
@ -338,7 +271,7 @@ func (ct *Cointop) PortfolioChart() error {
// ShortenChart decreases the chart height by one row
func (ct *Cointop) ShortenChart() error {
log.Debug("ShortenChart()")
ct.debuglog("ShortenChart()")
candidate := ct.State.chartHeight - 1
if candidate < 5 {
return nil
@ -346,17 +279,13 @@ func (ct *Cointop) ShortenChart() error {
ct.State.chartHeight = candidate
ct.State.lastChartHeight = ct.State.chartHeight
if err := ct.Save(); err != nil {
return err
}
go ct.UpdateChart()
return nil
}
// EnlargeChart increases the chart height by one row
func (ct *Cointop) EnlargeChart() error {
log.Debug("EnlargeChart()")
ct.debuglog("EnlargeChart()")
candidate := ct.State.lastChartHeight + 1
if candidate > 30 {
return nil
@ -364,17 +293,13 @@ func (ct *Cointop) EnlargeChart() error {
ct.State.chartHeight = candidate
ct.State.lastChartHeight = ct.State.chartHeight
if err := ct.Save(); err != nil {
return err
}
go ct.UpdateChart()
return nil
}
// NextChartRange sets the chart to the next range option
func (ct *Cointop) NextChartRange() error {
log.Debug("NextChartRange()")
ct.debuglog("NextChartRange()")
sel := 0
max := len(ct.chartRanges)
for i, k := range ct.chartRanges {
@ -395,7 +320,7 @@ func (ct *Cointop) NextChartRange() error {
// PrevChartRange sets the chart to the prevous range option
func (ct *Cointop) PrevChartRange() error {
log.Debug("PrevChartRange()")
ct.debuglog("PrevChartRange()")
sel := 0
for i, k := range ct.chartRanges {
if k == ct.State.selectedChartRange {
@ -414,7 +339,7 @@ func (ct *Cointop) PrevChartRange() error {
// FirstChartRange sets the chart to the first range option
func (ct *Cointop) FirstChartRange() error {
log.Debug("FirstChartRange()")
ct.debuglog("FirstChartRange()")
ct.State.selectedChartRange = ct.chartRanges[0]
go ct.UpdateChart()
return nil
@ -422,7 +347,7 @@ func (ct *Cointop) FirstChartRange() error {
// LastChartRange sets the chart to the last range option
func (ct *Cointop) LastChartRange() error {
log.Debug("LastChartRange()")
ct.debuglog("LastChartRange()")
ct.State.selectedChartRange = ct.chartRanges[len(ct.chartRanges)-1]
go ct.UpdateChart()
return nil
@ -430,7 +355,7 @@ func (ct *Cointop) LastChartRange() error {
// ToggleCoinChart toggles between the global chart and the coin chart
func (ct *Cointop) ToggleCoinChart() error {
log.Debug("ToggleCoinChart()")
ct.debuglog("ToggleCoinChart()")
highlightedcoin := ct.HighlightedRowCoin()
if ct.State.selectedCoin == highlightedcoin {
ct.State.selectedCoin = nil
@ -452,7 +377,7 @@ func (ct *Cointop) ToggleCoinChart() error {
// ShowChartLoader shows chart loading indicator
func (ct *Cointop) ShowChartLoader() error {
log.Debug("ShowChartLoader()")
ct.debuglog("ShowChartLoader()")
ct.UpdateUI(func() error {
content := "\n\nLoading..."
return ct.Views.Chart.Update(ct.colorscheme.Chart(content))
@ -463,10 +388,10 @@ func (ct *Cointop) ShowChartLoader() error {
// ChartWidth returns the width for chart
func (ct *Cointop) ChartWidth() int {
log.Debug("ChartWidth()")
w := ct.Width()
max := ct.State.maxChartWidth
if max > 0 && w > max {
ct.debuglog("chartWidth()")
w := ct.width()
max := 130
if w > max {
return max
}
@ -475,7 +400,7 @@ func (ct *Cointop) ChartWidth() int {
// ToggleChartFullscreen toggles the chart fullscreen mode
func (ct *Cointop) ToggleChartFullscreen() error {
log.Debug("ToggleChartFullscreen()")
ct.debuglog("ToggleChartFullscreen()")
ct.State.onlyChart = !ct.State.onlyChart
ct.State.onlyTable = false
if !ct.State.onlyChart {

@ -1,9 +1,5 @@
package cointop
import (
log "github.com/sirupsen/logrus"
)
// Coin is the row structure
type Coin struct {
ID string
@ -20,20 +16,17 @@ type Coin struct {
PercentChange24H float64
PercentChange7D float64
PercentChange30D float64
PercentChange1Y float64
LastUpdated string
// for favorites
Favorite bool
// for portfolio
Holdings float64
Balance float64
BuyPrice float64
BuyCurrency string
Holdings float64
Balance float64
}
// AllCoins returns a slice of all the coins
func (ct *Cointop) AllCoins() []*Coin {
log.Debug("AllCoins()")
ct.debuglog("AllCoins()")
if ct.IsFavoritesVisible() {
var list []*Coin
for i := range ct.State.allCoins {
@ -61,7 +54,7 @@ func (ct *Cointop) AllCoins() []*Coin {
// CoinBySymbol returns the coin struct given the symbol
func (ct *Cointop) CoinBySymbol(symbol string) *Coin {
log.Debug("CoinBySymbol()")
ct.debuglog("CoinBySymbol()")
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.Symbol == symbol {
@ -73,7 +66,7 @@ func (ct *Cointop) CoinBySymbol(symbol string) *Coin {
// CoinByName returns the coin struct given the name
func (ct *Cointop) CoinByName(name string) *Coin {
log.Debug("CoinByName()")
ct.debuglog("CoinByName()")
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.Name == name {
@ -85,7 +78,7 @@ func (ct *Cointop) CoinByName(name string) *Coin {
// CoinByID returns the coin struct given the ID
func (ct *Cointop) CoinByID(id string) *Coin {
log.Debug("CoinByID()")
ct.debuglog("CoinByID()")
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.ID == id {
@ -94,36 +87,3 @@ func (ct *Cointop) CoinByID(id string) *Coin {
}
return nil
}
// UpdateCoin updates coin info after fetching from API
func (ct *Cointop) UpdateCoin(coin *Coin) error {
log.Debug("UpdateCoin()")
v, err := ct.api.GetCoinData(coin.Name, ct.State.currencyConversion)
if err != nil {
log.Debugf("UpdateCoin() could not fetch coin data %s", coin.Name)
return err
}
coin = &Coin{
ID: v.ID,
Name: v.Name,
Symbol: v.Symbol,
Rank: v.Rank,
Price: v.Price,
Volume24H: v.Volume24H,
MarketCap: v.MarketCap,
AvailableSupply: v.AvailableSupply,
TotalSupply: v.TotalSupply,
PercentChange1H: v.PercentChange1H,
PercentChange24H: v.PercentChange24H,
PercentChange7D: v.PercentChange7D,
PercentChange30D: v.PercentChange30D,
PercentChange1Y: v.PercentChange1Y,
LastUpdated: v.LastUpdated,
Slug: v.Slug,
}
ct.State.allCoinsSlugMap.Store(coin.Name, coin)
return nil
}

@ -5,8 +5,8 @@ import (
"strconv"
"time"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/cointop-sh/cointop/pkg/table"
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/table"
)
// SupportedCoinTableHeaders are all the supported coin table header columns
@ -19,7 +19,6 @@ var SupportedCoinTableHeaders = []string{
"24h_change",
"7d_change",
"30d_change",
"1y_change",
"24h_volume",
"market_cap",
"available_supply",
@ -60,15 +59,15 @@ func (ct *Cointop) GetCoinsTableHeaders() []string {
// GetCoinsTable returns the table for diplaying the coins
func (ct *Cointop) GetCoinsTable() *table.Table {
maxX := ct.Width()
maxX := ct.width()
t := table.NewTable().SetWidth(maxX)
var rows [][]*table.RowCell
headers := ct.GetCoinsTableHeaders()
if ct.IsFavoritesVisible() {
headers = ct.GetFavoritesTableHeaders()
}
ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
ct.ClearSyncMap(ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft)
for _, coin := range ct.State.coins {
if coin == nil {
continue
@ -82,7 +81,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
star := " "
rankcolor := ct.colorscheme.TableRow
if coin.Favorite {
star = ct.State.favoriteChar
star = "*"
rankcolor = ct.colorscheme.TableRowFavorite
}
rank := fmt.Sprintf("%s%6v ", star, coin.Rank)
@ -123,7 +122,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
Text: symbol,
})
case "price":
text := ct.FormatPrice(coin.Price)
text := humanize.Monetaryf(coin.Price, 2)
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -136,9 +135,6 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "24h_volume":
text := humanize.Monetaryf(coin.Volume24H, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.Volume24H, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -225,30 +221,8 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
Color: color30d,
Text: text,
})
case "1y_change":
color1y := ct.colorscheme.TableColumnChange
if coin.PercentChange1Y > 0 {
color1y = ct.colorscheme.TableColumnChangeUp
}
if coin.PercentChange1Y < 0 {
color1y = ct.colorscheme.TableColumnChangeDown
}
text := fmt.Sprintf("%v%%", humanize.Numericf(coin.PercentChange1Y, 2))
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: color1y,
Text: text,
})
case "market_cap":
text := humanize.Monetaryf(coin.MarketCap, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.MarketCap, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -261,9 +235,6 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "total_supply":
text := humanize.Numericf(coin.TotalSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.TotalSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -276,9 +247,6 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "available_supply":
text := humanize.Numericf(coin.AvailableSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.AvailableSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -291,7 +259,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "last_updated":
unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
lastUpdated := humanize.FormatTime(time.Unix(unix, 0), "15:04:05 Jan 02")
lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02")
ct.SetTableColumnWidthFromString(header, lastUpdated)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,

@ -9,16 +9,14 @@ import (
"sync"
"time"
"github.com/cointop-sh/cointop/pkg/api"
"github.com/cointop-sh/cointop/pkg/api/types"
"github.com/cointop-sh/cointop/pkg/cache"
"github.com/cointop-sh/cointop/pkg/filecache"
"github.com/cointop-sh/cointop/pkg/gocui"
"github.com/cointop-sh/cointop/pkg/pathutil"
"github.com/cointop-sh/cointop/pkg/table"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/api"
"github.com/miguelmota/cointop/pkg/api/types"
"github.com/miguelmota/cointop/pkg/cache"
"github.com/miguelmota/cointop/pkg/filecache"
"github.com/miguelmota/cointop/pkg/pathutil"
"github.com/miguelmota/cointop/pkg/table"
"github.com/miguelmota/cointop/pkg/ui"
"github.com/miguelmota/gocui"
)
// TODO: clean up and optimize codebase
@ -35,11 +33,6 @@ type Views struct {
Input *InputView
}
type sortConstraint struct {
sortBy string
sortDesc bool
}
// State is the state preferences of cointop
type State struct {
allCoins []*Coin
@ -51,19 +44,17 @@ type State struct {
coinsTableColumns []string
convertMenuVisible bool
defaultView string
defaultChartRange string
maxChartWidth int
columnLookup []string
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions.
favoritesBySymbol map[string]bool
favorites map[string]bool
favoritesTableColumns []string
favoriteChar string
helpVisible bool
hideMarketbar bool
hideChart bool
hideTable bool
hideStatusbar bool
hidePortfolioBalances bool
keepRowFocusOnSort bool
lastSelectedRowIndex int
marketBarHeight int
@ -76,13 +67,13 @@ type State struct {
refreshRate time.Duration
running bool
searchFieldVisible bool
lastSearchQuery string
selectedCoin *Coin
selectedChartRange string
selectedView string
lastSelectedView string
shortcutKeys map[string]string
viewSorts map[string]*sortConstraint
sortDesc bool
sortBy string
tableOffsetX int
onlyTable bool
onlyChart bool
@ -93,13 +84,6 @@ type State struct {
priceAlerts *PriceAlerts
priceAlertEditID string
priceAlertNewID string
compactNotation bool
tableCompactNotation bool
favoritesCompactNotation bool
portfolioCompactNotation bool
enableMouse bool
altCoinLink string
}
// Cointop cointop
@ -110,7 +94,7 @@ type Cointop struct {
apiKeys *APIKeys
cache *cache.Cache
colorsDir string
config ConfigFileConfig
config config // toml config
configFilepath string
api api.Interface
apiChoice string
@ -118,6 +102,7 @@ type Cointop struct {
chartRangesMap map[string]time.Duration
colorschemeName string
colorscheme *Colorscheme
debug bool
filecache *filecache.FileCache
logfile *os.File
forceRefresh chan bool
@ -133,10 +118,8 @@ type Cointop struct {
// PortfolioEntry is portfolio entry
type PortfolioEntry struct {
Coin string
Holdings float64
BuyPrice float64
BuyCurrency string
Coin string
Holdings float64
}
// Portfolio is portfolio structure
@ -163,33 +146,28 @@ type PriceAlerts struct {
// Config config options
type Config struct {
APIChoice string
CacheDir string
ColorsDir string
Colorscheme string
ConfigFilepath string
CoinMarketCapAPIKey string
CoinGeckoAPIKey string
CoinGeckoProAPIKey string
NoPrompts bool
HideMarketbar bool
HideChart bool
HideTable bool
HideStatusbar bool
HidePortfolioBalances bool
NoCache bool
OnlyTable bool
OnlyChart bool
RefreshRate *uint
PerPage uint
MaxPages uint
APIChoice string
CacheDir string
ColorsDir string
Colorscheme string
ConfigFilepath string
CoinMarketCapAPIKey string
NoPrompts bool
HideMarketbar bool
HideChart bool
HideTable bool
HideStatusbar bool
NoCache bool
OnlyTable bool
OnlyChart bool
RefreshRate *uint
PerPage uint
MaxPages uint
}
// APIKeys is api keys structure
type APIKeys struct {
cmc string
coingecko string
coingeckoPro string
cmc string
}
// DefaultCurrency ...
@ -198,29 +176,14 @@ var DefaultCurrency = "USD"
// DefaultChartRange ...
var DefaultChartRange = "1Y"
// DefaultCompactNotation ...
var DefaultCompactNotation = false
// DefaultEnableMouse ...
var DefaultEnableMouse = true
// DefaultAltCoinLink ...
var DefaultAltCoinLink = ""
// DefaultMaxChartWidth ...
var DefaultMaxChartWidth = 175
// DefaultChartHeight ...
var DefaultChartHeight = 10
// DefaultSortBy ...
var DefaultSortBy = "rank"
// DefaultPerPage ...
var DefaultPerPage = uint(100)
var DefaultPerPage uint = 100
// DefaultMaxPages ...
var DefaultMaxPages = uint(10)
// MaxPages
var DefaultMaxPages uint = 35
// DefaultColorscheme ...
var DefaultColorscheme = "cointop"
@ -231,11 +194,16 @@ var DefaultConfigFilepath = pathutil.NormalizePath(":PREFERRED_CONFIG_HOME:/coin
// DefaultCacheDir ...
var DefaultCacheDir = filecache.DefaultCacheDir
// DefaultFavoriteChar ...
var DefaultFavoriteChar = "*"
// DefaultColorsDir ...
var DefaultColorsDir = fmt.Sprintf("%s/colors", DefaultConfigFilepath)
// NewCointop initializes cointop
func NewCointop(config *Config) (*Cointop, error) {
var debug bool
if os.Getenv("DEBUG") != "" {
debug = true
}
if config == nil {
config = &Config{}
}
@ -266,24 +234,23 @@ func NewCointop(config *Config) (*Cointop, error) {
colorsDir: config.ColorsDir,
configFilepath: configFilepath,
chartRanges: ChartRanges(),
debug: debug,
chartRangesMap: ChartRangesMap(),
limiter: time.NewTicker(2 * time.Second).C,
filecache: nil,
State: &State{
allCoins: []*Coin{},
cacheDir: DefaultCacheDir,
coinsTableColumns: DefaultCoinTableHeaders,
currencyConversion: DefaultCurrency,
defaultChartRange: DefaultChartRange,
maxChartWidth: DefaultMaxChartWidth,
allCoins: []*Coin{},
cacheDir: DefaultCacheDir,
coinsTableColumns: DefaultCoinTableHeaders,
currencyConversion: DefaultCurrency,
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
favoritesBySymbol: make(map[string]bool),
favorites: make(map[string]bool),
favoritesTableColumns: DefaultCoinTableHeaders,
favoriteChar: DefaultFavoriteChar,
hideMarketbar: config.HideMarketbar,
hideChart: config.HideChart,
hideTable: config.HideTable,
hideStatusbar: config.HideStatusbar,
hidePortfolioBalances: config.HidePortfolioBalances,
keepRowFocusOnSort: false,
marketBarHeight: 1,
maxPages: int(maxPages),
@ -292,18 +259,15 @@ func NewCointop(config *Config) (*Cointop, error) {
refreshRate: 60 * time.Second,
selectedChartRange: DefaultChartRange,
shortcutKeys: DefaultShortcuts(),
selectedView: CoinsView,
sortBy: DefaultSortBy,
page: 0,
perPage: int(perPage),
viewSorts: map[string]*sortConstraint{
CoinsView: {DefaultSortBy, false},
},
portfolio: &Portfolio{
Entries: make(map[string]*PortfolioEntry),
},
portfolioTableColumns: DefaultPortfolioTableHeaders,
chartHeight: DefaultChartHeight,
lastChartHeight: DefaultChartHeight,
chartHeight: 10,
lastChartHeight: 10,
tableOffsetX: 0,
tableColumnWidths: sync.Map{},
tableColumnAlignLeft: sync.Map{},
@ -311,12 +275,6 @@ func NewCointop(config *Config) (*Cointop, error) {
Entries: make([]*PriceAlert, 0),
SoundEnabled: true,
},
compactNotation: DefaultCompactNotation,
enableMouse: DefaultEnableMouse,
altCoinLink: DefaultAltCoinLink,
tableCompactNotation: DefaultCompactNotation,
favoritesCompactNotation: DefaultCompactNotation,
portfolioCompactNotation: DefaultCompactNotation,
},
Views: &Views{
Chart: NewChartView(),
@ -329,8 +287,9 @@ func NewCointop(config *Config) (*Cointop, error) {
Input: NewInputView(),
},
}
ct.setLogConfiguration()
if debug {
ct.initlog()
}
err := ct.SetupConfig()
if err != nil {
@ -387,25 +346,11 @@ func NewCointop(config *Config) (*Cointop, error) {
}
}
if config.CoinGeckoAPIKey != "" {
ct.apiKeys.coingecko = config.CoinGeckoAPIKey
if err := ct.SaveConfig(); err != nil {
return nil, err
}
}
if config.CoinGeckoProAPIKey != "" {
ct.apiKeys.coingeckoPro = config.CoinGeckoProAPIKey
if err := ct.SaveConfig(); err != nil {
return nil, err
}
}
if config.Colorscheme != "" {
ct.colorschemeName = config.Colorscheme
}
colors, err := ct.GetColorschemeColors()
colors, err := ct.getColorschemeColors()
if err != nil {
return nil, err
}
@ -438,55 +383,10 @@ func NewCointop(config *Config) (*Cointop, error) {
}
}
if ct.apiChoice == CoinGecko && ct.apiKeys.coingecko == "" {
apiKey := os.Getenv("COINGECKO_API_KEY")
if apiKey == "" {
// if !config.NoPrompts {
// apiKey, err = ct.ReadAPIKeyFromStdin("CoinGecko")
// if err != nil {
// return nil, err
// }
// ct.apiKeys.coingecko = apiKey
// }
} else {
ct.apiKeys.coingecko = apiKey
}
if err := ct.SaveConfig(); err != nil {
return nil, err
}
}
if ct.apiChoice == CoinGecko && ct.apiKeys.coingeckoPro == "" {
apiKey := os.Getenv("COINGECKO_PRO_API_KEY")
if apiKey == "" {
// if !config.NoPrompts {
// apiKey, err = ct.ReadAPIKeyFromStdin("CoinGecko Pro")
// if err != nil {
// return nil, err
// }
// ct.apiKeys.coingeckoPro = apiKey
// }
} else {
ct.apiKeys.coingeckoPro = apiKey
}
if err := ct.SaveConfig(); err != nil {
return nil, err
}
}
if ct.apiChoice == CoinMarketCap {
ct.api = api.NewCMC(ct.apiKeys.cmc)
} else if ct.apiChoice == CoinGecko {
ct.api = api.NewCG(&api.CoinGeckoConfig{
PerPage: perPage,
MaxPages: maxPages,
ApiKey: ct.apiKeys.coingecko,
ProApiKey: ct.apiKeys.coingeckoPro,
})
ct.api = api.NewCG(perPage, maxPages)
} else {
return nil, ErrInvalidAPIChoice
}
@ -497,7 +397,7 @@ func NewCointop(config *Config) (*Cointop, error) {
ct.filecache.Get(coinscachekey, &allCoinsSlugMap)
}
// fix for https://github.com/cointop-sh/cointop/issues/59
// fix for https://github.com/miguelmota/cointop/issues/59
// can remove this after everyone has cleared their cache
for _, v := range allCoinsSlugMap {
// Some APIs returns rank 0 for new coins
@ -524,12 +424,27 @@ func NewCointop(config *Config) (*Cointop, error) {
if max > 100 {
max = 100
}
ct.Sort(ct.State.viewSorts[ct.State.selectedView], ct.State.allCoins, false)
ct.Sort(ct.State.sortBy, ct.State.sortDesc, ct.State.allCoins, false)
ct.State.coins = ct.State.allCoins[0:max]
}
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
// Here we're doing a lookup based on symbol and setting the favorite to the coin name instead of coin symbol.
ct.State.allCoinsSlugMap.Range(func(key, value interface{}) bool {
if coin, ok := value.(*Coin); ok {
for k := range ct.State.favoritesBySymbol {
if coin.Symbol == k {
ct.State.favorites[coin.Name] = true
delete(ct.State.favoritesBySymbol, k)
}
}
}
return true
})
var globaldata []float64
chartcachekey := ct.CompositeCacheKey("globaldata", "", "", ct.State.selectedChartRange)
chartcachekey := ct.CacheKey(fmt.Sprintf("%s_%s", "globaldata", strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
if ct.filecache != nil {
ct.filecache.Get(chartcachekey, &globaldata)
}
@ -553,22 +468,23 @@ func NewCointop(config *Config) (*Cointop, error) {
// Run runs cointop
func (ct *Cointop) Run() error {
log.Debug("Run()")
ct.debuglog("run()")
ui, err := ui.NewUI()
if err != nil {
return err
}
ui.SetStyle(ct.colorscheme.BaseStyle())
ui.SetFgColor(ct.colorscheme.BaseFg())
ui.SetBgColor(ct.colorscheme.BaseBg())
ct.ui = ui
ct.g = ui.GetGocui()
defer ui.Close()
ui.SetInputEsc(true)
ui.SetMouse(ct.State.enableMouse)
ui.SetMouse(true)
ui.SetHighlight(true)
ui.SetManagerFunc(ct.layout)
if err := ct.SetKeybindings(); err != nil {
if err := ct.Keybindings(ct.g); err != nil {
return fmt.Errorf("keybindings: %v", err)
}
@ -593,19 +509,18 @@ type CleanConfig struct {
}
// Clean removes cache files
func (ct *Cointop) Clean(config *CleanConfig) error {
func Clean(config *CleanConfig) error {
if config == nil {
config = &CleanConfig{}
}
cacheCleaned := false
cacheDir := DefaultCacheDir
if config.CacheDir != "" {
cacheDir = pathutil.NormalizePath(config.CacheDir)
} else if ct.State.cacheDir != "" {
cacheDir = ct.State.cacheDir
}
cacheCleaned := false
if _, err := os.Stat(cacheDir); !os.IsNotExist(err) {
files, err := ioutil.ReadDir(cacheDir)
if err != nil {
@ -643,12 +558,12 @@ type ResetConfig struct {
}
// Reset removes configuration and cache files
func (ct *Cointop) Reset(config *ResetConfig) error {
func Reset(config *ResetConfig) error {
if config == nil {
config = &ResetConfig{}
}
if err := ct.Clean(&CleanConfig{
if err := Clean(&CleanConfig{
CacheDir: config.CacheDir,
Log: config.Log,
}); err != nil {
@ -657,7 +572,7 @@ func (ct *Cointop) Reset(config *ResetConfig) error {
configDeleted := false
for _, configPath := range PossibleConfigPaths {
for _, configPath := range possibleConfigPaths {
normalizedPath := pathutil.NormalizePath(configPath)
if _, err := os.Stat(normalizedPath); !os.IsNotExist(err) {
if config.Log {

@ -2,32 +2,33 @@ package cointop
import (
"fmt"
"strings"
"strconv"
"sync"
fcolor "github.com/fatih/color"
"github.com/gdamore/tcell/v2"
gocui "github.com/miguelmota/gocui"
xtermcolor "github.com/tomnomnom/xtermcolor"
)
// TODO: fix hex color support
// ColorschemeColors is a map of color string names to Attribute types
type ColorschemeColors map[string]interface{}
// colorschemeColors is a map of color string names to Attribute types
type colorschemeColors map[string]interface{}
// ISprintf is a sprintf interface
type ISprintf func(...interface{}) string
// ColorCache is a map of color string names to sprintf functions
type ColorCache map[string]ISprintf
// colorCache is a map of color string names to sprintf functions
type colorCache map[string]ISprintf
// Colorscheme is the struct for colorscheme
type Colorscheme struct {
colors ColorschemeColors
cache ColorCache
colors colorschemeColors
cache colorCache
cacheMutex sync.RWMutex
}
var FgColorschemeColorsMap = map[string]fcolor.Attribute{
var fgcolorschemeColorsMap = map[string]fcolor.Attribute{
"black": fcolor.FgBlack,
"blue": fcolor.FgBlue,
"cyan": fcolor.FgCyan,
@ -38,7 +39,7 @@ var FgColorschemeColorsMap = map[string]fcolor.Attribute{
"yellow": fcolor.FgYellow,
}
var BgColorschemeColorsMap = map[string]fcolor.Attribute{
var bgcolorschemeColorsMap = map[string]fcolor.Attribute{
"black": fcolor.BgBlack,
"blue": fcolor.BgBlue,
"cyan": fcolor.BgCyan,
@ -49,202 +50,187 @@ var BgColorschemeColorsMap = map[string]fcolor.Attribute{
"yellow": fcolor.BgYellow,
}
// See more: vendor/github.com/mattn/go-colorable/colorable_windows.go:905
// any new color for the below mapping should be compatible with this above list
// TcellColorschemeColorsMap map colorscheme names to tcell colors
var TcellColorschemeColorsMap = map[string]tcell.Color{
"black": tcell.ColorBlack,
"blue": tcell.ColorNavy,
"cyan": tcell.ColorTeal,
"green": tcell.ColorGreen,
"magenta": tcell.ColorPurple,
"red": tcell.ColorMaroon,
"white": tcell.ColorSilver,
"yellow": tcell.ColorOlive,
var gocuiColorschemeColorsMap = map[string]gocui.Attribute{
"black": gocui.ColorBlack,
"blue": gocui.ColorBlue,
"cyan": gocui.ColorCyan,
"green": gocui.ColorGreen,
"magenta": gocui.ColorMagenta,
"red": gocui.ColorRed,
"white": gocui.ColorWhite,
"yellow": gocui.ColorYellow,
}
// NewColorscheme ...
func NewColorscheme(colors ColorschemeColors) *Colorscheme {
// Build lookup table for defined values, then replace references to these
const prefix = "define_"
const reference = "$"
defines := ColorschemeColors{}
for k, v := range colors {
if strings.HasPrefix(k, prefix) {
defines[k[len(prefix):]] = v
}
}
for k, v := range colors {
if vs, ok := v.(string); ok {
if strings.HasPrefix(vs, reference) {
colors[k] = defines[vs[len(reference):]]
}
}
}
func NewColorscheme(colors colorschemeColors) *Colorscheme {
return &Colorscheme{
colors: colors,
cache: make(ColorCache),
cache: make(colorCache),
cacheMutex: sync.RWMutex{},
}
}
func (c *Colorscheme) BaseStyle() tcell.Style {
return c.Style("base")
// BaseFg ...
func (c *Colorscheme) BaseFg() gocui.Attribute {
return c.gocuiFgColor("base")
}
// BaseBg ...
func (c *Colorscheme) BaseBg() gocui.Attribute {
return c.gocuiBgColor("base")
}
// Chart ...
func (c *Colorscheme) Chart(a ...interface{}) string {
return c.Color("chart", a...)
return c.color("chart", a...)
}
// Marketbar ...
func (c *Colorscheme) Marketbar(a ...interface{}) string {
return c.Color("marketbar", a...)
return c.color("marketbar", a...)
}
// MarketbarSprintf ...
func (c *Colorscheme) MarketbarSprintf() ISprintf {
return c.ToSprintf("marketbar")
return c.toSprintf("marketbar")
}
// MarketbarChangeSprintf ...
func (c *Colorscheme) MarketbarChangeSprintf() ISprintf {
// NOTE: reusing table styles
return c.ToSprintf("table_column_change")
return c.toSprintf("table_column_change")
}
// MarketbarChangeDownSprintf ...
func (c *Colorscheme) MarketbarChangeDownSprintf() ISprintf {
// NOTE: reusing table styles
return c.ToSprintf("table_column_change_down")
return c.toSprintf("table_column_change_down")
}
// MarketbarChangeUpSprintf ...
func (c *Colorscheme) MarketbarChangeUpSprintf() ISprintf {
// NOTE: reusing table styles
return c.ToSprintf("table_column_change_up")
return c.toSprintf("table_column_change_up")
}
// MarketBarLabelActive ...
func (c *Colorscheme) MarketBarLabelActive(a ...interface{}) string {
return c.Color("marketbar_label_active", a...)
return c.color("marketbar_label_active", a...)
}
// Menu ...
func (c *Colorscheme) Menu(a ...interface{}) string {
return c.Color("menu", a...)
return c.color("menu", a...)
}
// MenuHeader ...
func (c *Colorscheme) MenuHeader(a ...interface{}) string {
return c.Color("menu_header", a...)
return c.color("menu_header", a...)
}
// MenuLabel ...
func (c *Colorscheme) MenuLabel(a ...interface{}) string {
return c.Color("menu_label", a...)
return c.color("menu_label", a...)
}
// MenuLabelActive ...
func (c *Colorscheme) MenuLabelActive(a ...interface{}) string {
return c.Color("menu_label_active", a...)
return c.color("menu_label_active", a...)
}
// Searchbar ...
func (c *Colorscheme) Searchbar(a ...interface{}) string {
return c.Color("searchbar", a...)
return c.color("searchbar", a...)
}
// Statusbar ...
func (c *Colorscheme) Statusbar(a ...interface{}) string {
return c.Color("statusbar", a...)
return c.color("statusbar", a...)
}
// TableColumnPrice ...
func (c *Colorscheme) TableColumnPrice(a ...interface{}) string {
return c.Color("table_column_price", a...)
return c.color("table_column_price", a...)
}
// TableColumnPriceSprintf ...
func (c *Colorscheme) TableColumnPriceSprintf() ISprintf {
return c.ToSprintf("table_column_price")
return c.toSprintf("table_column_price")
}
// TableColumnChange ...
func (c *Colorscheme) TableColumnChange(a ...interface{}) string {
return c.Color("table_column_change", a...)
return c.color("table_column_change", a...)
}
// TableColumnChangeSprintf ...
func (c *Colorscheme) TableColumnChangeSprintf() ISprintf {
return c.ToSprintf("table_column_change")
return c.toSprintf("table_column_change")
}
// TableColumnChangeDown ...
func (c *Colorscheme) TableColumnChangeDown(a ...interface{}) string {
return c.Color("table_column_change_down", a...)
return c.color("table_column_change_down", a...)
}
// TableColumnChangeDownSprintf ...
func (c *Colorscheme) TableColumnChangeDownSprintf() ISprintf {
return c.ToSprintf("table_column_change_down")
return c.toSprintf("table_column_change_down")
}
// TableColumnChangeUp ...
func (c *Colorscheme) TableColumnChangeUp(a ...interface{}) string {
return c.Color("table_column_change_up", a...)
return c.color("table_column_change_up", a...)
}
// TableColumnChangeUpSprintf ...
func (c *Colorscheme) TableColumnChangeUpSprintf() ISprintf {
return c.ToSprintf("table_column_change_up")
return c.toSprintf("table_column_change_up")
}
// TableHeader ...
func (c *Colorscheme) TableHeader(a ...interface{}) string {
return c.Color("table_header", a...)
return c.color("table_header", a...)
}
// TableHeaderSprintf ...
func (c *Colorscheme) TableHeaderSprintf() ISprintf {
return c.ToSprintf("table_header")
return c.toSprintf("table_header")
}
// TableHeaderColumnActive ...
func (c *Colorscheme) TableHeaderColumnActive(a ...interface{}) string {
return c.Color("table_header_column_active", a...)
return c.color("table_header_column_active", a...)
}
// TableHeaderColumnActiveSprintf ...
func (c *Colorscheme) TableHeaderColumnActiveSprintf() ISprintf {
return c.ToSprintf("table_header_column_active")
return c.toSprintf("table_header_column_active")
}
// TableRow ...
func (c *Colorscheme) TableRow(a ...interface{}) string {
return c.Color("table_row", a...)
return c.color("table_row", a...)
}
// TableRowSprintf ...
func (c *Colorscheme) TableRowSprintf() ISprintf {
return c.ToSprintf("table_row")
return c.toSprintf("table_row")
}
// TableRowActive ...
func (c *Colorscheme) TableRowActive(a ...interface{}) string {
return c.Color("table_row_active", a...)
return c.color("table_row_active", a...)
}
// TableRowFavorite ...
func (c *Colorscheme) TableRowFavorite(a ...interface{}) string {
return c.Color("table_row_favorite", a...)
return c.color("table_row_favorite", a...)
}
// TableRowFavoriteSprintf ...
func (c *Colorscheme) TableRowFavoriteSprintf() ISprintf {
return c.ToSprintf("table_row_favorite")
return c.toSprintf("table_row_favorite")
}
// Default ...
@ -252,56 +238,31 @@ func (c *Colorscheme) Default(a ...interface{}) string {
return fmt.Sprintf(a[0].(string), a[1:]...)
}
func (c *Colorscheme) ToSprintf(name string) ISprintf {
func (c *Colorscheme) toSprintf(name string) ISprintf {
c.cacheMutex.Lock()
defer c.cacheMutex.Unlock()
if cached, ok := c.cache[name]; ok {
return cached
}
// TODO: use c.Style(name)?
var attrs []fcolor.Attribute
if v, ok := c.colors[name+"_fg"].(string); ok {
if fg, ok := c.ToFgAttr(v); ok {
if fg, ok := c.toFgAttr(v); ok {
attrs = append(attrs, fg)
} else {
color := tcell.GetColor(v)
if color != tcell.ColorDefault {
// 24-bit foreground 38;2;⟨r⟩;⟨g⟩;⟨b⟩
r, g, b := color.RGB()
attrs = append(attrs, 38)
attrs = append(attrs, 2)
attrs = append(attrs, fcolor.Attribute(r))
attrs = append(attrs, fcolor.Attribute(g))
attrs = append(attrs, fcolor.Attribute(b))
}
}
}
if v, ok := c.colors[name+"_bg"].(string); ok {
if bg, ok := c.ToBgAttr(v); ok {
if bg, ok := c.toBgAttr(v); ok {
attrs = append(attrs, bg)
} else {
color := tcell.GetColor(v)
if color != tcell.ColorDefault {
// 24-bit background 48;2;⟨r⟩;⟨g⟩;⟨b⟩
r, g, b := color.RGB()
attrs = append(attrs, 48)
attrs = append(attrs, 2)
attrs = append(attrs, fcolor.Attribute(r))
attrs = append(attrs, fcolor.Attribute(g))
attrs = append(attrs, fcolor.Attribute(b))
}
}
}
if v, ok := c.colors[name+"_bold"].(bool); ok {
if bold, ok := c.ToBoldAttr(v); ok {
if bold, ok := c.toBoldAttr(v); ok {
attrs = append(attrs, bold)
}
}
if v, ok := c.colors[name+"_underline"].(bool); ok {
if underline, ok := c.ToUnderlineAttr(v); ok {
if underline, ok := c.toUnderlineAttr(v); ok {
attrs = append(attrs, underline)
}
}
@ -310,80 +271,96 @@ func (c *Colorscheme) ToSprintf(name string) ISprintf {
return c.cache[name]
}
func (c *Colorscheme) Color(name string, a ...interface{}) string {
return c.ToSprintf(name)(a...)
func (c *Colorscheme) color(name string, a ...interface{}) string {
return c.toSprintf(name)(a...)
}
func (c *Colorscheme) Style(name string) tcell.Style {
st := tcell.StyleDefault
st = st.Foreground(c.tcellColor(name + "_fg"))
st = st.Background(c.tcellColor(name + "_bg"))
if v, ok := c.colors[name+"_bold"].(bool); ok {
st = st.Bold(v)
}
if v, ok := c.colors[name+"_underline"].(bool); ok {
st = st.Underline(v)
func (c *Colorscheme) gocuiFgColor(name string) gocui.Attribute {
if v, ok := c.colors[name+"_fg"].(string); ok {
if fg, ok := c.toGocuiAttr(v); ok {
return fg
}
}
// TODO: Blink Dim Italic Reverse Strikethrough
return st
return gocui.ColorDefault
}
// tcellColor can supply for types of color name: specific mapped name, tcell color name, hex
// Examples: black, honeydew, #000000
func (c *Colorscheme) tcellColor(name string) tcell.Color {
v, ok := c.colors[name].(string)
if !ok {
return tcell.ColorDefault
func (c *Colorscheme) gocuiBgColor(name string) gocui.Attribute {
if v, ok := c.colors[name+"_bg"].(string); ok {
if bg, ok := c.toGocuiAttr(v); ok {
return bg
}
}
if color, found := TcellColorschemeColorsMap[v]; found {
return color
return gocui.ColorDefault
}
func (c *Colorscheme) toFgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := fgcolorschemeColorsMap[v]; ok {
return attr, true
}
color := tcell.GetColor(v)
if color != tcell.ColorDefault {
return color
if code, ok := HexToAnsi(v); ok {
return fcolor.Attribute(code), true
}
// find closest X11 color to RGB
// if code, ok := HexToAnsi(v); ok {
// return tcell.PaletteColor(int(code) & 0xff)
// }
return color
return 0, false
}
func (c *Colorscheme) ToFgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := FgColorschemeColorsMap[v]; ok {
func (c *Colorscheme) toBgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := bgcolorschemeColorsMap[v]; ok {
return attr, true
}
// find closest X11 color to RGB
// if code, ok := HexToAnsi(v); ok {
// return fcolor.Attribute(code), true
// }
if code, ok := HexToAnsi(v); ok {
return fcolor.Attribute(code), true
}
return 0, false
}
func (c *Colorscheme) ToBgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := BgColorschemeColorsMap[v]; ok {
// toBoldAttr converts a boolean to an Attribute type
func (c *Colorscheme) toBoldAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Bold, v
}
// toUnderlineAttr converts a boolean to an Attribute type
func (c *Colorscheme) toUnderlineAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Underline, v
}
// toGocuiAttr converts a color string name to a gocui Attribute type
func (c *Colorscheme) toGocuiAttr(v string) (gocui.Attribute, bool) {
if attr, ok := gocuiColorschemeColorsMap[v]; ok {
return attr, true
}
// find closest X11 color to RGB
// if code, ok := HexToAnsi(v); ok {
// return fcolor.Attribute(code), true
// }
if code, ok := HexToAnsi(v); ok {
return gocui.Attribute(code), true
}
return 0, false
}
// ToBoldAttr converts a boolean to an Attribute type
func (c *Colorscheme) ToBoldAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Bold, v
}
// HexToAnsi converts a hex color string to a uint8 ansi code
func HexToAnsi(h string) (uint8, bool) {
if h == "" {
return 0, false
}
// ToUnderlineAttr converts a boolean to an Attribute type
func (c *Colorscheme) ToUnderlineAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Underline, v
n, err := strconv.Atoi(h)
if err == nil {
if n <= 255 {
return uint8(n), true
}
}
code, err := xtermcolor.FromHexStr(h)
if err != nil {
return 0, false
}
return code, true
}
// gocui can use xterm colors

@ -11,22 +11,18 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/cointop-sh/cointop/pkg/pathutil"
"github.com/cointop-sh/cointop/pkg/toml"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/pathutil"
"github.com/miguelmota/cointop/pkg/toml"
)
// FilePerm is the default file permissions
var FilePerm = os.FileMode(0o644)
var fileperm = os.FileMode(0644)
// ErrInvalidPriceAlert is error for invalid price alert value
var ErrInvalidPriceAlert = errors.New("invalid price alert value")
// PossibleConfigPaths are the possible config file paths.
// NOTE: this is to support previous default config filepaths
var PossibleConfigPaths = []string{
var possibleConfigPaths = []string{
":PREFERRED_CONFIG_HOME:/cointop/config.toml",
":HOME:/.config/cointop/config.toml",
":HOME:/.config/cointop/config",
@ -34,58 +30,65 @@ var PossibleConfigPaths = []string{
":HOME:/.cointop/config.toml",
}
// ConfigFileConfig is the config file structure
type ConfigFileConfig struct {
Shortcuts map[string]interface{} `toml:"shortcuts"`
Favorites map[string]interface{} `toml:"favorites"`
Portfolio map[string]interface{} `toml:"portfolio"`
PriceAlerts map[string]interface{} `toml:"price_alerts"`
Currency interface{} `toml:"currency"`
DefaultView interface{} `toml:"default_view"`
DefaultChartRange interface{} `toml:"default_chart_range"`
CoinMarketCap map[string]interface{} `toml:"coinmarketcap"`
CoinGecko map[string]interface{} `toml:"coingecko"`
API interface{} `toml:"api"`
Colorscheme interface{} `toml:"colorscheme"`
RefreshRate interface{} `toml:"refresh_rate"`
CoinStructHash interface{} `toml:"coin_struct_version"`
CacheDir interface{} `toml:"cache_dir"`
CompactNotation interface{} `toml:"compact_notation"`
EnableMouse interface{} `toml:"enable_mouse"`
AltCoinLink interface{} `toml:"alt_coin_link"` // TODO: should really be in API-specific section
Table map[string]interface{} `toml:"table"`
Chart map[string]interface{} `toml:"chart"`
type config struct {
Shortcuts map[string]interface{} `toml:"shortcuts"`
Favorites map[string]interface{} `toml:"favorites"`
Portfolio map[string]interface{} `toml:"portfolio"`
PriceAlerts map[string]interface{} `toml:"price_alerts"`
Currency interface{} `toml:"currency"`
DefaultView interface{} `toml:"default_view"`
CoinMarketCap map[string]interface{} `toml:"coinmarketcap"`
API interface{} `toml:"api"`
Colorscheme interface{} `toml:"colorscheme"`
RefreshRate interface{} `toml:"refresh_rate"`
CacheDir interface{} `toml:"cache_dir"`
Table map[string]interface{} `toml:"table"`
}
// SetupConfig loads config file
func (ct *Cointop) SetupConfig() error {
type loadConfigFunc func() error
loaders := []loadConfigFunc{
ct.CreateConfigIfNotExists,
ct.ParseConfig,
ct.loadTableConfig,
ct.loadChartConfig,
ct.loadShortcutsFromConfig,
ct.loadFavoritesFromConfig,
ct.loadCurrencyFromConfig,
ct.loadDefaultViewFromConfig,
ct.loadDefaultChartRangeFromConfig,
ct.loadAPIKeysFromConfig,
ct.loadAPIChoiceFromConfig,
ct.loadColorschemeFromConfig,
ct.loadRefreshRateFromConfig,
ct.loadCacheDirFromConfig,
ct.loadCompactNotationFromConfig,
ct.loadEnableMouseFromConfig,
ct.loadAltCoinLinkFromConfig,
ct.loadPriceAlertsFromConfig,
ct.loadPortfolioFromConfig,
}
for _, f := range loaders {
if err := f(); err != nil {
return err
}
ct.debuglog("setupConfig()")
if err := ct.CreateConfigIfNotExists(); err != nil {
return err
}
if err := ct.parseConfig(); err != nil {
return err
}
if err := ct.loadTableConfig(); err != nil {
return err
}
if err := ct.loadShortcutsFromConfig(); err != nil {
return err
}
if err := ct.loadFavoritesFromConfig(); err != nil {
return err
}
if err := ct.loadCurrencyFromConfig(); err != nil {
return err
}
if err := ct.loadDefaultViewFromConfig(); err != nil {
return err
}
if err := ct.loadAPIKeysFromConfig(); err != nil {
return err
}
if err := ct.loadAPIChoiceFromConfig(); err != nil {
return err
}
if err := ct.loadColorschemeFromConfig(); err != nil {
return err
}
if err := ct.loadRefreshRateFromConfig(); err != nil {
return err
}
if err := ct.loadCacheDirFromConfig(); err != nil {
return err
}
if err := ct.loadPriceAlertsFromConfig(); err != nil {
return err
}
if err := ct.loadPortfolioFromConfig(); err != nil {
return err
}
return nil
@ -93,13 +96,13 @@ func (ct *Cointop) SetupConfig() error {
// CreateConfigIfNotExists creates config file if it doesn't exist
func (ct *Cointop) CreateConfigIfNotExists() error {
log.Debug("CreateConfigIfNotExists()")
ct.debuglog("createConfigIfNotExists()")
ct.configFilepath = pathutil.NormalizePath(ct.configFilepath)
// check if config file exists in one of th default paths
if ct.configFilepath == DefaultConfigFilepath {
for _, configPath := range PossibleConfigPaths {
for _, configPath := range possibleConfigPaths {
normalizedPath := pathutil.NormalizePath(configPath)
if _, err := os.Stat(normalizedPath); err == nil {
ct.configFilepath = normalizedPath
@ -108,12 +111,12 @@ func (ct *Cointop) CreateConfigIfNotExists() error {
}
}
err := ct.MakeConfigDir()
err := ct.makeConfigDir()
if err != nil {
return err
}
err = ct.MakeConfigFile()
err = ct.makeConfigFile()
if err != nil {
return err
}
@ -123,7 +126,7 @@ func (ct *Cointop) CreateConfigIfNotExists() error {
// ConfigDirPath returns the config directory path
func (ct *Cointop) ConfigDirPath() string {
log.Debug("ConfigDirPath()")
ct.debuglog("configDirPath()")
path := pathutil.NormalizePath(ct.configFilepath)
separator := string(filepath.Separator)
parts := strings.Split(path, separator)
@ -132,13 +135,13 @@ func (ct *Cointop) ConfigDirPath() string {
// ConfigFilePath return the config file path
func (ct *Cointop) ConfigFilePath() string {
log.Debug("ConfigFilePath()")
ct.debuglog("configFilePath()")
return pathutil.NormalizePath(ct.configFilepath)
}
// MakeConfigDir creates the directory for the config file
func (ct *Cointop) MakeConfigDir() error {
log.Debug("MakeConfigDir()")
// ConfigPath return the config file path
func (ct *Cointop) makeConfigDir() error {
ct.debuglog("makeConfigDir()")
path := ct.ConfigDirPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, os.ModePerm)
@ -148,8 +151,8 @@ func (ct *Cointop) MakeConfigDir() error {
}
// MakeConfigFile creates a new config file
func (ct *Cointop) MakeConfigFile() error {
log.Debug("MakeConfigFile()")
func (ct *Cointop) makeConfigFile() error {
ct.debuglog("makeConfigFile()")
path := ct.ConfigFilePath()
if _, err := os.Stat(path); os.IsNotExist(err) {
fo, err := os.Create(path)
@ -157,7 +160,7 @@ func (ct *Cointop) MakeConfigFile() error {
return err
}
defer fo.Close()
b, err := ct.ConfigToToml()
b, err := ct.configToToml()
if err != nil {
return err
}
@ -170,16 +173,16 @@ func (ct *Cointop) MakeConfigFile() error {
// SaveConfig writes settings to the config file
func (ct *Cointop) SaveConfig() error {
log.Debug("SaveConfig()")
ct.debuglog("saveConfig()")
ct.saveMux.Lock()
defer ct.saveMux.Unlock()
path := ct.ConfigFilePath()
if _, err := os.Stat(path); err == nil {
b, err := ct.ConfigToToml()
b, err := ct.configToToml()
if err != nil {
return err
}
err = ioutil.WriteFile(path, b, FilePerm)
err = ioutil.WriteFile(path, b, fileperm)
if err != nil {
return err
}
@ -188,9 +191,9 @@ func (ct *Cointop) SaveConfig() error {
}
// ParseConfig decodes the toml config file
func (ct *Cointop) ParseConfig() error {
log.Debug("ParseConfig()")
var conf ConfigFileConfig
func (ct *Cointop) parseConfig() error {
ct.debuglog("parseConfig()")
var conf config
path := ct.configFilepath
if _, err := toml.DecodeFile(path, &conf); err != nil {
return err
@ -201,8 +204,8 @@ func (ct *Cointop) ParseConfig() error {
}
// ConfigToToml encodes config struct to TOML
func (ct *Cointop) ConfigToToml() ([]byte, error) {
log.Debug("ConfigToToml()")
func (ct *Cointop) configToToml() ([]byte, error) {
ct.debuglog("configToToml()")
shortcutsIfcs := map[string]interface{}{}
for k, v := range ct.State.shortcutKeys {
var i interface{} = v
@ -220,44 +223,47 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
return favoritesIfc[i].(string) < favoritesIfc[j].(string)
})
var favoritesBySymbolIfc []interface{}
favoritesMapIfc := map[string]interface{}{
"names": favoritesIfc,
"columns": ct.State.favoritesTableColumns,
"character": ct.State.favoriteChar,
"compact_notation": ct.State.favoritesCompactNotation,
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
"symbols": favoritesBySymbolIfc,
"names": favoritesIfc,
}
var favoritesColumnsIfc interface{} = ct.State.favoritesTableColumns
favoritesMapIfc["columns"] = favoritesColumnsIfc
portfolioIfc := map[string]interface{}{}
var holdingsIfc [][]string
for name := range ct.State.portfolio.Entries {
entry, ok := ct.State.portfolio.Entries[name]
if !ok || entry.Coin == "" {
continue
}
tuple := []string{
entry.Coin,
strconv.FormatFloat(entry.Holdings, 'f', -1, 64),
strconv.FormatFloat(entry.BuyPrice, 'f', -1, 64),
entry.BuyCurrency,
}
var amount string = strconv.FormatFloat(entry.Holdings, 'f', -1, 64)
var coinName string = entry.Coin
var tuple []string = []string{coinName, amount}
holdingsIfc = append(holdingsIfc, tuple)
}
sort.Slice(holdingsIfc, func(i, j int) bool {
return holdingsIfc[i][0] < holdingsIfc[j][0]
})
portfolioIfc := map[string]interface{}{
"holdings": holdingsIfc,
"columns": ct.State.portfolioTableColumns,
"compact_notation": ct.State.portfolioCompactNotation,
}
portfolioIfc["holdings"] = holdingsIfc
var columnsIfc interface{} = ct.State.portfolioTableColumns
portfolioIfc["columns"] = columnsIfc
var currencyIfc interface{} = ct.State.currencyConversion
var defaultViewIfc interface{} = ct.State.defaultView
var colorschemeIfc interface{} = ct.colorschemeName
var refreshRateIfc interface{} = uint(ct.State.refreshRate.Seconds())
var cacheDirIfc interface{} = ct.State.cacheDir
cmcIfc := map[string]interface{}{
"pro_api_key": ct.apiKeys.cmc,
}
coingeckoIfc := map[string]interface{}{
"api_key": ct.apiKeys.coingecko,
"pro_api_key": ct.apiKeys.coingeckoPro,
}
var apiChoiceIfc interface{} = ct.apiChoice
var priceAlertsIfc []interface{}
for _, priceAlert := range ct.State.priceAlerts.Entries {
@ -276,39 +282,25 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
//"sound": ct.State.priceAlerts.SoundEnabled,
}
tableMapIfc := map[string]interface{}{
"columns": ct.State.coinsTableColumns,
"keep_row_focus_on_sort": ct.State.keepRowFocusOnSort,
"compact_notation": ct.State.tableCompactNotation,
}
var coinsTableColumnsIfc interface{} = ct.State.coinsTableColumns
tableMapIfc := map[string]interface{}{}
tableMapIfc["columns"] = coinsTableColumnsIfc
var keepRowFocusOnSortIfc interface{} = ct.State.keepRowFocusOnSort
tableMapIfc["keep_row_focus_on_sort"] = keepRowFocusOnSortIfc
chartMapIfc := map[string]interface{}{
"max_width": ct.State.maxChartWidth,
"height": ct.State.chartHeight,
}
currentCoinHash, _ := getStructHash(Coin{})
inputs := &ConfigFileConfig{
API: ct.apiChoice,
Colorscheme: ct.colorschemeName,
CoinMarketCap: cmcIfc,
CoinGecko: coingeckoIfc,
Currency: ct.State.currencyConversion,
DefaultView: ct.State.defaultView,
DefaultChartRange: ct.State.defaultChartRange,
Favorites: favoritesMapIfc,
RefreshRate: uint(ct.State.refreshRate.Seconds()),
Shortcuts: shortcutsIfcs,
Portfolio: portfolioIfc,
PriceAlerts: priceAlertsMapIfc,
CacheDir: ct.State.cacheDir,
Table: tableMapIfc,
Chart: chartMapIfc,
CoinStructHash: currentCoinHash,
CompactNotation: ct.State.compactNotation,
EnableMouse: ct.State.enableMouse,
AltCoinLink: ct.State.altCoinLink,
var inputs = &config{
API: apiChoiceIfc,
Colorscheme: colorschemeIfc,
CoinMarketCap: cmcIfc,
Currency: currencyIfc,
DefaultView: defaultViewIfc,
Favorites: favoritesMapIfc,
RefreshRate: refreshRateIfc,
Shortcuts: shortcutsIfcs,
Portfolio: portfolioIfc,
PriceAlerts: priceAlertsMapIfc,
CacheDir: cacheDirIfc,
Table: tableMapIfc,
}
var b bytes.Buffer
@ -323,7 +315,6 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
// LoadTableConfig loads table config from toml config into state struct
func (ct *Cointop) loadTableConfig() error {
log.Debug("loadTableConfig()")
err := ct.loadTableColumnsFromConfig()
if err != nil {
return err
@ -333,33 +324,12 @@ func (ct *Cointop) loadTableConfig() error {
if ok {
ct.State.keepRowFocusOnSort = keepRowFocusOnSortIfc.(bool)
}
if compactNotation, ok := ct.config.Table["compact_notation"]; ok {
ct.State.tableCompactNotation = compactNotation.(bool)
}
return nil
}
// LoadChartConfig loads chart config from toml config into state struct
func (ct *Cointop) loadChartConfig() error {
log.Debugf("loadChartConfig()")
maxChartWidthIfc, ok := ct.config.Chart["max_width"]
if ok {
ct.State.maxChartWidth = int(maxChartWidthIfc.(int64))
}
chartHeightIfc, ok := ct.config.Chart["height"]
if ok {
ct.State.chartHeight = int(chartHeightIfc.(int64))
ct.State.lastChartHeight = ct.State.chartHeight
}
return nil
}
// LoadTableColumnsFromConfig loads preferred coins table columns from config file to struct
func (ct *Cointop) loadTableColumnsFromConfig() error {
log.Debug("loadTableColumnsFromConfig()")
ct.debuglog("loadTableColumnsFromConfig()")
columnsIfc, ok := ct.config.Table["columns"]
if !ok {
return nil
@ -385,51 +355,21 @@ func (ct *Cointop) loadTableColumnsFromConfig() error {
// LoadShortcutsFromConfig loads keyboard shortcuts from config file to struct
func (ct *Cointop) loadShortcutsFromConfig() error {
log.Debug("loadShortcutsFromConfig()")
// Load the shortcut config into a key:action map (filtering to actions that exist). Keep track of actions.
config := make(map[string]string)
actions := make(map[string]bool)
ct.debuglog("loadShortcutsFromConfig()")
for k, ifc := range ct.config.Shortcuts {
if v, ok := ifc.(string); ok {
if !ct.ActionExists(v) {
log.Debugf("Shortcut '%s'=>%s is not a valid action", k, v)
continue
}
config[k] = v
actions[v] = true
}
}
// Count how many keys are configured per action.
actionCount := make(map[string]int)
for _, action := range ct.State.shortcutKeys {
actionCount[action] += 1
}
// merge defaults into the loaded config - if the key is not defined, and the action is not found, add it
for key, action := range ct.State.shortcutKeys {
if _, ok := config[key]; ok {
// k is already in the config - ignore it
} else if _, ok := actions[action]; ok {
if actionCount[action] == 1 {
// action is already in the config - ignore it
} else {
// there are multiple bindings, add them anyway
config[key] = action // add action
}
} else {
config[key] = action // add action
ct.State.shortcutKeys[k] = v
}
}
ct.State.shortcutKeys = config
return nil
}
// LoadCurrencyFromConfig loads currency from config file to struct
func (ct *Cointop) loadCurrencyFromConfig() error {
log.Debug("loadCurrencyFromConfig()")
ct.debuglog("loadCurrencyFromConfig()")
if currency, ok := ct.config.Currency.(string); ok {
ct.State.currencyConversion = strings.ToUpper(currency)
}
@ -438,7 +378,7 @@ func (ct *Cointop) loadCurrencyFromConfig() error {
// LoadDefaultViewFromConfig loads default view from config file to struct
func (ct *Cointop) loadDefaultViewFromConfig() error {
log.Debug("loadDefaultViewFromConfig()")
ct.debuglog("loadDefaultViewFromConfig()")
if defaultView, ok := ct.config.DefaultView.(string); ok {
defaultView = strings.ToLower(defaultView)
switch defaultView {
@ -459,45 +399,21 @@ func (ct *Cointop) loadDefaultViewFromConfig() error {
return nil
}
// LoadDefaultChartRangeFromConfig loads default chart range from config file to struct
func (ct *Cointop) loadDefaultChartRangeFromConfig() error {
log.Debug("loadDefaultChartRangeFromConfig()")
if defaultChartRange, ok := ct.config.DefaultChartRange.(string); ok {
// validate configured value
_, ok := ct.chartRangesMap[defaultChartRange]
if !ok {
return fmt.Errorf("invalid default chart range %q. Valid ranges are: %s", defaultChartRange, strings.Join(ChartRanges(), ","))
}
ct.State.defaultChartRange = defaultChartRange
ct.State.selectedChartRange = defaultChartRange
}
return nil
}
// LoadAPIKeysFromConfig loads API keys from config file to struct
func (ct *Cointop) loadAPIKeysFromConfig() error {
log.Debug("loadAPIKeysFromConfig()")
ct.debuglog("loadAPIKeysFromConfig()")
for key, value := range ct.config.CoinMarketCap {
k := strings.TrimSpace(strings.ToLower(key))
if k == "pro_api_key" {
ct.apiKeys.cmc = value.(string)
}
}
for key, value := range ct.config.CoinGecko {
k := strings.TrimSpace(strings.ToLower(key))
if k == "api_key" {
ct.apiKeys.coingecko = value.(string)
}
if k == "pro_api_key" {
ct.apiKeys.coingeckoPro = value.(string)
}
}
return nil
}
// LoadColorschemeFromConfig loads colorscheme name from config file to struct
func (ct *Cointop) loadColorschemeFromConfig() error {
log.Debug("loadColorschemeFromConfig()")
ct.debuglog("loadColorschemeFromConfig()")
if colorscheme, ok := ct.config.Colorscheme.(string); ok {
ct.colorschemeName = colorscheme
}
@ -507,7 +423,7 @@ func (ct *Cointop) loadColorschemeFromConfig() error {
// LoadRefreshRateFromConfig loads refresh rate from config file to struct
func (ct *Cointop) loadRefreshRateFromConfig() error {
log.Debug("loadRefreshRateFromConfig()")
ct.debuglog("loadRefreshRateFromConfig()")
if refreshRate, ok := ct.config.RefreshRate.(int64); ok {
ct.State.refreshRate = time.Duration(uint(refreshRate)) * time.Second
}
@ -517,7 +433,7 @@ func (ct *Cointop) loadRefreshRateFromConfig() error {
// LoadCacheDirFromConfig loads cache dir from config file to struct
func (ct *Cointop) loadCacheDirFromConfig() error {
log.Debug("loadCacheDirFromConfig()")
ct.debuglog("loadCacheDirFromConfig()")
if cacheDir, ok := ct.config.CacheDir.(string); ok {
ct.State.cacheDir = pathutil.NormalizePath(cacheDir)
}
@ -525,39 +441,46 @@ func (ct *Cointop) loadCacheDirFromConfig() error {
return nil
}
// loadCompactNotationFromConfig loads compact-notation setting from config file to struct
func (ct *Cointop) loadCompactNotationFromConfig() error {
log.Debug("loadCompactNotationFromConfig()")
if compactNotation, ok := ct.config.CompactNotation.(bool); ok {
ct.State.compactNotation = compactNotation
}
// GetColorschemeColors loads colors from colorsheme file to struct
func (ct *Cointop) getColorschemeColors() (map[string]interface{}, error) {
ct.debuglog("getColorschemeColors()")
var colors map[string]interface{}
if ct.colorschemeName == "" {
ct.colorschemeName = DefaultColorscheme
if _, err := toml.Decode(DefaultColors, &colors); err != nil {
return nil, err
}
} else {
colorsDir := fmt.Sprintf("%s/colors", ct.ConfigDirPath())
if ct.colorsDir != "" {
colorsDir = pathutil.NormalizePath(ct.colorsDir)
}
return nil
}
path := fmt.Sprintf("%s/%s.toml", colorsDir, ct.colorschemeName)
if _, err := os.Stat(path); os.IsNotExist(err) {
// NOTE: case for when cointop is set as the theme but the colorscheme file doesn't exist
if ct.colorschemeName == "cointop" {
if _, err := toml.Decode(DefaultColors, &colors); err != nil {
return nil, err
}
// loadCompactNotationFromConfig loads compact-notation setting from config file to struct
func (ct *Cointop) loadEnableMouseFromConfig() error {
log.Debug("loadEnableMouseFromConfig()")
if enableMouse, ok := ct.config.EnableMouse.(bool); ok {
ct.State.enableMouse = enableMouse
}
return colors, nil
}
return nil
}
return nil, fmt.Errorf("the colorscheme file %q was not found.\n%s", path, ColorschemeHelpString())
}
// loadAltCoinLinkFromConfig loads AltCoinLink setting from config file to struct
func (ct *Cointop) loadAltCoinLinkFromConfig() error {
log.Debug("loadAltCoinLinkFromConfig()")
if altCoinLink, ok := ct.config.AltCoinLink.(string); ok {
ct.State.altCoinLink = altCoinLink
if _, err := toml.DecodeFile(path, &colors); err != nil {
return nil, err
}
}
return nil
return colors, nil
}
// LoadAPIChoiceFromConfig loads API choices from config file to struct
func (ct *Cointop) loadAPIChoiceFromConfig() error {
log.Debug("loadAPIChoiceFromConfig()")
ct.debuglog("loadAPIKeysFromConfig()")
apiChoice, ok := ct.config.API.(string)
if ok {
apiChoice = strings.TrimSpace(strings.ToLower(apiChoice))
@ -568,23 +491,20 @@ func (ct *Cointop) loadAPIChoiceFromConfig() error {
// LoadFavoritesFromConfig loads favorites data from config file to struct
func (ct *Cointop) loadFavoritesFromConfig() error {
log.Debug("loadFavoritesFromConfig()")
ct.debuglog("loadFavoritesFromConfig()")
for k, valueIfc := range ct.config.Favorites {
if k == "character" {
if favoriteChar, ok := valueIfc.(string); ok {
if utf8.RuneCountInString(favoriteChar) != 1 {
return fmt.Errorf("invalid favorite-character. Must be one-character")
}
ct.State.favoriteChar = favoriteChar
}
} else if k == "compact_notation" {
ct.State.favoritesCompactNotation = valueIfc.(bool)
}
ifcs, ok := valueIfc.([]interface{})
if !ok {
continue
}
switch k {
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
case "symbols":
for _, ifc := range ifcs {
if v, ok := ifc.(string); ok {
ct.State.favoritesBySymbol[strings.ToUpper(v)] = true
}
}
case "names":
for _, ifc := range ifcs {
if v, ok := ifc.(string); ok {
@ -613,7 +533,7 @@ func (ct *Cointop) loadFavoritesFromConfig() error {
// LoadPortfolioFromConfig loads portfolio data from config file to struct
func (ct *Cointop) loadPortfolioFromConfig() error {
log.Debug("loadPortfolioFromConfig()")
ct.debuglog("loadPortfolioFromConfig()")
for key, valueIfc := range ct.config.Portfolio {
if key == "columns" {
@ -633,9 +553,33 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
}
}
} else if key == "holdings" {
// Defer until the end to work around premature-save issue
} else if key == "compact_notation" {
ct.State.portfolioCompactNotation = valueIfc.(bool)
holdingsIfc, ok := valueIfc.([]interface{})
if !ok {
continue
}
for _, itemIfc := range holdingsIfc {
tupleIfc, ok := itemIfc.([]interface{})
if !ok {
continue
}
if len(tupleIfc) > 2 {
continue
}
name, ok := tupleIfc[0].(string)
if !ok {
continue
}
holdings, err := ct.InterfaceToFloat64(tupleIfc[1])
if err != nil {
return nil
}
if err := ct.SetPortfolioEntry(name, holdings); err != nil {
return err
}
}
} else {
// Backward compatibility < v1.6.0
holdings, err := ct.InterfaceToFloat64(valueIfc)
@ -643,68 +587,18 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
return err
}
if err := ct.SetPortfolioEntry(key, holdings, 0.0, ""); err != nil {
if err := ct.SetPortfolioEntry(key, holdings); err != nil {
return err
}
}
}
// Process holdings last because it causes a ct.Save()
if valueIfc, ok := ct.config.Portfolio["holdings"]; ok {
if holdingsIfc, ok := valueIfc.([]interface{}); ok {
ct.loadPortfolioHoldingsFromConfig(holdingsIfc)
}
}
return nil
}
func (ct *Cointop) loadPortfolioHoldingsFromConfig(holdingsIfc []interface{}) error {
for _, itemIfc := range holdingsIfc {
tupleIfc, ok := itemIfc.([]interface{})
if !ok {
continue
}
if len(tupleIfc) > 4 {
continue
}
name, ok := tupleIfc[0].(string)
if !ok {
continue // was not a string
}
holdings, err := ct.InterfaceToFloat64(tupleIfc[1])
if err != nil {
return err // was not a float64
}
buyPrice := 0.0
if len(tupleIfc) >= 3 {
if buyPrice, err = ct.InterfaceToFloat64(tupleIfc[2]); err != nil {
return err
}
}
buyCurrency := ""
if len(tupleIfc) >= 4 {
if parseCurrency, ok := tupleIfc[3].(string); !ok {
return err // was not a string
} else {
buyCurrency = parseCurrency
}
}
// Watch out - this calls ct.Save() which may save a half-loaded configuration
if err := ct.SetPortfolioEntry(name, holdings, buyPrice, buyCurrency); err != nil {
return err
}
}
return nil
}
// LoadPriceAlertsFromConfig loads price alerts from config file to struct
func (ct *Cointop) loadPriceAlertsFromConfig() error {
log.Debug("loadPriceAlertsFromConfig()")
ct.debuglog("loadPriceAlertsFromConfig()")
priceAlertsIfc, ok := ct.config.PriceAlerts["alerts"]
if !ok {
return nil
@ -762,43 +656,6 @@ func (ct *Cointop) loadPriceAlertsFromConfig() error {
return nil
}
// GetColorschemeColors loads colors from colorscheme file to struct
func (ct *Cointop) GetColorschemeColors() (map[string]interface{}, error) {
log.Debug("GetColorschemeColors()")
var colors map[string]interface{}
if ct.colorschemeName == "" {
ct.colorschemeName = DefaultColorscheme
if _, err := toml.Decode(DefaultColors, &colors); err != nil {
return nil, err
}
} else {
colorsDir := fmt.Sprintf("%s/colors", ct.ConfigDirPath())
if ct.colorsDir != "" {
colorsDir = pathutil.NormalizePath(ct.colorsDir)
}
path := fmt.Sprintf("%s/%s.toml", colorsDir, ct.colorschemeName)
if _, err := os.Stat(path); os.IsNotExist(err) {
// NOTE: case for when cointop is set as the theme but the colorscheme file doesn't exist
if ct.colorschemeName == "cointop" {
if _, err := toml.Decode(DefaultColors, &colors); err != nil {
return nil, err
}
return colors, nil
}
return nil, fmt.Errorf("the colorscheme file %q was not found.\n%s", path, ColorschemeHelpString())
}
if _, err := toml.DecodeFile(path, &colors); err != nil {
return nil, err
}
}
return colors, nil
}
// InterfaceToFloat64 attempts to convert interface to float64
func (ct *Cointop) InterfaceToFloat64(value interface{}) (float64, error) {
var num float64

@ -3,18 +3,14 @@ package cointop
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
fcolor "github.com/fatih/color"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/mattn/go-runewidth"
log "github.com/sirupsen/logrus"
color "github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/pad"
)
// FiatCurrencyNames is a map of currency symbols to names.
// FiatCurrencyNames is a mpa of currency symbols to names.
// Keep these in alphabetical order.
var FiatCurrencyNames = map[string]string{
"AUD": "Australian Dollar",
@ -59,55 +55,53 @@ var FiatCurrencyNames = map[string]string{
// CryptocurrencyNames is a map of cryptocurrency symbols to name
var CryptocurrencyNames = map[string]string{
"BTC": "Bitcoin",
"ETH": "Ethereum",
"SATS": "Satoshi",
"BTC": "Bitcoin",
"ETH": "Ethereum",
}
// CurrencySymbolMap is map of fiat currency symbols to names.
// Keep these in alphabetical order.
var CurrencySymbolMap = map[string]string{
"AUD": "$",
"BGN": "Лв.",
"BRL": "R$",
"BTC": "Ƀ",
"CAD": "$",
"CFH": "₣",
"CLP": "$",
"CNY": "¥",
"CZK": "Kč",
"DKK": "Kr",
"ETH": "Ξ",
"EUR": "€",
"GBP": "£",
"HKD": "$",
"HRK": "kn",
"HUF": "Ft",
"IDR": "Rp.",
"ILS": "₪",
"INR": "₹",
"ISK": "kr",
"JPY": "¥",
"KRW": "₩",
"MXN": "$",
"MYR": "RM",
"NOK": "kr",
"NZD": "$",
"PHP": "₱",
"PKR": "₨",
"PLN": "zł",
"RON": "lei",
"RUB": "Ꝑ",
"SEK": "kr",
"SGD": "S$",
"SATS": "丰",
"THB": "฿",
"TRY": "₺",
"TWD": "NT$",
"UAH": "₴",
"USD": "$",
"VND": "₫",
"ZAR": "R",
"AUD": "$",
"BGN": "Лв.",
"BRL": "R$",
"BTC": "Ƀ",
"CAD": "$",
"CFH": "₣",
"CLP": "$",
"CNY": "¥",
"CZK": "Kč",
"DKK": "Kr",
"ETH": "Ξ",
"EUR": "€",
"GBP": "£",
"HKD": "$",
"HRK": "kn",
"HUF": "Ft",
"IDR": "Rp.",
"ILS": "₪",
"INR": "₹",
"ISK": "kr",
"JPY": "¥",
"KRW": "₩",
"MXN": "$",
"MYR": "RM",
"NOK": "kr",
"NZD": "$",
"PHP": "₱",
"PKR": "₨",
"PLN": "zł",
"RON": "lei",
"RUB": "Ꝑ",
"SEK": "kr",
"SGD": "S$",
"THB": "฿",
"TRY": "₺",
"TWD": "NT$",
"UAH": "₴",
"USD": "$",
"VND": "₫",
"ZAR": "R",
}
var alphanumericcharacters = []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}
@ -157,8 +151,8 @@ func (ct *Cointop) SortedSupportedCurrencyConversions() []string {
// UpdateConvertMenu updates the convert menu
func (ct *Cointop) UpdateConvertMenu() error {
log.Debug("UpdateConvertMenu()")
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close ", ct.Width()-24, " ")))
ct.debuglog("updateConvertMenu()")
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close ", ct.width()-24, " ")))
helpline := " Press the corresponding key to select currency for conversion\n\n"
cnt := 0
h := ct.Views.Menu.Height()
@ -178,10 +172,9 @@ func (ct *Cointop) UpdateConvertMenu() error {
}
shortcut := string(alphanumericcharacters[i])
if key == ct.State.currencyConversion {
Bold := fcolor.New(fcolor.Bold).SprintFunc()
shortcut = ct.colorscheme.MenuLabelActive(Bold("*"))
key = ct.colorscheme.Menu(Bold(key))
currency = ct.colorscheme.MenuLabelActive(Bold(currency))
shortcut = ct.colorscheme.MenuLabelActive(color.Bold("*"))
key = ct.colorscheme.Menu(color.Bold(key))
currency = ct.colorscheme.MenuLabelActive(color.Bold(currency))
} else {
key = ct.colorscheme.Menu(key)
currency = ct.colorscheme.MenuLabel(currency)
@ -233,12 +226,8 @@ func (ct *Cointop) SetCurrencyConverstion(convert string) error {
// SetCurrencyConverstionFn sets the currency conversion function
func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
log.Debug("SetCurrencyConverstionFn()")
ct.debuglog("setCurrencyConverstionFn()")
return func() error {
if !ct.State.convertMenuVisible {
return nil
}
ct.HideConvertMenu()
if err := ct.SetCurrencyConverstion(convert); err != nil {
@ -248,7 +237,7 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
if err := ct.Save(); err != nil {
return err
}
go ct.UpdateCurrentPageCoins()
go ct.RefreshAll()
return nil
}
@ -256,20 +245,13 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
// CurrencySymbol returns the symbol for the currency conversion
func (ct *Cointop) CurrencySymbol() string {
log.Debug("CurrencySymbol()")
symbol := CurrencySymbol(ct.State.currencyConversion)
width := runewidth.StringWidth(symbol)
if width > 1 {
symbol = pad.Right(symbol, width, " ")
}
return symbol
ct.debuglog("currencySymbol()")
return CurrencySymbol(ct.State.currencyConversion)
}
// ShowConvertMenu shows the convert menu view
func (ct *Cointop) ShowConvertMenu() error {
log.Debug("ShowConvertMenu()")
ct.debuglog("showConvertMenu()")
ct.State.convertMenuVisible = true
ct.UpdateConvertMenu()
ct.SetActiveView(ct.Views.Menu.Name())
@ -278,7 +260,7 @@ func (ct *Cointop) ShowConvertMenu() error {
// HideConvertMenu hides the convert menu view
func (ct *Cointop) HideConvertMenu() error {
log.Debug("HideConvertMenu()")
ct.debuglog("hideConvertMenu()")
ct.State.convertMenuVisible = false
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.SetActiveView(ct.Views.Table.Name())
@ -291,7 +273,7 @@ func (ct *Cointop) HideConvertMenu() error {
// ToggleConvertMenu toggles the convert menu view
func (ct *Cointop) ToggleConvertMenu() error {
log.Debug("ToggleConvertMenu()")
ct.debuglog("toggleConvertMenu()")
ct.State.convertMenuVisible = !ct.State.convertMenuVisible
if ct.State.convertMenuVisible {
return ct.ShowConvertMenu()
@ -308,40 +290,3 @@ func CurrencySymbol(currency string) string {
return "?"
}
// ConversionMouseLeftClick is called on mouse left click event
func (ct *Cointop) ConversionMouseLeftClick() error {
v, x, y, err := ct.g.GetViewRelativeMousePosition(ct.g.CurrentEvent)
if err != nil {
return err
}
// Find the menu entry that includes the mouse position
line := v.BufferLines()[y]
matches := regexp.MustCompile(`\[ . \] \w+ [^\[]+`).FindAllStringIndex(line, -1)
for _, match := range matches {
if x >= match[0] && x <= match[1] {
s := line[match[0]:match[1]]
convert := strings.Split(s, " ")[3]
return ct.SetCurrencyConverstionFn(convert)()
}
}
return nil
}
// Convert converts an amount to another currency type
func (ct *Cointop) Convert(convertFrom, convertTo string, amount float64) (float64, error) {
convertFrom = strings.ToLower(convertFrom)
convertTo = strings.ToLower(convertTo)
if convertFrom == convertTo {
return amount, nil
}
rate, err := ct.api.GetExchangeRate(convertFrom, convertTo, true)
if err != nil {
return 0, err
}
return rate * amount, nil
}

@ -1,31 +1,25 @@
package cointop
import (
"fmt"
"log"
"os"
"github.com/cointop-sh/cointop/pkg/pathutil"
log "github.com/sirupsen/logrus"
)
func (ct *Cointop) setLogConfiguration() {
if os.Getenv("DEBUG") != "" {
log.SetLevel(log.DebugLevel)
ct.setLogOutputFile()
}
}
func (ct *Cointop) setLogOutputFile() {
filename := pathutil.NormalizePath(":PREFERRED_TEMP_DIR:/cointop.log")
debugFile := os.Getenv("DEBUG_FILE")
if debugFile != "" {
filename = pathutil.NormalizePath(debugFile)
}
func (ct *Cointop) initlog() {
filename := "/tmp/cointop.log"
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
panic(err)
}
log.SetOutput(f)
ct.logfile = f
fmt.Printf("Writing debug log to %s\n", filename)
}
// debuglog writes a debug log message to /tmp/cointop.log if the DEBUG environment is set.
func (ct *Cointop) debuglog(format string, args ...interface{}) {
if !ct.debug {
return
}
log.Printf(format+"\n", args...)
}

@ -3,91 +3,85 @@ package cointop
// DefaultShortcuts is a map of the default shortcuts
func DefaultShortcuts() map[string]string {
return map[string]string{
"up": "move_up",
"down": "move_down",
"left": "previous_page",
"right": "next_page",
"pagedown": "page_down",
"pageup": "page_up",
"home": "move_to_page_first_row",
"end": "move_to_page_last_row",
"enter": "toggle_row_chart",
"esc": "quit_view",
"space": "toggle_favorite",
"tab": "move_down_or_next_page",
"ctrl+c": "quit",
"ctrl+C": "quit",
"ctrl+d": "page_down",
"ctrl+f": "open_search",
"ctrl+n": "next_page",
"ctrl+o": "open_alt_link",
"ctrl+p": "previous_page",
"ctrl+r": "refresh",
"ctrl+R": "refresh",
"ctrl+s": "save",
"ctrl+S": "save",
"ctrl+u": "page_up",
"ctrl+j": "enlarge_chart",
"ctrl+k": "shorten_chart",
"ctrl+space": "toggle_portfolio_balances",
"|": "toggle_chart_fullscreen",
"alt+up": "sort_column_asc",
"alt+down": "sort_column_desc",
"alt+left": "sort_left_column",
"alt+right": "sort_right_column",
"F1": "help",
"F5": "refresh",
"0": "move_to_first_page_first_row",
"1": "sort_column_1h_change",
"2": "sort_column_24h_change",
"3": "sort_column_30d_change",
"7": "sort_column_7d_change",
"a": "sort_column_available_supply",
"b": "sort_column_balance",
"c": "show_currency_convert_menu",
"C": "show_currency_convert_menu",
"e": "show_portfolio_edit_menu",
"E": "show_portfolio_edit_menu",
"A": "toggle_price_alerts",
"f": "toggle_favorite",
"F": "toggle_show_favorites",
"g": "move_to_page_first_row",
"G": "move_to_page_last_row",
"h": "previous_page",
"H": "move_to_page_visible_first_row",
"j": "move_down",
"k": "move_up",
"l": "next_page",
"L": "move_to_page_visible_last_row",
"m": "sort_column_market_cap",
"M": "move_to_page_visible_middle_row",
"n": "sort_column_name",
"o": "open_link",
"O": "open_link",
"p": "sort_column_price",
"P": "toggle_portfolio",
"r": "sort_column_rank",
"s": "sort_column_symbol",
"t": "sort_column_total_supply",
"u": "sort_column_last_updated",
"v": "sort_column_24h_volume",
"y": "sort_column_1y_change",
"q": "quit_view",
"Q": "quit_view",
"%": "sort_column_percent_holdings",
"$": "last_page",
"?": "help",
"/": "open_search",
"]": "next_chart_range",
"[": "previous_chart_range",
"}": "last_chart_range",
"{": "first_chart_range",
">": "scroll_right",
"<": "scroll_left",
"+": "show_price_alert_add_menu",
"\\\\": "toggle_table_fullscreen",
"!": "sort_column_cost",
"@": "sort_column_pnl",
"#": "sort_column_pnl_percent",
"up": "move_up",
"down": "move_down",
"left": "previous_page",
"right": "next_page",
"pagedown": "page_down",
"pageup": "page_up",
"home": "move_to_page_first_row",
"end": "move_to_page_last_row",
"enter": "toggle_row_chart",
"esc": "quit_view",
"space": "toggle_favorite",
"tab": "move_down_or_next_page",
"ctrl+c": "quit",
"ctrl+C": "quit",
"ctrl+d": "page_down",
"ctrl+f": "open_search",
"ctrl+n": "next_page",
"ctrl+p": "previous_page",
"ctrl+r": "refresh",
"ctrl+R": "refresh",
"ctrl+s": "save",
"ctrl+S": "save",
"ctrl+u": "page_up",
"ctrl+j": "enlarge_chart",
"ctrl+k": "shorten_chart",
"|": "toggle_chart_fullscreen",
"alt+up": "sort_column_asc",
"alt+down": "sort_column_desc",
"alt+left": "sort_left_column",
"alt+right": "sort_right_column",
"F1": "help",
"F5": "refresh",
"0": "first_page",
"1": "sort_column_1h_change",
"2": "sort_column_24h_change",
"3": "sort_column_30d_change",
"7": "sort_column_7d_change",
"a": "sort_column_available_supply",
"b": "sort_column_balance",
"c": "show_currency_convert_menu",
"C": "show_currency_convert_menu",
"e": "show_portfolio_edit_menu",
"E": "show_portfolio_edit_menu",
"A": "toggle_price_alerts",
"f": "toggle_favorite",
"F": "toggle_show_favorites",
"g": "move_to_page_first_row",
"G": "move_to_page_last_row",
"h": "previous_page",
"H": "move_to_page_visible_first_row",
"j": "move_down",
"k": "move_up",
"l": "next_page",
"L": "move_to_page_visible_last_row",
"m": "sort_column_market_cap",
"M": "move_to_page_visible_middle_row",
"n": "sort_column_name",
"o": "open_link",
"O": "open_link",
"p": "sort_column_price",
"P": "toggle_portfolio",
"r": "sort_column_rank",
"s": "sort_column_symbol",
"t": "sort_column_total_supply",
"u": "sort_column_last_updated",
"v": "sort_column_24h_volume",
"q": "quit_view",
"Q": "quit_view",
"%": "sort_column_percent_holdings",
"$": "last_page",
"?": "help",
"/": "open_search",
"]": "next_chart_range",
"[": "previous_chart_range",
"}": "last_chart_range",
"{": "first_chart_range",
">": "scroll_right",
"<": "scroll_left",
"+": "show_price_alert_add_menu",
"\\\\": "toggle_table_fullscreen",
}
}

@ -2,9 +2,8 @@ package cointop
import (
"fmt"
"os"
"github.com/cointop-sh/cointop/pkg/api"
"github.com/miguelmota/cointop/pkg/api"
)
// DominanceConfig is the config options for the dominance command
@ -23,9 +22,7 @@ func PrintBitcoinDominance(config *DominanceConfig) error {
if config.APIChoice == CoinMarketCap {
coinAPI = api.NewCMC("")
} else if config.APIChoice == CoinGecko {
coinAPI = api.NewCG(&api.CoinGeckoConfig{
ApiKey: os.Getenv("COINGECKO_PRO_API_KEY"),
})
coinAPI = api.NewCG(0, 0)
} else {
return ErrInvalidAPIChoice
}

@ -1,9 +1,7 @@
package cointop
import log "github.com/sirupsen/logrus"
// RowChanged is called when the row is updated
func (ct *Cointop) RowChanged() {
log.Debug("RowChanged()")
ct.debuglog("RowChanged()")
ct.RefreshRowLink()
}

@ -2,8 +2,6 @@ package cointop
import (
"sort"
log "github.com/sirupsen/logrus"
)
// GetFavoritesTableHeaders returns the favorites table headers
@ -13,7 +11,7 @@ func (ct *Cointop) GetFavoritesTableHeaders() []string {
// ToggleFavorite toggles coin as favorite
func (ct *Cointop) ToggleFavorite() error {
log.Debug("ToggleFavorite()")
ct.debuglog("toggleFavorite()")
coin := ct.HighlightedRowCoin()
if coin == nil {
return nil
@ -39,7 +37,7 @@ func (ct *Cointop) ToggleFavorite() error {
// ToggleFavorites toggles the favorites view
func (ct *Cointop) ToggleFavorites() error {
log.Debug("ToggleFavorites()")
ct.debuglog("toggleFavorites()")
ct.ToggleSelectedView(FavoritesView)
go ct.UpdateTable()
return nil
@ -47,7 +45,7 @@ func (ct *Cointop) ToggleFavorites() error {
// ToggleShowFavorites shows the favorites view
func (ct *Cointop) ToggleShowFavorites() error {
log.Debug("ToggleShowFavorites()")
ct.debuglog("toggleShowFavorites()")
ct.ToggleSelectedView(FavoritesView)
go ct.UpdateTable()
return nil
@ -55,8 +53,8 @@ func (ct *Cointop) ToggleShowFavorites() error {
// GetFavoritesSlice returns coin favorites as slice
func (ct *Cointop) GetFavoritesSlice() []*Coin {
log.Debug("GetFavoritesSlice()")
var sliced []*Coin
ct.debuglog("getFavoritesSlice()")
sliced := []*Coin{}
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.Favorite {
@ -64,7 +62,7 @@ func (ct *Cointop) GetFavoritesSlice() []*Coin {
}
}
sort.SliceStable(sliced, func(i, j int) bool {
sort.Slice(sliced, func(i, j int) bool {
return sliced[i].MarketCap > sliced[j].MarketCap
})

@ -4,20 +4,19 @@ import (
"fmt"
"sort"
"github.com/cointop-sh/cointop/pkg/pad"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/pad"
)
// UpdateHelp updates the help views
func (ct *Cointop) UpdateHelp() {
log.Debug("UpdateHelp()")
ct.debuglog("updateHelp()")
keys := make([]string, 0, len(ct.State.shortcutKeys))
for k := range ct.State.shortcutKeys {
keys = append(keys, k)
}
sort.Strings(keys)
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Help %s\n\n", pad.Left("[q] close ", ct.Width()-9, " ")))
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Help %s\n\n", pad.Left("[q] close ", ct.width()-9, " ")))
cnt := 0
h := ct.Views.Menu.Height()
percol := h - 11
@ -59,7 +58,7 @@ func (ct *Cointop) UpdateHelp() {
// ShowHelp shows the help view
func (ct *Cointop) ShowHelp() error {
log.Debug("ShowHelp()")
ct.debuglog("showHelp()")
ct.State.helpVisible = true
ct.UpdateHelp()
ct.SetActiveView(ct.Views.Menu.Name())
@ -68,7 +67,7 @@ func (ct *Cointop) ShowHelp() error {
// HideHelp hides the help view
func (ct *Cointop) HideHelp() error {
log.Debug("HideHelp()")
ct.debuglog("hideHelp()")
ct.State.helpVisible = false
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.SetActiveView(ct.Views.Table.Name())
@ -81,7 +80,7 @@ func (ct *Cointop) HideHelp() error {
// ToggleHelp toggles the help view
func (ct *Cointop) ToggleHelp() error {
log.Debug("ToggleHelp()")
ct.debuglog("toggleHelp()")
ct.State.helpVisible = !ct.State.helpVisible
if ct.State.helpVisible {
return ct.ShowHelp()

@ -2,390 +2,380 @@ package cointop
import (
"strings"
"unicode"
"github.com/cointop-sh/cointop/pkg/gocui"
"github.com/gdamore/tcell/v2"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/gocui"
)
// keyMap translates key alternative names to a canonical version
func keyMap(k string) string {
key := k
switch strings.ToLower(k) {
case "lsqrbracket", "leftsqrbracket", "leftsquarebracket":
key = "["
case "rsqrbracket", "rightsqrbracket", "rightsquarebracket":
key = "]"
case "space", "spacebar":
key = " " // with meta should be "space"
case "\\\\", "backslash":
key = "\\"
case "underscore":
key = "_"
case "arrowup", "uparrow":
key = "Up"
case "arrowdown", "downarrow":
key = "Down"
case "arrowleft", "leftarrow":
key = "Left"
case "arrowright", "rightarrow":
key = "Right"
case "return":
key = "Enter"
case "escape":
key = "Esc"
case "pageup":
key = "PgUp"
case "pagedown", "pgdown":
key = "PgDn"
}
return key
}
// ParseKeys returns string keyboard key as gocui key type
func (ct *Cointop) ParseKeys(s string) (interface{}, tcell.ModMask) {
// TODO: change file convention to match tcell (no aliases, dash between mod and key)
// TODO: change to return EventKey?
func (ct *Cointop) ParseKeys(s string) (interface{}, gocui.Modifier) {
var key interface{}
mod := tcell.ModNone
// translate legacy and special names for keys
keyName := keyMap(strings.TrimSpace(s))
if len(keyName) > 1 {
keyName = strings.Replace(keyName, "+", "-", -1)
split := strings.Split(keyName, "-")
if len(split) > 1 {
m := strings.ToLower(strings.TrimSpace(split[0]))
k := strings.TrimSpace(split[1])
k = keyMap(k)
if k == " " {
k = "Space" // fix mod+space
}
if m == "alt" {
mod = tcell.ModAlt
keyName = k
} else if m == "ctrl" {
// let the lookup handle it
keyName = m + "-" + k
} else {
keyName = m + "-" + k
}
// TODO: other mods?
}
}
// First try looking up keyname directly
lcKeyName := strings.ToLower(keyName)
for key, name := range tcell.KeyNames {
if strings.ToLower(name) == lcKeyName {
if strings.HasPrefix(name, "Ctrl-") {
mod = tcell.ModCtrl
mod := gocui.ModNone
split := strings.Split(s, "+")
if len(split) > 1 {
m := strings.ToLower(strings.TrimSpace(split[0]))
k := strings.ToLower(strings.TrimSpace(split[1]))
if m == "alt" {
mod = gocui.ModAlt
s = k
} else if m == "ctrl" {
switch k {
case "0":
key = '0'
case "1":
key = '1'
case "2":
key = gocui.KeyCtrl2
case "3":
key = gocui.KeyCtrl3
case "4":
key = gocui.KeyCtrl4
case "5":
key = gocui.KeyCtrl5
case "6":
key = gocui.KeyCtrl6
case "7":
key = gocui.KeyCtrl7
case "8":
key = gocui.KeyCtrl8
case "9":
key = '9'
case "a":
key = gocui.KeyCtrlA
case "b":
key = gocui.KeyCtrlB
case "c":
key = gocui.KeyCtrlC
case "d":
key = gocui.KeyCtrlD
case "e":
key = gocui.KeyCtrlE
case "f":
key = gocui.KeyCtrlF
case "g":
key = gocui.KeyCtrlG
case "h":
key = gocui.KeyCtrlH
case "i":
key = gocui.KeyCtrlI
case "j":
key = gocui.KeyCtrlJ
case "k":
key = gocui.KeyCtrlK
case "l":
key = gocui.KeyCtrlL
case "m":
key = gocui.KeyCtrlL
case "n":
key = gocui.KeyCtrlN
case "o":
key = gocui.KeyCtrlO
case "p":
key = gocui.KeyCtrlP
case "q":
key = gocui.KeyCtrlQ
case "r":
key = gocui.KeyCtrlR
case "s":
key = gocui.KeyCtrlS
case "t":
key = gocui.KeyCtrlT
case "u":
key = gocui.KeyCtrlU
case "v":
key = gocui.KeyCtrlV
case "w":
key = gocui.KeyCtrlW
case "x":
key = gocui.KeyCtrlX
case "y":
key = gocui.KeyCtrlY
case "z":
key = gocui.KeyCtrlZ
case "~":
key = gocui.KeyCtrlTilde
case "[", "lsqrbracket", "leftsqrbracket", "leftsquarebracket":
key = gocui.KeyCtrlLsqBracket
case "]", "rsqrbracket", "rightsqrbracket", "rightsquarebracket":
key = gocui.KeyCtrlRsqBracket
case "space":
key = gocui.KeyCtrlSpace
case "backslash":
key = gocui.KeyCtrlBackslash
case "underscore":
key = gocui.KeyCtrlUnderscore
case "\\\\":
key = '\\'
}
return key, mod
}
}
// Then try one-rune variants
if len(keyName) == 1 {
r := []rune(keyName)
if len(s) == 1 {
r := []rune(s)
key = r[0]
return key, mod
}
if key == nil {
log.Debugf("Could not map key '%s' to key", s)
}
return key, mod
}
// SetKeybindingAction maps a shortcut key to an action
func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error {
if shortcutKey == "" {
return nil
}
action = strings.TrimSpace(strings.ToLower(action))
var fn func(g *gocui.Gui, v *gocui.View) error
key, mod := ct.ParseKeys(shortcutKey)
view := "table"
switch action {
case "move_up":
fn = ct.Keyfn(ct.CursorUp)
case "move_down":
fn = ct.Keyfn(ct.CursorDown)
case "previous_page":
fn = ct.handleHkey(key)
case "next_page":
fn = ct.Keyfn(ct.NextPage)
case "page_down":
fn = ct.Keyfn(ct.PageDown)
case "page_up":
fn = ct.Keyfn(ct.PageUp)
case "sort_column_symbol":
fn = ct.Sortfn("symbol", false)
case "move_to_page_first_row":
fn = ct.Keyfn(ct.NavigateFirstLine)
case "move_to_page_last_row":
fn = ct.Keyfn(ct.NavigateLastLine)
case "open_link":
fn = ct.Keyfn(ct.OpenLink)
case "open_alt_link":
fn = ct.Keyfn(ct.OpenAltLink)
case "refresh":
fn = ct.Keyfn(ct.Refresh)
case "sort_column_asc":
fn = ct.Keyfn(ct.SortAsc)
case "sort_column_desc":
fn = ct.Keyfn(ct.SortDesc)
case "sort_left_column":
fn = ct.Keyfn(ct.SortPrevCol)
case "sort_right_column":
fn = ct.Keyfn(ct.SortNextCol)
case "help", "toggle_show_help":
fn = ct.Keyfn(ct.ToggleHelp)
view = ""
case "show_help":
fn = ct.Keyfn(ct.ShowHelp)
view = ""
case "hide_help":
fn = ct.Keyfn(ct.HideHelp)
view = "help"
case "first_page":
fn = ct.Keyfn(ct.FirstPage)
case "move_to_first_page_first_row":
fn = ct.Keyfn(ct.NavigateToFirstPageFirstRow)
case "sort_column_1h_change":
fn = ct.Sortfn("1h_change", true)
case "sort_column_24h_change":
fn = ct.Sortfn("24h_change", true)
case "sort_column_7d_change":
fn = ct.Sortfn("7d_change", true)
case "sort_column_30d_change":
fn = ct.Sortfn("30d_change", true)
case "sort_column_1y_change":
fn = ct.Sortfn("1y_change", true)
case "sort_column_available_supply":
fn = ct.Sortfn("available_supply", true)
case "toggle_row_chart":
fn = ct.Keyfn(ct.ToggleCoinChart)
case "move_to_page_visible_first_row":
fn = ct.Keyfn(ct.NavigatePageFirstLine)
case "move_to_page_visible_last_row":
fn = ct.Keyfn(ct.navigatePageLastLine)
case "sort_column_market_cap":
fn = ct.Sortfn("market_cap", true)
case "move_to_page_visible_middle_row":
fn = ct.Keyfn(ct.NavigatePageMiddleLine)
case "scroll_left":
fn = ct.Keyfn(ct.TableScrollLeft)
case "scroll_right":
fn = ct.Keyfn(ct.TableScrollRight)
case "sort_column_name":
fn = ct.Sortfn("name", false)
case "sort_column_price":
fn = ct.Sortfn("price", true)
case "sort_column_rank":
fn = ct.Sortfn("rank", false)
case "sort_column_total_supply":
fn = ct.Sortfn("total_supply", true)
case "sort_column_last_updated":
fn = ct.Sortfn("last_updated", true)
case "sort_column_24h_volume":
fn = ct.Sortfn("24h_volume", true)
case "sort_column_balance":
fn = ct.Sortfn("balance", true)
case "sort_column_holdings":
fn = ct.Sortfn("holdings", true)
case "sort_column_percent_holdings":
fn = ct.Sortfn("percent_holdings", true)
case "last_page":
fn = ct.Keyfn(ct.LastPage)
case "open_search":
fn = ct.Keyfn(ct.OpenSearch)
view = ""
case "toggle_price_alerts":
fn = ct.Keyfn(ct.TogglePriceAlerts)
case "toggle_favorite":
fn = ct.Keyfn(ct.ToggleFavorite)
case "toggle_favorites":
fn = ct.Keyfn(ct.ToggleFavorites)
case "toggle_show_favorites":
fn = ct.Keyfn(ct.ToggleShowFavorites)
case "save":
fn = ct.Keyfn(ct.Save)
case "quit":
fn = ct.Keyfn(ct.Quit)
view = ""
case "quit_view":
fn = ct.Keyfn(ct.QuitView)
case "next_chart_range":
fn = ct.Keyfn(ct.NextChartRange)
case "previous_chart_range":
fn = ct.Keyfn(ct.PrevChartRange)
case "first_chart_range":
fn = ct.Keyfn(ct.FirstChartRange)
case "last_chart_range":
fn = ct.Keyfn(ct.LastChartRange)
case "toggle_show_currency_convert_menu":
fn = ct.Keyfn(ct.ToggleConvertMenu)
case "show_currency_convert_menu":
fn = ct.Keyfn(ct.ShowConvertMenu)
case "hide_currency_convert_menu":
fn = ct.Keyfn(ct.HideConvertMenu)
view = "convertmenu"
case "toggle_portfolio":
fn = ct.Keyfn(ct.TogglePortfolio)
case "toggle_show_portfolio":
fn = ct.Keyfn(ct.ToggleShowPortfolio)
case "toggle_portfolio_balances":
fn = ct.Keyfn(ct.TogglePortfolioBalances)
case "show_portfolio_edit_menu":
fn = ct.Keyfn(ct.TogglePortfolioUpdateMenu)
case "show_price_alert_edit_menu":
fn = ct.Keyfn(ct.ShowPriceAlertsUpdateMenu)
case "show_price_alert_add_menu":
fn = ct.Keyfn(ct.ShowPriceAlertsAddMenu)
case "toggle_table_fullscreen":
fn = ct.Keyfn(ct.ToggleTableFullscreen)
view = ""
case "toggle_chart_fullscreen":
fn = ct.Keyfn(ct.ToggleChartFullscreen)
view = ""
case "enlarge_chart":
fn = ct.Keyfn(ct.EnlargeChart)
case "shorten_chart":
fn = ct.Keyfn(ct.ShortenChart)
case "move_down_or_next_page":
fn = ct.Keyfn(ct.CursorDownOrNextPage)
case "move_up_or_previous_page":
fn = ct.Keyfn(ct.CursorUpOrPreviousPage)
case "sort_column_cost":
fn = ct.Sortfn("cost", true)
case "sort_column_pnl":
fn = ct.Sortfn("pnl", true)
case "sort_column_pnl_percent":
fn = ct.Sortfn("pnl_percent", true)
default:
fn = ct.Keyfn(ct.Noop)
s = strings.ToLower(s)
switch s {
case "arrowup", "uparrow", "up":
key = gocui.KeyArrowUp
case "arrowdown", "downarrow", "down":
key = gocui.KeyArrowDown
case "arrowleft", "leftarrow", "left":
key = gocui.KeyArrowLeft
case "arrowright", "rightarrow", "right":
key = gocui.KeyArrowRight
case "enter", "return":
key = gocui.KeyEnter
case "space", "spacebar":
key = gocui.KeySpace
case "esc", "escape":
key = gocui.KeyEsc
case "f1":
key = gocui.KeyF1
case "f2":
key = gocui.KeyF2
case "f3":
key = gocui.KeyF3
case "f4":
key = gocui.KeyF4
case "f5":
key = gocui.KeyF5
case "f6":
key = gocui.KeyF6
case "f7":
key = gocui.KeyF7
case "f8":
key = gocui.KeyF8
case "f9":
key = gocui.KeyF9
case "tab":
key = gocui.KeyTab
case "pageup", "pgup":
key = gocui.KeyPgup
case "pagedown", "pgdown", "pgdn":
key = gocui.KeyPgdn
case "home":
key = gocui.KeyHome
case "end":
key = gocui.KeyEnd
case "\\\\":
key = '\\'
}
ct.SetKeybindingMod(key, mod, fn, view)
return nil
return key, mod
}
// SetKeybindings sets keyboard shortcut key bindings
func (ct *Cointop) SetKeybindings() error {
// Keybindings sets keyboard shortcut key bindings
func (ct *Cointop) Keybindings(g *gocui.Gui) error {
for k, v := range ct.State.shortcutKeys {
if err := ct.SetKeybindingAction(k, v); err != nil {
return err
if k == "" {
continue
}
v = strings.TrimSpace(strings.ToLower(v))
var fn func(g *gocui.Gui, v *gocui.View) error
key, mod := ct.ParseKeys(k)
view := "table"
switch v {
case "move_up":
fn = ct.Keyfn(ct.CursorUp)
case "move_down":
fn = ct.Keyfn(ct.CursorDown)
case "previous_page":
fn = ct.handleHkey(key)
case "next_page":
fn = ct.Keyfn(ct.NextPage)
case "page_down":
fn = ct.Keyfn(ct.PageDown)
case "page_up":
fn = ct.Keyfn(ct.PageUp)
case "sort_column_symbol":
fn = ct.Sortfn("symbol", false)
case "move_to_page_first_row":
fn = ct.Keyfn(ct.NavigateFirstLine)
case "move_to_page_last_row":
fn = ct.Keyfn(ct.NavigateLastLine)
case "open_link":
fn = ct.Keyfn(ct.OpenLink)
case "refresh":
fn = ct.Keyfn(ct.Refresh)
case "sort_column_asc":
fn = ct.Keyfn(ct.SortAsc)
case "sort_column_desc":
fn = ct.Keyfn(ct.SortDesc)
case "sort_left_column":
fn = ct.Keyfn(ct.SortPrevCol)
case "sort_right_column":
fn = ct.Keyfn(ct.SortNextCol)
case "help", "toggle_show_help":
fn = ct.Keyfn(ct.ToggleHelp)
view = ""
case "show_help":
fn = ct.Keyfn(ct.ShowHelp)
view = ""
case "hide_help":
fn = ct.Keyfn(ct.HideHelp)
view = "help"
case "first_page":
fn = ct.Keyfn(ct.FirstPage)
case "sort_column_1h_change":
fn = ct.Sortfn("1h_change", true)
case "sort_column_24h_change":
fn = ct.Sortfn("24h_change", true)
case "sort_column_7d_change":
fn = ct.Sortfn("7d_change", true)
case "sort_column_30d_change":
fn = ct.Sortfn("30d_change", true)
case "sort_column_available_supply":
fn = ct.Sortfn("available_supply", true)
case "toggle_row_chart":
fn = ct.Keyfn(ct.ToggleCoinChart)
case "move_to_page_visible_first_row":
fn = ct.Keyfn(ct.NavigatePageFirstLine)
case "move_to_page_visible_last_row":
fn = ct.Keyfn(ct.navigatePageLastLine)
case "sort_column_market_cap":
fn = ct.Sortfn("market_cap", true)
case "move_to_page_visible_middle_row":
fn = ct.Keyfn(ct.NavigatePageMiddleLine)
case "scroll_left":
fn = ct.Keyfn(ct.TableScrollLeft)
case "scroll_right":
fn = ct.Keyfn(ct.TableScrollRight)
case "sort_column_name":
fn = ct.Sortfn("name", false)
case "sort_column_price":
fn = ct.Sortfn("price", true)
case "sort_column_rank":
fn = ct.Sortfn("rank", false)
case "sort_column_total_supply":
fn = ct.Sortfn("total_supply", true)
case "sort_column_last_updated":
fn = ct.Sortfn("last_updated", true)
case "sort_column_24h_volume":
fn = ct.Sortfn("24h_volume", true)
case "sort_column_balance":
fn = ct.Sortfn("balance", true)
case "sort_column_holdings":
fn = ct.Sortfn("holdings", true)
case "sort_column_percent_holdings":
fn = ct.Sortfn("percent_holdings", true)
case "last_page":
fn = ct.Keyfn(ct.LastPage)
case "open_search":
fn = ct.Keyfn(ct.openSearch)
view = ""
case "toggle_price_alerts":
fn = ct.Keyfn(ct.TogglePriceAlerts)
case "toggle_favorite":
fn = ct.Keyfn(ct.ToggleFavorite)
case "toggle_favorites":
fn = ct.Keyfn(ct.ToggleFavorites)
case "toggle_show_favorites":
fn = ct.Keyfn(ct.ToggleShowFavorites)
case "save":
fn = ct.Keyfn(ct.Save)
case "quit":
fn = ct.Keyfn(ct.Quit)
view = ""
case "quit_view":
fn = ct.Keyfn(ct.QuitView)
case "next_chart_range":
fn = ct.Keyfn(ct.NextChartRange)
case "previous_chart_range":
fn = ct.Keyfn(ct.PrevChartRange)
case "first_chart_range":
fn = ct.Keyfn(ct.FirstChartRange)
case "last_chart_range":
fn = ct.Keyfn(ct.LastChartRange)
case "toggle_show_currency_convert_menu":
fn = ct.Keyfn(ct.ToggleConvertMenu)
case "show_currency_convert_menu":
fn = ct.Keyfn(ct.ShowConvertMenu)
case "hide_currency_convert_menu":
fn = ct.Keyfn(ct.HideConvertMenu)
view = "convertmenu"
case "toggle_portfolio":
fn = ct.Keyfn(ct.TogglePortfolio)
case "toggle_show_portfolio":
fn = ct.Keyfn(ct.ToggleShowPortfolio)
case "show_portfolio_edit_menu":
fn = ct.Keyfn(ct.TogglePortfolioUpdateMenu)
case "show_price_alert_edit_menu":
fn = ct.Keyfn(ct.ShowPriceAlertsUpdateMenu)
case "show_price_alert_add_menu":
fn = ct.Keyfn(ct.ShowPriceAlertsAddMenu)
case "toggle_table_fullscreen":
fn = ct.Keyfn(ct.ToggleTableFullscreen)
view = ""
case "toggle_chart_fullscreen":
fn = ct.Keyfn(ct.ToggleChartFullscreen)
view = ""
case "enlarge_chart":
fn = ct.Keyfn(ct.EnlargeChart)
case "shorten_chart":
fn = ct.Keyfn(ct.ShortenChart)
case "move_down_or_next_page":
fn = ct.Keyfn(ct.CursorDownOrNextPage)
case "move_up_or_previous_page":
fn = ct.Keyfn(ct.CursorUpOrPreviousPage)
default:
fn = ct.Keyfn(ct.Noop)
}
ct.SetKeybindingMod(key, mod, fn, view)
}
// keys to force quit
ct.SetKeybindingMod(tcell.KeyCtrlC, tcell.ModNone, ct.Keyfn(ct.Quit), "")
ct.SetKeybindingMod(tcell.KeyCtrlZ, tcell.ModNone, ct.Keyfn(ct.Quit), "")
ct.SetKeybindingMod(gocui.KeyCtrlC, gocui.ModNone, ct.Keyfn(ct.Quit), "")
ct.SetKeybindingMod(gocui.KeyCtrlZ, gocui.ModNone, ct.Keyfn(ct.Quit), "")
// searchfield keys
ct.SetKeybindingMod(tcell.KeyEnter, tcell.ModNone, ct.Keyfn(ct.DoSearch), ct.Views.SearchField.Name())
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.CancelSearch), ct.Views.SearchField.Name())
ct.SetKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.Keyfn(ct.DoSearch), ct.Views.SearchField.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.CancelSearch), ct.Views.SearchField.Name())
// keys to quit help when open
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
// keys to quit portfolio update menu when open
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
// keys to quit convert menu when open
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
// keys to update portfolio holdings
ct.SetKeybindingMod(tcell.KeyEnter, tcell.ModNone, ct.Keyfn(ct.EnterKeyPressHandler), ct.Views.Input.Name())
// Work around issue with key-binding for '/' interfering with expressions
key, mod := ct.ParseKeys("/")
ct.DeleteKeybindingMod(key, mod, "")
ct.SetKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.Keyfn(ct.EnterKeyPressHandler), ct.Views.Input.Name())
// mouse events
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.MouseLeftClick), ct.Views.Table.Name()) // click to focus
// clicking table headers sorts table
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.TableHeaderMouseLeftClick), ct.Views.TableHeader.Name())
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.StatusbarMouseLeftClick), ct.Views.Statusbar.Name())
// debug mouse clicks
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.MouseDebug), "")
ct.SetMousebindingMod(tcell.WheelUp, tcell.ModNone, ct.Keyfn(ct.CursorUpOrPreviousPage), ct.Views.Table.Name())
ct.SetMousebindingMod(tcell.WheelDown, tcell.ModNone, ct.Keyfn(ct.CursorDownOrNextPage), ct.Views.Table.Name())
ct.SetKeybindingMod(gocui.MouseRelease, gocui.ModNone, ct.Keyfn(ct.MouseRelease), "")
ct.SetKeybindingMod(gocui.MouseLeft, gocui.ModNone, ct.Keyfn(ct.MouseLeftClick), "")
ct.SetKeybindingMod(gocui.MouseMiddle, gocui.ModNone, ct.Keyfn(ct.MouseMiddleClick), "")
ct.SetKeybindingMod(gocui.MouseRight, gocui.ModNone, ct.Keyfn(ct.MouseRightClick), "")
ct.SetKeybindingMod(gocui.MouseWheelUp, gocui.ModNone, ct.Keyfn(ct.MouseWheelUp), "")
ct.SetKeybindingMod(gocui.MouseWheelDown, gocui.ModNone, ct.Keyfn(ct.MouseWheelDown), "")
// character key press to select option
// TODO: use scrolling table
keys := ct.SortedSupportedCurrencyConversions()
for i, k := range keys {
ct.SetKeybindingMod(alphanumericcharacters[i], tcell.ModNone, ct.Keyfn(ct.SetCurrencyConverstionFn(k)), ct.Views.Menu.Name())
ct.SetKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.Keyfn(ct.SetCurrencyConverstionFn(k)), ct.Views.Menu.Name())
}
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.ConversionMouseLeftClick), ct.Views.Menu.Name())
return nil
}
// MouseDebug emit a debug message about which View and coordinates are in MouseClick
func (ct *Cointop) MouseDebug() error {
v, x, y, err := ct.g.GetViewRelativeMousePosition(ct.g.CurrentEvent)
if err != nil {
return err
}
log.Debugf("XXX MouseDebug view=%s %d,%d", v.Name(), x, y)
return nil
}
// SetKeybindingMod sets the keybinding modifier key
func (ct *Cointop) SetKeybindingMod(key interface{}, mod tcell.ModMask, callback func(g *gocui.Gui, v *gocui.View) error, view string) error {
// TODO: take EventKey?
func (ct *Cointop) SetKeybindingMod(key interface{}, mod gocui.Modifier, callback func(g *gocui.Gui, v *gocui.View) error, view string) error {
var err error
switch t := key.(type) {
case tcell.Key:
err = ct.g.SetKeybinding(view, t, 0, mod, callback)
case gocui.Key:
err = ct.g.SetKeybinding(view, t, mod, callback)
case rune:
err = ct.g.SetKeybinding(view, tcell.KeyRune, t, mod, callback)
if err != nil {
return err
}
// Binding Shift+[key] if key is uppercase and modifiers missing Shift
// to support using on Windows
if unicode.ToUpper(t) == t && (tcell.ModShift&mod == 0) {
err = ct.g.SetKeybinding(view, tcell.KeyRune, t, mod|tcell.ModShift, callback)
}
}
return err
}
// SetMousebindingMod adds a binding for a mouse eventdef
func (ct *Cointop) SetMousebindingMod(btn tcell.ButtonMask, mod tcell.ModMask, callback func(g *gocui.Gui, v *gocui.View) error, view string) error {
return ct.g.SetMousebinding(view, btn, mod, callback)
}
// DeleteKeybinding ...
func (ct *Cointop) DeleteKeybinding(shortcutKey string) error {
key, mod := ct.ParseKeys(shortcutKey)
return ct.DeleteKeybindingMod(key, mod, "")
}
// DeleteKeybindingMod ...
func (ct *Cointop) DeleteKeybindingMod(key interface{}, mod tcell.ModMask, view string) error {
// TODO: take EventKey
var err error
switch t := key.(type) {
case tcell.Key:
err = ct.g.DeleteKeybinding(view, t, 0, mod)
case rune:
err = ct.g.DeleteKeybinding(view, tcell.KeyRune, t, mod)
err = ct.g.SetKeybinding(view, t, mod, callback)
}
return err
}
@ -393,10 +383,7 @@ func (ct *Cointop) DeleteKeybindingMod(key interface{}, mod tcell.ModMask, view
// Keyfn returns the keybinding function as a wrapped gocui view function
func (ct *Cointop) Keyfn(fn func() error) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if fn != nil {
return fn()
}
return nil
return fn()
}
}

@ -1,7 +1,8 @@
package cointop
import (
log "github.com/sirupsen/logrus"
"fmt"
"strings"
)
// TODO: break up into small functions
@ -10,9 +11,9 @@ var lastWidth int
// layout sets initial layout
func (ct *Cointop) layout() error {
log.Debug("Layout()")
maxY := ct.Height()
maxX := ct.Width()
ct.debuglog("layout()")
maxY := ct.height()
maxX := ct.width()
topOffset := 0
headerHeight := 1
@ -58,7 +59,8 @@ func (ct *Cointop) layout() error {
} else {
if err := ct.ui.SetView(ct.Views.Marketbar, 0, topOffset-1, maxX, marketbarHeight+1); err != nil {
ct.Views.Marketbar.SetFrame(false)
ct.Views.Marketbar.SetStyle(ct.colorscheme.Style(ct.Views.Marketbar.Name()))
ct.Views.Marketbar.SetFgColor(ct.colorscheme.gocuiFgColor(ct.Views.Marketbar.Name()))
ct.Views.Marketbar.SetBgColor(ct.colorscheme.gocuiBgColor(ct.Views.Marketbar.Name()))
go func() {
ct.UpdateMarketbar()
_, found := ct.cache.Get(ct.Views.Marketbar.Name())
@ -91,10 +93,11 @@ func (ct *Cointop) layout() error {
if err := ct.ui.SetView(ct.Views.Chart, 0, chartTopOffset, maxX, topOffset+chartHeight); err != nil {
ct.Views.Chart.Clear()
ct.Views.Chart.SetFrame(false)
ct.Views.Chart.SetStyle(ct.colorscheme.Style(ct.Views.Chart.Name()))
ct.Views.Chart.SetFgColor(ct.colorscheme.gocuiFgColor(ct.Views.Chart.Name()))
ct.Views.Chart.SetBgColor(ct.colorscheme.gocuiBgColor(ct.Views.Chart.Name()))
go func() {
ct.UpdateChart()
cachekey := ct.CompositeCacheKey("globaldata", "", "", ct.State.selectedChartRange)
cachekey := strings.ToLower(fmt.Sprintf("%s_%s", "globaldata", strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
_, found := ct.cache.Get(cachekey)
if found {
ct.cache.Delete(cachekey)
@ -122,7 +125,8 @@ func (ct *Cointop) layout() error {
topOffset = topOffset + chartHeight
if err := ct.ui.SetView(ct.Views.TableHeader, tableOffsetX, topOffset-1, maxX, topOffset+1); err != nil {
ct.Views.TableHeader.SetFrame(false)
ct.Views.TableHeader.SetStyle(ct.colorscheme.Style(ct.Views.TableHeader.Name()))
ct.Views.TableHeader.SetFgColor(ct.colorscheme.gocuiFgColor(ct.Views.TableHeader.Name()))
ct.Views.TableHeader.SetBgColor(ct.colorscheme.gocuiBgColor(ct.Views.TableHeader.Name()))
go ct.UpdateTableHeader()
}
@ -130,7 +134,8 @@ func (ct *Cointop) layout() error {
if err := ct.ui.SetView(ct.Views.Table, tableOffsetX, topOffset-1, maxX, maxY-statusbarHeight); err != nil {
ct.Views.Table.SetFrame(false)
ct.Views.Table.SetHighlight(true)
ct.Views.Table.SetSelStyle(ct.colorscheme.Style("table_row_active"))
ct.Views.Table.SetSelFgColor(ct.colorscheme.gocuiFgColor("table_row_active"))
ct.Views.Table.SetSelBgColor(ct.colorscheme.gocuiBgColor("table_row_active"))
_, found := ct.cache.Get("allCoinsSlugMap")
if found {
ct.cache.Delete("allCoinsSlugMap")
@ -145,7 +150,8 @@ func (ct *Cointop) layout() error {
if !ct.State.hideStatusbar {
if err := ct.ui.SetView(ct.Views.Statusbar, 0, maxY-statusbarHeight-1, maxX, maxY); err != nil {
ct.Views.Statusbar.SetFrame(false)
ct.Views.Statusbar.SetStyle(ct.colorscheme.Style(ct.Views.Statusbar.Name()))
ct.Views.Statusbar.SetFgColor(ct.colorscheme.gocuiFgColor(ct.Views.Statusbar.Name()))
ct.Views.Statusbar.SetBgColor(ct.colorscheme.gocuiBgColor(ct.Views.Statusbar.Name()))
go ct.UpdateStatusbar("")
}
} else {
@ -161,19 +167,22 @@ func (ct *Cointop) layout() error {
ct.Views.SearchField.SetEditable(true)
ct.Views.SearchField.SetWrap(true)
ct.Views.SearchField.SetFrame(false)
ct.Views.SearchField.SetStyle(ct.colorscheme.Style("searchbar"))
ct.Views.SearchField.SetFgColor(ct.colorscheme.gocuiFgColor("searchbar"))
ct.Views.SearchField.SetBgColor(ct.colorscheme.gocuiBgColor("searchbar"))
}
if err := ct.ui.SetView(ct.Views.Menu, 1, 1, maxX-1, maxY-1); err != nil {
ct.Views.Menu.SetFrame(false)
ct.Views.Menu.SetStyle(ct.colorscheme.Style("menu"))
ct.Views.Menu.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.Menu.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
}
if err := ct.ui.SetView(ct.Views.Input, 3, 6, 30, 8); err != nil {
ct.Views.Input.SetFrame(true)
ct.Views.Input.SetEditable(true)
ct.Views.Input.SetWrap(true)
ct.Views.Input.SetStyle(ct.colorscheme.Style("menu"))
ct.Views.Input.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.Input.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
// run only once on init.
// this bit of code should be at the bottom

@ -4,18 +4,15 @@ import (
"sync"
"time"
"github.com/cointop-sh/cointop/pkg/api/types"
log "github.com/sirupsen/logrus"
types "github.com/miguelmota/cointop/pkg/api/types"
)
var (
coinslock sync.Mutex
updatecoinsmux sync.Mutex
)
var coinslock sync.Mutex
var updatecoinsmux sync.Mutex
// UpdateCoins updates coins view
func (ct *Cointop) UpdateCoins() error {
log.Debug("UpdateCoins()")
ct.debuglog("updateCoins()")
coinslock.Lock()
defer coinslock.Unlock()
cachekey := ct.CacheKey("allCoinsSlugMap")
@ -26,15 +23,12 @@ func (ct *Cointop) UpdateCoins() error {
if found {
// cache hit
allCoinsSlugMap, _ = cached.(map[string]types.Coin)
log.Debug("UpdateCoins() soft cache hit")
ct.debuglog("soft cache hit")
}
// cache miss or coin struct has been changed from the last time
isCacheMissed := allCoinsSlugMap == nil
currentCoinHash, _ := getStructHash(Coin{})
isCoinStructHashChanged := currentCoinHash != ct.config.CoinStructHash
if isCacheMissed || isCoinStructHashChanged {
log.Debug("UpdateCoins() cache miss or coin struct has changed")
// cache miss
if allCoinsSlugMap == nil {
ct.debuglog("cache miss")
ch := make(chan []types.Coin)
err = ct.api.GetAllCoinData(ct.State.currencyConversion, ch)
if err != nil {
@ -51,25 +45,9 @@ func (ct *Cointop) UpdateCoins() error {
return nil
}
// UpdateCurrentPageCoins updates all the coins in the current page
func (ct *Cointop) UpdateCurrentPageCoins() error {
log.Debugf("UpdateCurrentPageCoins(%d)", len(ct.State.coins))
currentPageCoins := make([]string, len(ct.State.coins))
for i, entry := range ct.State.coins {
currentPageCoins[i] = entry.Name
}
coins, err := ct.api.GetCoinDataBatch(currentPageCoins, ct.State.currencyConversion)
if err != nil {
return err
}
go ct.processCoins(coins)
return nil
}
// ProcessCoinsMap processes coins map
func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) {
log.Debug("ProcessCoinsMap()")
ct.debuglog("processCoinsMap()")
var coins []types.Coin
for _, v := range coinsMap {
@ -81,7 +59,7 @@ func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) {
// ProcessCoins processes coins list
func (ct *Cointop) processCoins(coins []types.Coin) {
log.Debug("ProcessCoins()")
ct.debuglog("processCoins()")
updatecoinsmux.Lock()
defer updatecoinsmux.Unlock()
@ -90,7 +68,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
for _, v := range coins {
k := v.Name
// Fix for https://github.com/cointop-sh/cointop/issues/59
// Fix for https://github.com/miguelmota/cointop/issues/59
// some APIs returns rank 0 for new coins
// or coins with low market cap data so we need to put them
// at the end of the list
@ -113,9 +91,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
PercentChange24H: v.PercentChange24H,
PercentChange7D: v.PercentChange7D,
PercentChange30D: v.PercentChange30D,
PercentChange1Y: v.PercentChange1Y,
LastUpdated: v.LastUpdated,
Slug: v.Slug,
})
if ilast != nil {
last, _ := ilast.(*Coin)
@ -136,7 +112,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
})
if len(ct.State.allCoins) < size {
var list []*Coin
list := []*Coin{}
for _, v := range coins {
k := v.Name
icoin, _ := ct.State.allCoinsSlugMap.Load(k)
@ -165,10 +141,8 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
c.PercentChange24H = cm.PercentChange24H
c.PercentChange7D = cm.PercentChange7D
c.PercentChange30D = cm.PercentChange30D
c.PercentChange1Y = cm.PercentChange1Y
c.LastUpdated = cm.LastUpdated
c.Favorite = cm.Favorite
c.Slug = cm.Slug
}
}
@ -177,14 +151,14 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
}
time.AfterFunc(10*time.Millisecond, func() {
ct.Sort(ct.State.viewSorts[ct.State.selectedView], ct.State.coins, true)
ct.Sort(ct.State.sortBy, ct.State.sortDesc, ct.State.coins, true)
ct.UpdateTable()
})
}
// GetListCount returns count of coins list
func (ct *Cointop) GetListCount() int {
log.Debug("GetListCount()")
ct.debuglog("getListCount()")
if ct.IsFavoritesVisible() {
return len(ct.State.favorites)
} else if ct.IsPortfolioVisible() {

@ -6,13 +6,11 @@ import (
"strings"
"time"
fcolor "github.com/fatih/color"
"github.com/cointop-sh/cointop/pkg/api/types"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
types "github.com/miguelmota/cointop/pkg/api/types"
"github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
)
// MarketbarView is structure for marketbar view
@ -20,18 +18,17 @@ type MarketbarView = ui.View
// NewMarketbarView returns a new marketbar view
func NewMarketbarView() *MarketbarView {
return ui.NewView("marketbar")
var view *MarketbarView = ui.NewView("marketbar")
return view
}
// UpdateMarketbar updates the market bar view
func (ct *Cointop) UpdateMarketbar() error {
log.Debug("UpdateMarketbar()")
maxX := ct.Width()
ct.debuglog("updateMarketbar()")
maxX := ct.width()
logo := "cointop"
if ct.colorschemeName == "cointop" {
Green := fcolor.New(fcolor.FgGreen).SprintFunc()
Cyan := fcolor.New(fcolor.FgCyan).SprintFunc()
logo = fmt.Sprintf("%s%s%s%s", Green(""), Cyan(""), Green(""), Cyan("cointop"))
logo = fmt.Sprintf("%s%s%s%s", color.Green(""), color.Cyan(""), color.Green(""), color.Cyan("cointop"))
}
var content string
@ -43,9 +40,6 @@ func (ct *Cointop) UpdateMarketbar() error {
total = math.Round(total*1e2) / 1e2
totalstr = humanize.Monetaryf(total, 2)
}
if ct.State.compactNotation {
totalstr = humanize.ScaleNumericf(total, 3)
}
timeframe := ct.State.selectedChartRange
chartname := ct.SelectedCoinName()
@ -59,7 +53,7 @@ func (ct *Cointop) UpdateMarketbar() error {
var percentChange24H float64
for _, p := range ct.GetPortfolioSlice() {
n := (p.Balance / total) * p.PercentChange24H
n := ((p.Balance / total) * p.PercentChange24H)
if math.IsNaN(n) {
continue
}
@ -76,7 +70,6 @@ func (ct *Cointop) UpdateMarketbar() error {
color24h = ct.colorscheme.MarketbarChangeDownSprintf()
arrow = "▼"
}
percentChange24Hstr := color24h(fmt.Sprintf("%.2f%%%s", percentChange24H, arrow))
chartInfo := ""
if !ct.State.hideChart {
@ -87,28 +80,21 @@ func (ct *Cointop) UpdateMarketbar() error {
)
}
totalstr = fmt.Sprintf("%s%s", ct.CurrencySymbol(), totalstr)
if ct.State.hidePortfolioBalances {
totalstr = HiddenBalanceChars
percentChange24Hstr = HiddenBalanceChars
}
content = fmt.Sprintf(
"%sTotal Portfolio Value %s: %s • 24H: %s",
"%sTotal Portfolio Value: %s • 24H: %s",
chartInfo,
ct.State.currencyConversion,
ct.colorscheme.MarketBarLabelActive(totalstr),
percentChange24Hstr,
ct.colorscheme.MarketBarLabelActive(fmt.Sprintf("%s%s", ct.CurrencySymbol(), totalstr)),
color24h(fmt.Sprintf("%.2f%%%s", percentChange24H, arrow)),
)
} else {
ct.State.marketBarHeight = 1
if ct.Width() < 125 {
if ct.width() < 125 {
ct.State.marketBarHeight = 2
}
var market types.GlobalMarketData
var err error
cachekey := ct.CompositeCacheKey("market", "", ct.State.currencyConversion, "")
cachekey := ct.CacheKey("market")
cached, found := ct.cache.Get(cachekey)
if found {
@ -116,7 +102,7 @@ func (ct *Cointop) UpdateMarketbar() error {
var ok bool
market, ok = cached.(types.GlobalMarketData)
if ok {
log.Debug("UpdateMarketbar() soft cache hit")
ct.debuglog("soft cache hit")
}
}
@ -145,7 +131,7 @@ func (ct *Cointop) UpdateMarketbar() error {
chartInfo := ""
if !ct.State.hideChart {
chartInfo = fmt.Sprintf(
"[ Chart: %s %s] ",
"[ Chart: %s %s ] ",
ct.colorscheme.MarketBarLabelActive(chartname),
timeframe,
)
@ -154,26 +140,18 @@ func (ct *Cointop) UpdateMarketbar() error {
separator1 := "•"
separator2 := "•"
offset := strings.Repeat(" ", 12)
if ct.Width() < 105 {
if ct.width() < 105 {
separator1 = "\n" + offset
} else if ct.Width() < 125 {
} else if ct.width() < 125 {
separator2 = "\n" + offset
}
marketCapStr := humanize.Monetaryf(market.TotalMarketCapUSD, 0)
volumeStr := humanize.Monetaryf(market.Total24HVolumeUSD, 0)
if ct.State.compactNotation {
marketCapStr = humanize.ScaleNumericf(market.TotalMarketCapUSD, 3)
volumeStr = humanize.ScaleNumericf(market.Total24HVolumeUSD, 3)
}
content = fmt.Sprintf(
"%sGlobal %s ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%",
"%sGlobal ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%",
chartInfo,
ct.State.currencyConversion,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), marketCapStr),
fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.TotalMarketCapUSD, 0)),
separator1,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), volumeStr),
fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.Total24HVolumeUSD, 0)),
separator2,
market.BitcoinPercentageOfMarketCap,
)

@ -1,20 +1,18 @@
package cointop
import (
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
import "github.com/miguelmota/cointop/pkg/ui"
// MenuView is structure for menu view
type MenuView = ui.View
// NewMenuView returns a new menu view
func NewMenuView() *MenuView {
return ui.NewView("menu")
var view *MenuView = ui.NewView("menu")
return view
}
// HideMenu hides the menu view
func (ct *Cointop) HideMenu() error {
log.Debug("HideMenu()")
ct.debuglog("hideMenu()")
return nil
}

@ -2,31 +2,29 @@ package cointop
import (
"math"
log "github.com/sirupsen/logrus"
)
// CurrentPage returns the current page
func (ct *Cointop) CurrentPage() int {
log.Debug("CurrentPage()")
ct.debuglog("currentPage()")
return ct.State.page + 1
}
// CurrentDisplayPage returns the current page in human readable format
func (ct *Cointop) CurrentDisplayPage() int {
log.Debug("CurrentDisplayPage()")
ct.debuglog("currentDisplayPage()")
return ct.State.page + 1
}
// TotalPages returns the number of total pages
func (ct *Cointop) TotalPages() int {
log.Debug("TotalPages()")
ct.debuglog("totalPages()")
return ct.GetListCount() / ct.State.perPage
}
// TotalPagesDisplay returns the number of total pages in human readable format
func (ct *Cointop) TotalPagesDisplay() int {
log.Debug("TotalPagesDisplay()")
ct.debuglog("totalPagesDisplay()")
return ct.TotalPages() + 1
}
@ -37,7 +35,7 @@ func (ct *Cointop) TotalPerPage() int {
// SetPage navigates to the selected page
func (ct *Cointop) SetPage(page int) int {
log.Debug("SetPage()")
ct.debuglog("setPage()")
if (page*ct.State.perPage) < ct.GetListCount() && page >= 0 {
ct.State.page = page
}
@ -46,7 +44,7 @@ func (ct *Cointop) SetPage(page int) int {
// CursorDown moves the cursor one row down
func (ct *Cointop) CursorDown() error {
log.Debug("CursorDown()")
ct.debuglog("cursorDown()")
// return if already at the bottom
if ct.IsLastRow() {
return nil
@ -71,7 +69,7 @@ func (ct *Cointop) CursorDown() error {
// CursorUp moves the cursor one row up
func (ct *Cointop) CursorUp() error {
log.Debug("CursorUp()")
ct.debuglog("cursorUp()")
// return if already at the top
if ct.IsFirstRow() {
return nil
@ -96,7 +94,7 @@ func (ct *Cointop) CursorUp() error {
// PageDown moves the cursor one page down
func (ct *Cointop) PageDown() error {
log.Debug("PageDown()")
ct.debuglog("pageDown()")
// return if already at the bottom
if ct.IsLastRow() {
return nil
@ -133,7 +131,7 @@ func (ct *Cointop) PageDown() error {
// PageUp moves the cursor one page up
func (ct *Cointop) PageUp() error {
log.Debug("PageUp()")
ct.debuglog("pageUp()")
// return if already at the top
if ct.IsFirstRow() {
return nil
@ -161,7 +159,7 @@ func (ct *Cointop) PageUp() error {
// NavigateFirstLine moves the cursor to the first row of the table
func (ct *Cointop) NavigateFirstLine() error {
log.Debug("NavigateFirstLine()")
ct.debuglog("navigateFirstLine()")
// return if already at the top
if ct.IsFirstRow() {
return nil
@ -182,7 +180,7 @@ func (ct *Cointop) NavigateFirstLine() error {
// NavigateLastLine moves the cursor to the last row of the table
func (ct *Cointop) NavigateLastLine() error {
log.Debug("NavigateLastLine()")
ct.debuglog("navigateLastLine()")
// return if already at the bottom
if ct.IsLastRow() {
return nil
@ -211,7 +209,7 @@ func (ct *Cointop) NavigateLastLine() error {
// NavigatePageFirstLine moves the cursor to the visible first row of the table
func (ct *Cointop) NavigatePageFirstLine() error {
log.Debug("NavigatePageFirstLine()")
ct.debuglog("navigatePageFirstLine()")
// return if already at the correct line
if ct.IsPageFirstLine() {
return nil
@ -227,7 +225,7 @@ func (ct *Cointop) NavigatePageFirstLine() error {
// NavigatePageMiddleLine moves the cursor to the visible middle row of the table
func (ct *Cointop) NavigatePageMiddleLine() error {
log.Debug("NavigatePageMiddleLine()")
ct.debuglog("navigatePageMiddleLine()")
// return if already at the correct line
if ct.IsPageMiddleLine() {
return nil
@ -244,7 +242,7 @@ func (ct *Cointop) NavigatePageMiddleLine() error {
// NavigatePageLastLine moves the cursor to the visible last row of the table
func (ct *Cointop) navigatePageLastLine() error {
log.Debug("NavigatePageLastLine()")
ct.debuglog("navigatePageLastLine()")
// return if already at the correct line
if ct.IsPageLastLine() {
return nil
@ -261,7 +259,7 @@ func (ct *Cointop) navigatePageLastLine() error {
// NextPage navigates to the next page
func (ct *Cointop) NextPage() error {
log.Debug("NextPage()")
ct.debuglog("nextPage()")
// return if already at the last page
if ct.IsLastPage() {
@ -276,7 +274,7 @@ func (ct *Cointop) NextPage() error {
// PrevPage navigates to the previous page
func (ct *Cointop) PrevPage() error {
log.Debug("PrevPage()")
ct.debuglog("prevPage()")
// return if already at the first page
if ct.IsFirstPage() {
@ -291,7 +289,7 @@ func (ct *Cointop) PrevPage() error {
// NextPageTop navigates to the first row of the next page
func (ct *Cointop) nextPageTop() error {
log.Debug("NextPageTop()")
ct.debuglog("nextPageTop()")
ct.NextPage()
ct.NavigateFirstLine()
@ -301,7 +299,7 @@ func (ct *Cointop) nextPageTop() error {
// PrevPageTop navigates to the first row of the previous page
func (ct *Cointop) PrevPageTop() error {
log.Debug("PrevtPageTop()")
ct.debuglog("prevtPageTop()")
ct.PrevPage()
ct.NavigateLastLine()
@ -309,16 +307,9 @@ func (ct *Cointop) PrevPageTop() error {
return nil
}
// NavigateToFirstPageFirstRow navigates to the first row on the first page
func (ct *Cointop) NavigateToFirstPageFirstRow() error {
log.Debug("TopCoin()")
ct.GoToGlobalIndex(0)
return nil
}
// FirstPage navigates to the first page
func (ct *Cointop) FirstPage() error {
log.Debug("FirstPage()")
ct.debuglog("firstPage()")
// return if already at the first page
if ct.IsFirstPage() {
@ -333,7 +324,7 @@ func (ct *Cointop) FirstPage() error {
// LastPage navigates to the last page
func (ct *Cointop) LastPage() error {
log.Debug("LastPage()")
ct.debuglog("lastPage()")
// return if already at the last page
if ct.IsLastPage() {
@ -348,7 +339,7 @@ func (ct *Cointop) LastPage() error {
// IsFirstRow returns true if cursor is on first row
func (ct *Cointop) IsFirstRow() bool {
log.Debug("IsFirstRow()")
ct.debuglog("isFirstRow()")
oy := ct.Views.Table.OriginY()
cy := ct.Views.Table.CursorY()
return (cy + oy) == 0
@ -356,7 +347,7 @@ func (ct *Cointop) IsFirstRow() bool {
// IsLastRow returns true if cursor is on last row
func (ct *Cointop) IsLastRow() bool {
log.Debug("IsLastRow()")
ct.debuglog("isLastRow()")
oy := ct.Views.Table.OriginY()
cy := ct.Views.Table.CursorY()
numRows := ct.TableRowsLen() - 1
@ -365,19 +356,19 @@ func (ct *Cointop) IsLastRow() bool {
// IsFirstPage returns true if cursor is on the first page
func (ct *Cointop) IsFirstPage() bool {
log.Debug("IsFirstPage()")
ct.debuglog("isFirstPage()")
return ct.State.page == 0
}
// IsLastPage returns true if cursor is on the last page
func (ct *Cointop) IsLastPage() bool {
log.Debug("IsLastPage()")
ct.debuglog("isLastPage()")
return ct.State.page == ct.TotalPages()-1
}
// IsPageFirstLine returns true if the cursor is on the visible first row
func (ct *Cointop) IsPageFirstLine() bool {
log.Debug("IsPageFirstLine()")
ct.debuglog("isPageFirstLine()")
cy := ct.Views.Table.CursorY()
return cy == 0
@ -385,7 +376,7 @@ func (ct *Cointop) IsPageFirstLine() bool {
// IsPageMiddleLine returns true if the cursor is on the visible middle row
func (ct *Cointop) IsPageMiddleLine() bool {
log.Debug("IsPageMiddleLine()")
ct.debuglog("isPageMiddleLine()")
cy := ct.Views.Table.CursorY()
sy := ct.Views.Table.Height()
return (sy/2)-1 == cy
@ -393,7 +384,7 @@ func (ct *Cointop) IsPageMiddleLine() bool {
// IsPageLastLine returns true if the cursor is on the visible last row
func (ct *Cointop) IsPageLastLine() bool {
log.Debug("IsPageLastLine()")
ct.debuglog("isPageLastLine()")
cy := ct.Views.Table.CursorY()
sy := ct.Views.Table.Height()
@ -402,7 +393,7 @@ func (ct *Cointop) IsPageLastLine() bool {
// GoToPageRowIndex navigates to the selected row index of the page
func (ct *Cointop) GoToPageRowIndex(idx int) error {
log.Debug("GoToPageRowIndex()")
ct.debuglog("goToPageRowIndex()")
if idx < 0 {
idx = 0
}
@ -416,18 +407,13 @@ func (ct *Cointop) GoToPageRowIndex(idx int) error {
// GoToGlobalIndex navigates to the selected row index of all page rows
func (ct *Cointop) GoToGlobalIndex(idx int) error {
log.Debugf("GoToGlobalIndex(%d)", idx)
target := ct.State.allCoins[idx]
ct.debuglog("goToGlobalIndex()")
l := ct.TableRowsLen()
atpage := idx / l
ct.SetPage(atpage)
rowIndex := (idx % l)
ct.HighlightRow(rowIndex)
ct.UpdateTable()
// Look for the coin in the current page
for i, coin := range ct.State.coins {
if coin == target {
ct.HighlightRow(i)
}
}
return nil
}
@ -436,7 +422,7 @@ func (ct *Cointop) HighlightRow(pageRowIndex int) error {
if pageRowIndex < 0 {
pageRowIndex = 0
}
log.Debug("HighlightRow()")
ct.debuglog("highlightRow()")
ct.Views.Table.SetOrigin(0, 0)
ct.Views.Table.SetCursor(0, 0)
ox := ct.Views.Table.OriginX()
@ -455,7 +441,7 @@ func (ct *Cointop) HighlightRow(pageRowIndex int) error {
cy = h - (l - pageRowIndex)
}
}
log.Debugf("HighlightRow idx:%v h:%v cy:%v oy:%v", pageRowIndex, h, cy, oy)
ct.debuglog("highlightRow idx:%v h:%v cy:%v oy:%v", pageRowIndex, h, cy, oy)
ct.Views.Table.SetOrigin(ox, oy)
ct.Views.Table.SetCursor(cx, cy)
return nil
@ -463,7 +449,7 @@ func (ct *Cointop) HighlightRow(pageRowIndex int) error {
// GoToCoinRow navigates to the row of the matched coin
func (ct *Cointop) GoToCoinRow(coin *Coin) error {
log.Debug("GoToCoinRow()")
ct.debuglog("goToCoinRow()")
if coin == nil {
return nil
}
@ -497,7 +483,7 @@ func (ct *Cointop) GetCoinRowIndex(coin *Coin) int {
// CursorDownOrNextPage moves the cursor down one row or goes to the next page if cursor is on the last row
func (ct *Cointop) CursorDownOrNextPage() error {
log.Debug("CursorDownOrNextPage()")
ct.debuglog("CursorDownOrNextPage()")
if ct.IsLastRow() {
if ct.IsLastPage() {
return nil
@ -519,7 +505,7 @@ func (ct *Cointop) CursorDownOrNextPage() error {
// CursorUpOrPreviousPage moves the cursor up one row or goes to the previous page if cursor is on the first row
func (ct *Cointop) CursorUpOrPreviousPage() error {
log.Debug("CursorUpOrPreviousPage()")
ct.debuglog("CursorUpOrPreviousPage()")
if ct.IsFirstRow() {
if ct.IsFirstPage() {
return nil
@ -549,10 +535,10 @@ func (ct *Cointop) TableScrollLeft() error {
return nil
}
// TableScrollRight scrolls the table to the right
// TableScrollRight scrolls the the table to the right
func (ct *Cointop) TableScrollRight() error {
ct.State.tableOffsetX--
maxX := int(math.Min(float64(1-(ct.maxTableWidth-ct.Width())), 0))
maxX := int(math.Min(float64(1-(ct.maxTableWidth-ct.width())), 0))
if ct.State.tableOffsetX <= maxX {
ct.State.tableOffsetX = maxX
}
@ -560,14 +546,39 @@ func (ct *Cointop) TableScrollRight() error {
return nil
}
// MouseRelease is called on mouse releae event
func (ct *Cointop) MouseRelease() error {
return nil
}
// MouseLeftClick is called on mouse left click event
func (ct *Cointop) MouseLeftClick() error {
return ct.g.SetCursorFromCurrentMouseEvent()
return nil
}
// MouseMiddleClick is called on mouse middle click event
func (ct *Cointop) MouseMiddleClick() error {
return nil
}
// MouseRightClick is called on mouse right click event
func (ct *Cointop) MouseRightClick() error {
return ct.OpenLink()
}
// MouseWheelUp is called on mouse wheel up event
func (ct *Cointop) MouseWheelUp() error {
return nil
}
// MouseWheelDown is called on mouse wheel down event
func (ct *Cointop) MouseWheelDown() error {
return nil
}
// TableRowsLen returns the number of table row entries
func (ct *Cointop) TableRowsLen() int {
log.Debug("TableRowsLen()")
ct.debuglog("TableRowsLen()")
if ct.IsFavoritesVisible() {
return ct.FavoritesLen()
}

@ -12,12 +12,10 @@ import (
"time"
"unicode/utf8"
"github.com/cointop-sh/cointop/pkg/asciitable"
"github.com/cointop-sh/cointop/pkg/eval"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/table"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/asciitable"
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table"
)
// SupportedPortfolioTableHeaders are all the supported portfolio table header columns
@ -32,13 +30,8 @@ var SupportedPortfolioTableHeaders = []string{
"24h_change",
"7d_change",
"30d_change",
"1y_change",
"percent_holdings",
"last_updated",
"cost_price",
"cost",
"pnl",
"pnl_percent",
}
// DefaultPortfolioTableHeaders are the default portfolio table header columns
@ -53,23 +46,9 @@ var DefaultPortfolioTableHeaders = []string{
"24h_change",
"7d_change",
"percent_holdings",
"cost_price",
"cost",
"pnl",
"pnl_percent",
"last_updated",
}
// HiddenBalanceChars are the characters to show when hidding balances
var HiddenBalanceChars = "********"
var costColumns = map[string]bool{
"cost_price": true,
"cost": true,
"pnl": true,
"pnl_percent": true,
}
// ValidPortfolioTableHeader returns the portfolio table headers
func (ct *Cointop) ValidPortfolioTableHeader(name string) bool {
for _, v := range SupportedPortfolioTableHeaders {
@ -89,31 +68,12 @@ func (ct *Cointop) GetPortfolioTableHeaders() []string {
// GetPortfolioTable returns the table for displaying portfolio holdings
func (ct *Cointop) GetPortfolioTable() *table.Table {
total := ct.GetPortfolioTotal()
maxX := ct.Width()
maxX := ct.width()
t := table.NewTable().SetWidth(maxX)
var rows [][]*table.RowCell
headers := ct.GetPortfolioTableHeaders()
ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
displayCostColumns := false
for _, coin := range ct.State.coins {
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
displayCostColumns = true
break
}
}
if !displayCostColumns {
filtered := make([]string, 0)
for _, header := range headers {
if _, ok := costColumns[header]; !ok {
filtered = append(filtered, header)
}
}
headers = filtered
}
ct.ClearSyncMap(ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft)
for _, coin := range ct.State.coins {
leftMargin := 1
rightMargin := 1
@ -123,7 +83,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
case "rank":
star := ct.colorscheme.TableRow(" ")
if coin.Favorite {
star = ct.colorscheme.TableRowFavorite(ct.State.favoriteChar)
star = ct.colorscheme.TableRowFavorite("*")
}
rank := fmt.Sprintf("%s%v", star, ct.colorscheme.TableRow(fmt.Sprintf("%6v ", coin.Rank)))
ct.SetTableColumnWidth(header, 8)
@ -165,7 +125,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
Text: symbol,
})
case "price":
text := ct.FormatPrice(coin.Price)
text := humanize.Monetaryf(coin.Price, 2)
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
@ -179,9 +139,6 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
})
case "holdings":
text := strconv.FormatFloat(coin.Holdings, 'f', -1, 64)
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -194,9 +151,6 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
})
case "balance":
text := humanize.Monetaryf(coin.Balance, 2)
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
colorBalance := ct.colorscheme.TableColumnPrice
@ -284,34 +238,12 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
Color: color30d,
Text: text,
})
case "1y_change":
color1y := ct.colorscheme.TableColumnChange
if coin.PercentChange1Y > 0 {
color1y = ct.colorscheme.TableColumnChangeUp
}
if coin.PercentChange1Y < 0 {
color1y = ct.colorscheme.TableColumnChangeDown
}
text := fmt.Sprintf("%.2f%%", coin.PercentChange1Y)
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: color1y,
Text: text,
})
case "percent_holdings":
percentHoldings := (coin.Balance / total) * 1e2
if math.IsNaN(percentHoldings) {
percentHoldings = 0
}
text := fmt.Sprintf("%.2f%%", percentHoldings)
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -324,7 +256,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
})
case "last_updated":
unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
lastUpdated := humanize.FormatTime(time.Unix(unix, 0), "15:04:05 Jan 02")
lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02")
ct.SetTableColumnWidthFromString(header, lastUpdated)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -335,117 +267,6 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
Color: ct.colorscheme.TableRow,
Text: lastUpdated,
})
case "cost_price":
text := fmt.Sprintf("%s %s", coin.BuyCurrency, ct.FormatPrice(coin.BuyPrice))
if coin.BuyPrice == 0.0 || coin.BuyCurrency == "" {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: ct.colorscheme.TableRow,
Text: text,
})
case "cost":
cost := 0.0
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
cost = costPrice * coin.Holdings
}
}
text := humanize.FixedMonetaryf(cost, 2)
if coin.BuyPrice == 0.0 {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: ct.colorscheme.TableColumnPrice,
Text: text,
})
case "pnl":
text := ""
colorProfit := ct.colorscheme.TableColumnChange
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
profit := (coin.Price - costPrice) * coin.Holdings
text = humanize.FixedMonetaryf(profit, 2)
if profit > 0 {
colorProfit = ct.colorscheme.TableColumnChangeUp
} else if profit < 0 {
colorProfit = ct.colorscheme.TableColumnChangeDown
}
} else {
text = "?"
}
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
colorProfit = ct.colorscheme.TableColumnChange
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: colorProfit,
Text: text,
})
case "pnl_percent":
profitPercent := 0.0
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
profitPercent = 100 * (coin.Price/costPrice - 1)
}
}
colorProfit := ct.colorscheme.TableColumnChange
if profitPercent > 0 {
colorProfit = ct.colorscheme.TableColumnChangeUp
} else if profitPercent < 0 {
colorProfit = ct.colorscheme.TableColumnChangeDown
}
text := fmt.Sprintf("%.2f%%", profitPercent)
if coin.BuyPrice == 0.0 {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
colorProfit = ct.colorscheme.TableColumnChange
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: colorProfit,
Text: text,
})
}
}
@ -464,7 +285,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
// TogglePortfolio toggles the portfolio view
func (ct *Cointop) TogglePortfolio() error {
log.Debug("TogglePortfolio()")
ct.debuglog("togglePortfolio()")
ct.ToggleSelectedView(PortfolioView)
go ct.UpdateChart()
go ct.UpdateTable()
@ -473,7 +294,7 @@ func (ct *Cointop) TogglePortfolio() error {
// ToggleShowPortfolio shows the portfolio view
func (ct *Cointop) ToggleShowPortfolio() error {
log.Debug("ToggleShowPortfolio()")
ct.debuglog("toggleShowPortfolio()")
ct.SetSelectedView(PortfolioView)
go ct.UpdateChart()
go ct.UpdateTable()
@ -482,7 +303,7 @@ func (ct *Cointop) ToggleShowPortfolio() error {
// TogglePortfolioUpdateMenu toggles the portfolio update menu
func (ct *Cointop) TogglePortfolioUpdateMenu() error {
log.Debug("TogglePortfolioUpdateMenu()")
ct.debuglog("togglePortfolioUpdateMenu()")
if ct.IsPriceAlertsVisible() {
return ct.ShowPriceAlertsUpdateMenu()
}
@ -502,11 +323,11 @@ func (ct *Cointop) CoinHoldings(coin *Coin) float64 {
// UpdatePortfolioUpdateMenu updates the portfolio update menu view
func (ct *Cointop) UpdatePortfolioUpdateMenu() error {
log.Debug("UpdatePortfolioUpdateMenu()")
ct.debuglog("updatePortfolioUpdateMenu()")
coin := ct.HighlightedRowCoin()
exists := ct.PortfolioEntryExists(coin)
value := strconv.FormatFloat(ct.CoinHoldings(coin), 'f', -1, 64)
log.Debugf("UpdatePortfolioUpdateMenu() holdings %v", value)
ct.debuglog("holdings %v", value)
var mode string
var current string
var submitText string
@ -518,7 +339,7 @@ func (ct *Cointop) UpdatePortfolioUpdateMenu() error {
mode = "Add"
submitText = "Add"
}
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" %s Portfolio Entry %s\n\n", mode, pad.Left("[q] close ", ct.Width()-25, " ")))
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" %s Portfolio Entry %s\n\n", mode, pad.Left("[q] close ", ct.width()-25, " ")))
label := fmt.Sprintf(" Enter holdings for %s %s", ct.colorscheme.MenuLabel(coin.Name), current)
content := fmt.Sprintf("%s\n%s\n\n%s%s\n\n\n [Enter] %s [ESC] Cancel", header, label, strings.Repeat(" ", 29), coin.Symbol, submitText)
@ -534,7 +355,7 @@ func (ct *Cointop) UpdatePortfolioUpdateMenu() error {
// ShowPortfolioUpdateMenu shows the portfolio update menu
func (ct *Cointop) ShowPortfolioUpdateMenu() error {
log.Debug("ShowPortfolioUpdateMenu()")
ct.debuglog("showPortfolioUpdateMenu()")
// TODO: separation of concerns
if ct.IsPriceAlertsVisible() {
@ -558,7 +379,7 @@ func (ct *Cointop) ShowPortfolioUpdateMenu() error {
// HidePortfolioUpdateMenu hides the portfolio update menu
func (ct *Cointop) HidePortfolioUpdateMenu() error {
log.Debug("HidePortfolioUpdateMenu()")
ct.debuglog("hidePortfolioUpdateMenu()")
ct.State.portfolioUpdateMenuVisible = false
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.ui.SetViewOnBottom(ct.Views.Input)
@ -576,7 +397,7 @@ func (ct *Cointop) HidePortfolioUpdateMenu() error {
// SetPortfolioHoldings sets portfolio entry holdings from inputed value
func (ct *Cointop) SetPortfolioHoldings() error {
log.Debug("SetPortfolioHoldings()")
ct.debuglog("setPortfolioHoldings()")
defer ct.HidePortfolioUpdateMenu()
coin := ct.HighlightedRowCoin()
if coin == nil {
@ -593,20 +414,19 @@ func (ct *Cointop) SetPortfolioHoldings() error {
return nil
}
input := string(b[:n])
holdings, err := eval.EvaluateExpressionToFloat64(input, coin)
if err != nil {
// leave value as is if expression can't be evaluated
return nil
}
shouldDelete := holdings == 0
value := normalizeFloatString(string(b), true)
shouldDelete := value == ""
var holdings float64
// TODO: add fields to form, parse here
buyPrice := 0.0
buyCurrency := ""
if !shouldDelete {
holdings, err = strconv.ParseFloat(value, 64)
if err != nil {
return err
}
}
idx := ct.GetPortfolioCoinIndex(coin)
if err := ct.SetPortfolioEntry(coin.Name, holdings, buyPrice, buyCurrency); err != nil {
if err := ct.SetPortfolioEntry(coin.Name, holdings); err != nil {
return err
}
@ -631,7 +451,7 @@ func (ct *Cointop) SetPortfolioHoldings() error {
// PortfolioEntry returns a portfolio entry
func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
// log.Debug("PortfolioEntry()") // too many
//ct.debuglog("portfolioEntry()") // too many
if c == nil {
return &PortfolioEntry{}, true
}
@ -641,29 +461,31 @@ func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
var ok bool
key := strings.ToLower(c.Name)
if p, ok = ct.State.portfolio.Entries[key]; !ok {
p = &PortfolioEntry{
Coin: c.Name,
Holdings: 0,
// NOTE: if not found then try the symbol
key := strings.ToLower(c.Symbol)
if p, ok = ct.State.portfolio.Entries[key]; !ok {
p = &PortfolioEntry{
Coin: c.Name,
Holdings: 0,
}
isNew = true
}
isNew = true
}
return p, isNew
}
// SetPortfolioEntry sets a portfolio entry
func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64, buyPrice float64, buyCurrency string) error {
log.Debug("SetPortfolioEntry()")
func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
ct.debuglog("setPortfolioEntry()")
ic, _ := ct.State.allCoinsSlugMap.Load(strings.ToLower(coin))
c, _ := ic.(*Coin)
p, isNew := ct.PortfolioEntry(c)
if isNew {
key := strings.ToLower(coin)
ct.State.portfolio.Entries[key] = &PortfolioEntry{
Coin: coin,
Holdings: holdings,
BuyPrice: buyPrice,
BuyCurrency: buyCurrency,
Coin: coin,
Holdings: holdings,
}
} else {
p.Holdings = holdings
@ -678,7 +500,7 @@ func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64, buyPrice flo
// RemovePortfolioEntry removes a portfolio entry
func (ct *Cointop) RemovePortfolioEntry(coin string) error {
log.Debug("RemovePortfolioEntry()")
ct.debuglog("removePortfolioEntry()")
delete(ct.State.portfolio.Entries, strings.ToLower(coin))
if err := ct.Save(); err != nil {
return err
@ -688,35 +510,32 @@ func (ct *Cointop) RemovePortfolioEntry(coin string) error {
// PortfolioEntryExists returns true if portfolio entry exists
func (ct *Cointop) PortfolioEntryExists(c *Coin) bool {
log.Debug("PortfolioEntryExists()")
ct.debuglog("portfolioEntryExists()")
_, isNew := ct.PortfolioEntry(c)
return !isNew
}
// PortfolioEntriesCount returns the count of portfolio entries
func (ct *Cointop) PortfolioEntriesCount() int {
log.Debug("PortfolioEntriesCount()")
ct.debuglog("portfolioEntriesCount()")
return len(ct.State.portfolio.Entries)
}
// GetPortfolioSlice returns portfolio entries as a slice
func (ct *Cointop) GetPortfolioSlice() []*Coin {
log.Debug("GetPortfolioSlice()")
var sliced []*Coin
ct.debuglog("getPortfolioSlice()")
sliced := []*Coin{}
if ct.PortfolioEntriesCount() == 0 {
return sliced
}
for _, p := range ct.State.portfolio.Entries {
coinIfc, _ := ct.State.allCoinsSlugMap.Load(p.Coin)
coin, ok := coinIfc.(*Coin)
if !ok {
log.Errorf("Could not find coin %s", p.Coin)
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
p, isNew := ct.PortfolioEntry(coin)
if isNew {
continue
}
coin.Holdings = p.Holdings
coin.BuyPrice = p.BuyPrice
coin.BuyCurrency = p.BuyCurrency
balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance)
if ct.State.currencyConversion == "ETH" || ct.State.currencyConversion == "BTC" {
@ -727,7 +546,7 @@ func (ct *Cointop) GetPortfolioSlice() []*Coin {
sliced = append(sliced, coin)
}
sort.SliceStable(sliced, func(i, j int) bool {
sort.Slice(sliced, func(i, j int) bool {
return sliced[i].Balance > sliced[j].Balance
})
@ -740,7 +559,7 @@ func (ct *Cointop) GetPortfolioSlice() []*Coin {
// GetPortfolioTotal returns the total balance of portfolio entries
func (ct *Cointop) GetPortfolioTotal() float64 {
log.Debug("GetPortfolioTotal()")
ct.debuglog("getPortfolioTotal()")
portfolio := ct.GetPortfolioSlice()
var total float64
for _, p := range portfolio {
@ -751,7 +570,7 @@ func (ct *Cointop) GetPortfolioTotal() float64 {
// RefreshPortfolioCoins refreshes portfolio entry coin data
func (ct *Cointop) RefreshPortfolioCoins() error {
log.Debug("RefreshPortfolioCoins()")
ct.debuglog("refreshPortfolioCoins()")
holdings := ct.GetPortfolioSlice()
holdingCoins := make([]string, len(holdings))
for i, entry := range holdings {
@ -778,7 +597,6 @@ type TablePrintOptions struct {
Convert string
NoHeader bool
PercentChange24H bool
HideBalances bool
}
// outputFormats is list of valid output formats
@ -800,7 +618,7 @@ var portfolioColumns = map[string]bool{
// PrintHoldingsTable prints the holdings in an ASCII table
func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
log.Debug("PrintHoldingsTable()")
ct.debuglog("printHoldingsTable()")
if options == nil {
options = &TablePrintOptions{}
}
@ -819,7 +637,6 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
filterCols := options.Cols
holdings := ct.GetPortfolioSlice()
noHeader := options.NoHeader
hideBalances := options.HideBalances
if format == "" {
format = "table"
@ -830,7 +647,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
return fmt.Errorf("the option %q is not a valid column name", sortBy)
}
ct.Sort(&sortConstraint{sortBy: sortBy, sortDesc: sortDesc}, holdings, true)
ct.Sort(sortBy, sortDesc, holdings, true)
}
if _, ok := outputFormats[format]; !ok {
@ -841,7 +658,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
records := make([][]string, len(holdings))
symbol := ct.CurrencySymbol()
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings", "cost_price", "cost", "pnl", "pnl_percent"}
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"}
if len(filterCols) > 0 {
for _, col := range filterCols {
valid := false
@ -901,7 +718,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
item[i] = entry.Symbol
case "price":
if humanReadable {
item[i] = fmt.Sprintf("%s%s", symbol, ct.FormatPrice(entry.Price))
item[i] = fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Price, 2))
} else {
item[i] = strconv.FormatFloat(entry.Price, 'f', -1, 64)
}
@ -911,18 +728,12 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
} else {
item[i] = strconv.FormatFloat(entry.Holdings, 'f', -1, 64)
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "balance":
if humanReadable {
item[i] = fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Balance, 2))
} else {
item[i] = strconv.FormatFloat(entry.Balance, 'f', -1, 64)
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "24h%":
if humanReadable {
item[i] = fmt.Sprintf("%s%%", humanize.Numericf(entry.PercentChange24H, 2))
@ -935,73 +746,6 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
} else {
item[i] = fmt.Sprintf("%.2f", percentHoldings)
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "cost_price":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
if humanReadable {
item[i] = fmt.Sprintf("%s %s", entry.BuyCurrency, ct.FormatPrice(entry.BuyPrice))
} else {
item[i] = fmt.Sprintf("%s %s", entry.BuyCurrency, strconv.FormatFloat(entry.BuyPrice, 'f', -1, 64))
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "cost":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
cost := costPrice * entry.Holdings
if humanReadable {
item[i] = fmt.Sprintf("%s%s", symbol, humanize.FixedMonetaryf(cost, 2))
} else {
item[i] = strconv.FormatFloat(cost, 'f', -1, 64)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "pnl":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
profit := (entry.Price - costPrice) * entry.Holdings
if humanReadable {
// TODO: if <0 "£-3.71" should be "-£3.71"?
item[i] = fmt.Sprintf("%s%s", symbol, humanize.FixedMonetaryf(profit, 2))
} else {
item[i] = strconv.FormatFloat(profit, 'f', -1, 64)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "pnl_percent":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
profitPercent := 100 * (entry.Price/costPrice - 1)
if humanReadable {
item[i] = fmt.Sprintf("%s%%", humanize.Numericf(profitPercent, 2))
} else {
item[i] = fmt.Sprintf("%.2f", profitPercent)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
}
}
records[i] = item
@ -1073,7 +817,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
// PrintHoldingsTotal prints the total holdings amount
func (ct *Cointop) PrintHoldingsTotal(options *TablePrintOptions) error {
log.Debug("PrintHoldingsTotal()")
ct.debuglog("PrintHoldingsTotal()")
if options == nil {
options = &TablePrintOptions{}
}
@ -1147,7 +891,7 @@ func (ct *Cointop) PrintHoldingsTotal(options *TablePrintOptions) error {
// PrintHoldings24HChange prints the total holdings amount
func (ct *Cointop) PrintHoldings24HChange(options *TablePrintOptions) error {
log.Debug("PrintHoldings24HChange()")
ct.debuglog("PrintHoldings24HChange()")
if options == nil {
options = &TablePrintOptions{}
}
@ -1179,16 +923,19 @@ func (ct *Cointop) PrintHoldings24HChange(options *TablePrintOptions) error {
}
}
n := (entry.Balance / total) * entry.PercentChange24H
n := ((entry.Balance / total) * entry.PercentChange24H)
if math.IsNaN(n) {
continue
}
percentChange24H += n
}
value := fmt.Sprintf("%.2f", percentChange24H)
value := strconv.FormatFloat(percentChange24H, 'f', -1, 64)
if humanReadable {
value = fmt.Sprintf("%s%%", value)
value = fmt.Sprintf("%.2f%%", percentChange24H)
} else {
value = fmt.Sprintf("%.2f", percentChange24H)
}
if format == "csv" {
@ -1250,14 +997,3 @@ func (ct *Cointop) IsPortfolioVisible() bool {
func (ct *Cointop) PortfolioLen() int {
return len(ct.GetPortfolioSlice())
}
// TogglePortfolioBalances toggles hide/show portfolio balances. Useful for keeping balances secret when sharing screen or taking screenshots.
func (ct *Cointop) TogglePortfolioBalances() error {
ct.State.hidePortfolioBalances = !ct.State.hidePortfolioBalances
ct.UpdateUI(func() error {
go ct.UpdateChart()
go ct.UpdateTable()
return nil
})
return nil
}

@ -2,12 +2,10 @@ package cointop
import (
"fmt"
"math"
"os"
"strings"
"github.com/cointop-sh/cointop/pkg/api"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/api"
"github.com/miguelmota/cointop/pkg/humanize"
)
// PriceConfig is the config options for the coin price method
@ -58,10 +56,7 @@ func GetCoinPrices(config *PricesConfig) ([]string, error) {
if config.APIChoice == CoinMarketCap {
priceAPI = api.NewCMC("")
} else if config.APIChoice == CoinGecko {
priceAPI = api.NewCG(&api.CoinGeckoConfig{
ApiKey: os.Getenv("COINGECKO_API_KEY"),
ProApiKey: os.Getenv("COINGECKO_PRO_API_KEY"),
})
priceAPI = api.NewCG(0, 0)
} else {
return nil, ErrInvalidAPIChoice
}
@ -80,15 +75,3 @@ func GetCoinPrices(config *PricesConfig) ([]string, error) {
return prices, nil
}
// FormatPrice formats the coin price number of decimals and currency format
func (ct *Cointop) FormatPrice(price float64) string {
decimals := 2
if price < 1 {
decimals = 8
}
if price == math.Trunc(price) {
decimals = 2
}
return humanize.Monetaryf(price, decimals)
}

@ -8,11 +8,10 @@ import (
"strings"
"time"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/cointop-sh/cointop/pkg/notifier"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/table"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/notifier"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table"
)
// GetPriceAlertsTableHeaders returns the alerts table headers
@ -43,13 +42,13 @@ var PriceAlertFrequencyMap = map[string]bool{
// GetPriceAlertsTable returns the table for displaying alerts
func (ct *Cointop) GetPriceAlertsTable() *table.Table {
log.Debug("GetPriceAlertsTable()")
maxX := ct.Width()
ct.debuglog("getPriceAlertsTable()")
maxX := ct.width()
t := table.NewTable().SetWidth(maxX)
var rows [][]*table.RowCell
headers := ct.GetPriceAlertsTableHeaders()
ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
ct.ClearSyncMap(ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft)
for _, entry := range ct.State.priceAlerts.Entries {
if entry.Expired {
continue
@ -97,7 +96,7 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table {
})
case "target_price":
targetPrice := fmt.Sprintf("%s %s", entry.Operator, ct.FormatPrice(entry.TargetPrice))
targetPrice := fmt.Sprintf("%s %s", entry.Operator, humanize.Monetaryf(entry.TargetPrice, 2))
ct.SetTableColumnWidthFromString(header, targetPrice)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, &table.RowCell{
@ -146,7 +145,7 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table {
// TogglePriceAlerts toggles the price alerts view
func (ct *Cointop) TogglePriceAlerts() error {
log.Debug("TogglePriceAlerts()")
ct.debuglog("togglePriceAlerts()")
ct.ToggleSelectedView(PriceAlertsView)
ct.NavigateFirstLine()
go ct.UpdateTable()
@ -160,7 +159,7 @@ func (ct *Cointop) IsPriceAlertsVisible() bool {
// PriceAlertWatcher starts the price alert watcher
func (ct *Cointop) PriceAlertWatcher() error {
log.Debug("PriceAlertWatcher()")
ct.debuglog("priceAlertWatcher()")
alerts := ct.State.priceAlerts.Entries
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
@ -176,7 +175,7 @@ func (ct *Cointop) PriceAlertWatcher() error {
// CheckPriceAlert checks the price alert
func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
log.Debug("CheckPriceAlert()")
ct.debuglog("checkPriceAlert()")
if alert.Expired {
return nil
}
@ -188,7 +187,7 @@ func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
}
var msg string
title := "Cointop Alert"
priceStr := fmt.Sprintf("%s%s (%s%s)", ct.CurrencySymbol(), ct.FormatPrice(alert.TargetPrice), ct.CurrencySymbol(), humanize.Monetaryf(coin.Price, 2))
priceStr := fmt.Sprintf("%s%s (%s%s)", ct.CurrencySymbol(), humanize.Numericf(alert.TargetPrice, 2), ct.CurrencySymbol(), humanize.Monetaryf(coin.Price, 2))
if alert.Operator == ">" {
if coin.Price > alert.TargetPrice {
msg = fmt.Sprintf("%s price is greater than %v", alert.CoinName, priceStr)
@ -228,7 +227,7 @@ func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
// UpdatePriceAlertsUpdateMenu updates the alerts update menu view
func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool, coin *Coin) error {
log.Debug("UpdatePriceAlertsUpdateMenu()")
ct.debuglog("updatePriceAlertsUpdateMenu()")
isEdit := false
var value string
@ -267,7 +266,7 @@ func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool, coin *Coin) error {
mode = "Edit"
current = fmt.Sprintf("(current %s%s)", ct.CurrencySymbol(), currentPrice)
submitText = "Set"
offset = ct.Width() - 21
offset = ct.width() - 21
} else {
if coin == nil {
coin = ct.HighlightedRowCoin()
@ -276,7 +275,7 @@ func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool, coin *Coin) error {
value = fmt.Sprintf("> %s", currentPrice)
mode = "Create"
submitText = "Create"
offset = ct.Width() - 23
offset = ct.width() - 23
}
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" %s Alert Entry %s\n\n", mode, pad.Left("[q] close ", offset, " ")))
label := fmt.Sprintf(" Enter target price for %s %s", ct.colorscheme.MenuLabel(coin.Name), current)
@ -294,7 +293,7 @@ func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool, coin *Coin) error {
// ShowPriceAlertsAddMenu shows the alert add menu
func (ct *Cointop) ShowPriceAlertsAddMenu() error {
log.Debug("ShowPriceAlertsAddMenu()")
ct.debuglog("showPriceAlertsAddMenu()")
coin := ct.HighlightedRowCoin()
ct.SetSelectedView(PriceAlertsView)
ct.UpdatePriceAlertsUpdateMenu(true, coin)
@ -307,7 +306,7 @@ func (ct *Cointop) ShowPriceAlertsAddMenu() error {
// ShowPriceAlertsUpdateMenu shows the alerts update menu
func (ct *Cointop) ShowPriceAlertsUpdateMenu() error {
log.Debug("ShowPriceAlertsUpdateMenu()")
ct.debuglog("showPriceAlertsUpdateMenu()")
coin := ct.HighlightedRowCoin()
ct.SetSelectedView(PriceAlertsView)
ct.UpdatePriceAlertsUpdateMenu(false, coin)
@ -320,7 +319,7 @@ func (ct *Cointop) ShowPriceAlertsUpdateMenu() error {
// HidePriceAlertsUpdateMenu hides the alerts update menu
func (ct *Cointop) HidePriceAlertsUpdateMenu() error {
log.Debug("HidePriceAlertsUpdateMenu()")
ct.debuglog("hidePriceAlertsUpdateMenu()")
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.ui.SetViewOnBottom(ct.Views.Input)
ct.ui.SetCursor(false)
@ -346,7 +345,7 @@ func (ct *Cointop) EnterKeyPressHandler() error {
// CreatePriceAlert sets price from inputed value
func (ct *Cointop) CreatePriceAlert() error {
log.Debug("CreatePriceAlert()")
ct.debuglog("createPriceAlert()")
defer ct.HidePriceAlertsUpdateMenu()
isNew := ct.State.priceAlertNewID != ""
@ -437,7 +436,7 @@ func (ct *Cointop) ParsePriceAlertInput(value string) (string, float64, error) {
// SetPriceAlert sets a price alert
func (ct *Cointop) SetPriceAlert(coinName string, operator string, targetPrice float64) error {
log.Debug("SetPriceAlert()")
ct.debuglog("setPriceAlert()")
if operator == "" {
operator = "="
@ -475,9 +474,9 @@ func (ct *Cointop) SetPriceAlert(coinName string, operator string, targetPrice f
// RemovePriceAlert removes a price alert entry
func (ct *Cointop) RemovePriceAlert(id string) error {
log.Debug("RemovePriceAlert()")
ct.debuglog("removePriceAlert()")
for i, entry := range ct.State.priceAlerts.Entries {
if entry.ID == id {
if entry.ID == ct.State.priceAlertEditID {
ct.State.priceAlerts.Entries = append(ct.State.priceAlerts.Entries[:i], ct.State.priceAlerts.Entries[i+1:]...)
}
}

@ -3,8 +3,7 @@ package cointop
import (
"os"
"github.com/cointop-sh/cointop/pkg/gocui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/gocui"
)
// Quit quits the program
@ -15,7 +14,7 @@ func (ct *Cointop) Quit() error {
// QuitView exists the current view
func (ct *Cointop) QuitView() error {
log.Debug("QuitView()")
ct.debuglog("quitView()")
if ct.State.selectedView != CoinsView {
ct.SetSelectedView(CoinsView)
return ct.UpdateTable()
@ -29,7 +28,7 @@ func (ct *Cointop) QuitView() error {
// Exit safely exits the program
func (ct *Cointop) Exit() {
log.Debug("Exit()")
ct.debuglog("exit()")
ct.logfile.Close()
if ct.g != nil {
ct.g.Close()

@ -3,13 +3,11 @@ package cointop
import (
"strings"
"time"
log "github.com/sirupsen/logrus"
)
// Refresh triggers a force refresh of coin data
func (ct *Cointop) Refresh() error {
log.Debug("Refresh()")
ct.debuglog("refresh()")
go func() {
<-ct.limiter
ct.forceRefresh <- true
@ -19,7 +17,7 @@ func (ct *Cointop) Refresh() error {
// RefreshAll triggers a force refresh of all data
func (ct *Cointop) RefreshAll() error {
log.Debug("RefreshAll()")
ct.debuglog("refreshAll()")
ct.refreshMux.Lock()
defer ct.refreshMux.Unlock()
ct.setRefreshStatus()
@ -35,7 +33,7 @@ func (ct *Cointop) RefreshAll() error {
// SetRefreshStatus sets the refresh ticker
func (ct *Cointop) setRefreshStatus() {
log.Debug("setRefreshStatus()")
ct.debuglog("setRefreshStatus()")
go func() {
ct.loadingTicks("refreshing", 900)
ct.RowChanged()
@ -44,7 +42,7 @@ func (ct *Cointop) setRefreshStatus() {
// LoadingTicks sets the loading ticking dots
func (ct *Cointop) loadingTicks(s string, t int) {
log.Debug("loadingTicks()")
ct.debuglog("loadingTicks()")
interval := 150
k := 0
for i := 0; i < (t / interval); i++ {
@ -59,7 +57,7 @@ func (ct *Cointop) loadingTicks(s string, t int) {
// intervalFetchData does a force refresh at every interval
func (ct *Cointop) intervalFetchData() {
log.Debug("intervalFetchData()")
ct.debuglog("intervalFetchData()")
go func() {
for {
select {

@ -1,10 +1,8 @@
package cointop
import log "github.com/sirupsen/logrus"
// Save saves the cointop settings to the config file
func (ct *Cointop) Save() error {
log.Debug("Save()")
ct.debuglog("Save()")
ct.SetSavingStatus()
if err := ct.SaveConfig(); err != nil {
return err
@ -17,7 +15,7 @@ func (ct *Cointop) Save() error {
// SetSavingStatus sets the saving indicator in the statusbar
func (ct *Cointop) SetSavingStatus() {
log.Debug("SetSavingStatus()")
ct.debuglog("SetSavingStatus()")
if ct.g == nil {
return
}

@ -4,9 +4,8 @@ import (
"regexp"
"strings"
"github.com/cointop-sh/cointop/pkg/levenshtein"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/levenshtein"
"github.com/miguelmota/cointop/pkg/ui"
)
// SearchFieldView is structure for search field view
@ -14,7 +13,8 @@ type SearchFieldView = ui.View
// NewSearchFieldView returns a new search field view
func NewSearchFieldView() *SearchFieldView {
return ui.NewView("searchfield")
var view *SearchFieldView = ui.NewView("searchfield")
return view
}
// InputView is structure for help view
@ -22,15 +22,13 @@ type InputView = ui.View
// NewInputView returns a new help view
func NewInputView() *InputView {
return ui.NewView("input")
var view *InputView = ui.NewView("input")
return view
}
// OpenSearch opens the search field
func (ct *Cointop) OpenSearch() error {
log.Debug("OpenSearch()")
if ct.ui.ActiveViewName() != ct.Views.Table.Name() {
return nil
}
func (ct *Cointop) openSearch() error {
ct.debuglog("openSearch()")
ct.State.searchFieldVisible = true
ct.ui.SetCursor(true)
ct.SetActiveView(ct.Views.SearchField.Name())
@ -39,7 +37,7 @@ func (ct *Cointop) OpenSearch() error {
// CancelSearch closes the search field
func (ct *Cointop) CancelSearch() error {
log.Debug("CancelSearch()")
ct.debuglog("cancelSearch()")
ct.State.searchFieldVisible = false
ct.ui.SetCursor(false)
ct.SetActiveView(ct.Views.Table.Name())
@ -48,7 +46,7 @@ func (ct *Cointop) CancelSearch() error {
// DoSearch triggers the search and sets views
func (ct *Cointop) DoSearch() error {
log.Debug("DoSearch()")
ct.debuglog("doSearch()")
ct.Views.SearchField.Rewind()
b := make([]byte, 100)
n, err := ct.Views.SearchField.Read(b)
@ -66,7 +64,7 @@ func (ct *Cointop) DoSearch() error {
if n == 0 {
return nil
}
q := strings.TrimSpace(string(b[:n]))
q := string(b)
// remove slash
regex := regexp.MustCompile(`/(.*)`)
matches := regex.FindStringSubmatch(q)
@ -78,68 +76,26 @@ func (ct *Cointop) DoSearch() error {
// Search performs the search and filtering
func (ct *Cointop) Search(q string) error {
log.Debugf("Search(%s)", q)
// If there are no coins, return no result
if len(ct.State.coins) == 0 {
return nil
}
// If search term is empty, use the previous search term.
ct.debuglog("search()")
q = strings.TrimSpace(strings.ToLower(q))
if q == "" {
q = ct.State.lastSearchQuery
} else {
ct.State.lastSearchQuery = q
}
canSearchSymbol := true
canSearchName := true
if strings.HasPrefix(q, "s:") {
canSearchSymbol = true
canSearchName = false
q = q[2:]
log.Debug("Search, by keyword")
}
if strings.HasPrefix(q, "n:") {
canSearchSymbol = false
canSearchName = true
q = q[2:]
log.Debug("Search, by name")
}
idx := -1
min := -1
var hasprefixidx []int
var hasprefixdist []int
// Start the search from the current position (+1), looking names that start with the search term, or symbols that match completely
currentIndex := ct.GetGlobalCoinIndex(ct.HighlightedRowCoin()) + 1
if ct.IsLastPage() && ct.IsLastRow() {
currentIndex = 0
}
for i := currentIndex; i < len(ct.State.allCoins); i++ {
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
name := strings.ToLower(coin.Name)
symbol := strings.ToLower(coin.Symbol)
// if query matches symbol, return immediately
if canSearchSymbol && symbol == q {
if symbol == q {
ct.GoToGlobalIndex(i)
return nil
}
if !canSearchName {
continue
}
// if query matches name, return immediately
if name == q {
ct.GoToGlobalIndex(i)
return nil
}
// store index with the smallest levenshtein
dist := levenshtein.DamerauLevenshteinDistance(name, q)
if min == -1 || dist <= min {
@ -154,22 +110,15 @@ func (ct *Cointop) Search(q string) error {
}
}
}
if !canSearchName {
return nil
}
// go to row if prefix match
if len(hasprefixidx) > 0 && hasprefixidx[0] != -1 && min > 0 {
ct.GoToGlobalIndex(hasprefixidx[0])
return nil
}
// go to row if levenshtein distance is small enough
if idx > -1 && min <= 6 {
ct.GoToGlobalIndex(idx)
return nil
}
return nil
}

@ -1,10 +1,8 @@
package cointop
import log "github.com/sirupsen/logrus"
// SelectedCoinName returns the selected coin name
func (ct *Cointop) SelectedCoinName() string {
log.Debug("SelectedCoinName()")
ct.debuglog("selectedCoinName()")
coin := ct.State.selectedCoin
if coin != nil {
return coin.Name
@ -15,7 +13,7 @@ func (ct *Cointop) SelectedCoinName() string {
// SelectedCoinSymbol returns the selected coin symbol
func (ct *Cointop) SelectedCoinSymbol() string {
log.Debug("SelectedCoinSymbol()")
ct.debuglog("selectedCoinSymbol()")
coin := ct.State.selectedCoin
if coin != nil {
return coin.Symbol

@ -1,10 +1,8 @@
package cointop
import log "github.com/sirupsen/logrus"
// Size returns window width and height
func (ct *Cointop) Size() (int, int) {
log.Debug("Size()")
func (ct *Cointop) size() (int, int) {
ct.debuglog("size()")
if ct.g == nil {
return 0, 0
}
@ -13,22 +11,22 @@ func (ct *Cointop) Size() (int, int) {
}
// Width returns window width
func (ct *Cointop) Width() int {
log.Debug("Width()")
w, _ := ct.Size()
func (ct *Cointop) width() int {
ct.debuglog("width()")
w, _ := ct.size()
return w
}
// Height returns window height
func (ct *Cointop) Height() int {
log.Debug("Height()")
_, h := ct.Size()
func (ct *Cointop) height() int {
ct.debuglog("height()")
_, h := ct.size()
return h
}
// ViewWidth returns view width
func (ct *Cointop) ViewWidth(view string) int {
log.Debug("ViewWidth()")
ct.debuglog("viewWidth()")
v, err := ct.g.View(view)
if err != nil {
return 0
@ -39,8 +37,8 @@ func (ct *Cointop) ViewWidth(view string) int {
// ClampedWidth returns the clamped width
func (ct *Cointop) ClampedWidth() int {
log.Debug("ClampedWidth()")
w := ct.Width()
ct.debuglog("clampedWidth()")
w := ct.width()
if w > ct.maxTableWidth {
return ct.maxTableWidth
}

@ -4,27 +4,26 @@ import (
"sort"
"sync"
"github.com/cointop-sh/cointop/pkg/gocui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/gocui"
)
var sortlock sync.Mutex
// Sort sorts the list of coins
func (ct *Cointop) Sort(sortCons *sortConstraint, list []*Coin, renderHeaders bool) {
log.Debug("Sort()")
func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bool) {
ct.debuglog("sort()")
sortlock.Lock()
defer sortlock.Unlock()
ct.State.viewSorts[ct.State.selectedView] = sortCons
ct.State.sortBy = sortBy
ct.State.sortDesc = desc
if list == nil {
return
}
if len(list) < 2 {
return
}
sort.SliceStable(list[:], func(i, j int) bool {
if sortCons.sortDesc {
sort.Slice(list[:], func(i, j int) bool {
if ct.State.sortDesc {
i, j = j, i
}
a := list[i]
@ -35,7 +34,7 @@ func (ct *Cointop) Sort(sortCons *sortConstraint, list []*Coin, renderHeaders bo
if b == nil {
return false
}
switch sortCons.sortBy {
switch sortBy {
case "rank":
return a.Rank < b.Rank
case "name":
@ -60,22 +59,12 @@ func (ct *Cointop) Sort(sortCons *sortConstraint, list []*Coin, renderHeaders bo
return a.PercentChange7D < b.PercentChange7D
case "30d_change":
return a.PercentChange30D < b.PercentChange30D
case "1y_change":
return a.PercentChange1Y < b.PercentChange1Y
case "total_supply":
return a.TotalSupply < b.TotalSupply
case "available_supply":
return a.AvailableSupply < b.AvailableSupply
case "last_updated":
return a.LastUpdated < b.LastUpdated
case "cost_price":
return a.BuyPrice < b.BuyPrice
case "cost":
return (a.BuyPrice * a.Holdings) < (b.BuyPrice * b.Holdings) // TODO: convert?
case "pnl":
return (a.Price - a.BuyPrice) < (b.Price - b.BuyPrice)
case "pnl_percent":
return (a.Price - a.BuyPrice) < (b.Price - b.BuyPrice)
default:
return a.Rank < b.Rank
}
@ -88,23 +77,23 @@ func (ct *Cointop) Sort(sortCons *sortConstraint, list []*Coin, renderHeaders bo
// SortAsc sorts list of coins in ascending order
func (ct *Cointop) SortAsc() error {
log.Debug("SortAsc()")
ct.State.viewSorts[ct.State.selectedView].sortDesc = false
ct.debuglog("sortAsc()")
ct.State.sortDesc = false
ct.UpdateTable()
return nil
}
// SortDesc sorts list of coins in descending order
func (ct *Cointop) SortDesc() error {
log.Debug("SortDesc()")
ct.State.viewSorts[ct.State.selectedView].sortDesc = true
ct.debuglog("sortDesc()")
ct.State.sortDesc = true
ct.UpdateTable()
return nil
}
// SortPrevCol sorts the previous column
func (ct *Cointop) SortPrevCol() error {
log.Debug("SortPrevCol()")
ct.debuglog("sortPrevCol()")
cols := ct.GetActiveTableHeaders()
i := ct.GetSortColIndex()
k := i - 1
@ -112,17 +101,14 @@ func (ct *Cointop) SortPrevCol() error {
k = 0
}
nextsortBy := cols[k]
curSortConst := ct.State.viewSorts[ct.State.selectedView]
curSortConst.sortBy = nextsortBy
ct.Sort(curSortConst, ct.State.coins, true)
ct.Sort(nextsortBy, ct.State.sortDesc, ct.State.coins, true)
ct.UpdateTable()
return nil
}
// SortNextCol sorts the next column
func (ct *Cointop) SortNextCol() error {
log.Debug("SortNextCol()")
ct.debuglog("sortNextCol()")
cols := ct.GetActiveTableHeaders()
l := len(cols)
i := ct.GetSortColIndex()
@ -131,32 +117,26 @@ func (ct *Cointop) SortNextCol() error {
k = l - 1
}
nextsortBy := cols[k]
curSortCons := ct.State.viewSorts[ct.State.selectedView]
curSortCons.sortBy = nextsortBy
ct.Sort(curSortCons, ct.State.coins, true)
ct.Sort(nextsortBy, ct.State.sortDesc, ct.State.coins, true)
ct.UpdateTable()
return nil
}
// SortToggle toggles the sort order
func (ct *Cointop) SortToggle(sortBy string, desc bool) error {
log.Debug("SortToggle()")
curSortCons := ct.State.viewSorts[ct.State.selectedView]
if curSortCons.sortBy == sortBy {
curSortCons.sortDesc = !curSortCons.sortDesc
} else {
curSortCons.sortBy = sortBy
curSortCons.sortDesc = desc
ct.debuglog("sortToggle()")
if ct.State.sortBy == sortBy {
desc = !ct.State.sortDesc
}
ct.Sort(curSortCons, ct.State.coins, true)
ct.Sort(sortBy, desc, ct.State.coins, true)
ct.UpdateTable()
return nil
}
// Sortfn returns the sort function as a wrapped gocui keybinding function
func (ct *Cointop) Sortfn(sortBy string, desc bool) func(g *gocui.Gui, v *gocui.View) error {
log.Debug("Sortfn()")
ct.debuglog("sortfn()")
return func(g *gocui.Gui, v *gocui.View) error {
coin := ct.HighlightedRowCoin()
err := ct.SortToggle(sortBy, desc)
@ -175,10 +155,10 @@ func (ct *Cointop) Sortfn(sortBy string, desc bool) func(g *gocui.Gui, v *gocui.
// GetSortColIndex gets the sort column index
func (ct *Cointop) GetSortColIndex() int {
log.Debug("GetSortColIndex()")
ct.debuglog("getSortColIndex()")
cols := ct.GetActiveTableHeaders()
for i, col := range cols {
if ct.State.viewSorts[ct.State.selectedView].sortBy == col {
if ct.State.sortBy == col {
return i
}
}

@ -2,14 +2,11 @@ package cointop
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
"github.com/cointop-sh/cointop/pkg/open"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/open"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
)
// StatusbarView is structure for statusbar view
@ -17,12 +14,13 @@ type StatusbarView = ui.View
// NewStatusbarView returns a new statusbar view
func NewStatusbarView() *StatusbarView {
return ui.NewView("statusbar")
var view *StatusbarView = ui.NewView("statusbar")
return view
}
// UpdateStatusbar updates the statusbar view
func (ct *Cointop) UpdateStatusbar(s string) error {
log.Debug("UpdateStatusbar()")
ct.debuglog("UpdateStatusbar()")
currpage := ct.CurrentDisplayPage()
totalpages := ct.TotalPagesDisplay()
var quitText string
@ -54,7 +52,7 @@ func (ct *Cointop) UpdateStatusbar(s string) error {
content = fmt.Sprintf("%s %s[+]Add", helpStr, editStr)
} else {
base := fmt.Sprintf("%s %sChart %sRange %sSearch %sConvert %s %s", helpStr, "[Enter]", "[[ ]]", "[/]", "[C]", favoritesText, portfolioText)
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.Width(), " ")
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.width(), " ")
v := ct.Version()
size := utf8.RuneCountInString(str)
end := size - utf8.RuneCountInString(v) + 2
@ -74,7 +72,7 @@ func (ct *Cointop) UpdateStatusbar(s string) error {
// RefreshRowLink updates the row link in the statusbar
func (ct *Cointop) RefreshRowLink() error {
log.Debug("RefreshRowLink()")
ct.debuglog("RefreshRowLink()")
var shortcut string
if !open.CommandExists() {
shortcut = "[O]Open "
@ -85,51 +83,3 @@ func (ct *Cointop) RefreshRowLink() error {
return nil
}
// StatusbarMouseLeftClick is called on mouse left click event
func (ct *Cointop) StatusbarMouseLeftClick() error {
_, x, _, err := ct.g.GetViewRelativeMousePosition(ct.g.CurrentEvent)
if err != nil {
return err
}
// Parse the statusbar text to identify hotspots and actions
b := make([]byte, 1000)
ct.Views.Statusbar.Rewind()
if n, err := ct.Views.Statusbar.Read(b); err != nil {
return err
} else {
// Find all the "[X]word" substrings, then look for the one that was clicked
matches := regexp.MustCompile(`\[.*?\]\w+`).FindAllIndex(b[:n], -1)
for _, match := range matches {
if x >= match[0] && x <= match[1] {
s := string(b[match[0]:match[1]])
word := strings.Split(s, "]")[1] // matches the \w+ from regex
// Quit/Return Help Chart Range Search Convert Favorites Portfolio Edit(portfolio) Unfavorite
switch word {
case "Help":
ct.ToggleHelp()
case "Range":
// left hand edge of "Range" is Prev, the rest is Next
if x-match[0] < 3 {
ct.PrevChartRange()
} else {
ct.NextChartRange()
}
case "Search":
ct.OpenSearch()
case "Convert":
ct.ToggleConvertMenu()
case "Favorites":
ct.ToggleSelectedView(FavoritesView)
case "Portfolio":
ct.ToggleSelectedView(PortfolioView)
}
}
}
}
return nil
}

@ -5,13 +5,11 @@ import (
"fmt"
"os"
"strings"
log "github.com/sirupsen/logrus"
)
// ReadAPIKeyFromStdin reads the user inputed API from the stdin prompt
func (ct *Cointop) ReadAPIKeyFromStdin(name string) (string, error) {
log.Debug("ReadAPIKeyFromStdin()")
ct.debuglog("ReadAPIKeyFromStdin()")
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Enter %s API Key: ", name)
text, err := reader.ReadString('\n')

@ -3,11 +3,9 @@ package cointop
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/ui"
)
// TableView is structure for table view
@ -15,14 +13,15 @@ type TableView = ui.View
// NewTableView returns a new table view
func NewTableView() *TableView {
return ui.NewView("table")
var view *TableView = ui.NewView("table")
return view
}
const dots = "..."
// RefreshTable refreshes the table
func (ct *Cointop) RefreshTable() error {
log.Debug("RefreshTable()")
ct.debuglog("refreshTable()")
statusText := ""
switch ct.State.selectedView {
@ -65,7 +64,7 @@ func (ct *Cointop) RefreshTable() error {
// UpdateTable updates the table
func (ct *Cointop) UpdateTable() error {
log.Debug("UpdateTable()")
ct.debuglog("UpdateTable()")
ct.State.allCoinsSlugMap.Range(func(key, value interface{}) bool {
k := key.(string)
if v, ok := value.(*Coin); ok {
@ -81,18 +80,24 @@ func (ct *Cointop) UpdateTable() error {
} else if ct.IsPortfolioVisible() {
ct.State.coins = ct.GetPortfolioSlice()
} else {
// TODO: maintain state of previous sorting
if ct.State.sortBy == "holdings" {
ct.State.sortBy = "rank"
ct.State.sortDesc = false
}
ct.State.coins = ct.GetTableCoinsSlice()
}
ct.Sort(ct.State.viewSorts[ct.State.selectedView], ct.State.coins, true)
ct.Sort(ct.State.sortBy, ct.State.sortDesc, ct.State.coins, true)
go ct.RefreshTable()
return nil
}
// GetTableCoinsSlice returns a slice of the table rows
func (ct *Cointop) GetTableCoinsSlice() []*Coin {
log.Debug("GetTableCoinsSlice()")
var sliced []*Coin
ct.debuglog("GetTableCoinsSlice()")
sliced := []*Coin{}
start := ct.State.page * ct.State.perPage
end := start + ct.State.perPage
allCoins := ct.AllCoins()
@ -134,7 +139,7 @@ func (ct *Cointop) GetTableCoinsSlice() []*Coin {
// HighlightedRowIndex returns the index of the highlighted row within the per-page limit
func (ct *Cointop) HighlightedRowIndex() int {
log.Debug("HighlightedRowIndex()")
ct.debuglog("HighlightedRowIndex()")
oy := ct.Views.Table.OriginY()
cy := ct.Views.Table.CursorY()
idx := oy + cy
@ -150,7 +155,7 @@ func (ct *Cointop) HighlightedRowIndex() int {
// HighlightedRowCoin returns the coin at the index of the highlighted row
func (ct *Cointop) HighlightedRowCoin() *Coin {
log.Debug("HighlightedRowCoin()")
ct.debuglog("HighlightedRowCoin()")
idx := ct.HighlightedRowIndex()
coins := ct.State.coins
if ct.IsPriceAlertsVisible() {
@ -169,7 +174,7 @@ func (ct *Cointop) HighlightedRowCoin() *Coin {
// HighlightedPageRowIndex returns the index of page row of the highlighted row
func (ct *Cointop) HighlightedPageRowIndex() int {
log.Debug("HighlightedPageRowIndex()")
ct.debuglog("HighlightedPageRowIndex()")
cy := ct.Views.Table.CursorY()
idx := cy
if idx < 0 {
@ -186,38 +191,18 @@ func (ct *Cointop) GetLastSelectedRowCoinIndex() int {
// RowLink returns the row url link
func (ct *Cointop) RowLink() string {
log.Debug("RowLink()")
coin := ct.HighlightedRowCoin()
if coin == nil {
return ""
}
// TODO: Can remove this one after some releases
// because it is a way to force old client refresh coin to have a slug
if coin.Slug == "" {
if err := ct.UpdateCoin(coin); err != nil {
log.Debugf("RowLink() Update coin got err %s", err.Error())
return ""
}
}
return ct.api.CoinLink(coin.Slug)
}
// RowLink returns the row url link
func (ct *Cointop) RowAltLink() string {
log.Debug("RowAltLink()")
ct.debuglog("RowLink()")
coin := ct.HighlightedRowCoin()
if coin == nil {
return ""
}
return ct.GetAltCoinLink(coin)
return ct.api.CoinLink(coin.Name)
}
// RowLinkShort returns a shortened version of the row url link
func (ct *Cointop) RowLinkShort() string {
log.Debug("RowLinkShort()")
ct.debuglog("RowLinkShort()")
link := ct.RowLink()
if link != "" {
u, err := url.Parse(link)
@ -239,23 +224,9 @@ func (ct *Cointop) RowLinkShort() string {
return ""
}
func (ct *Cointop) GetAltCoinLink(coin *Coin) string {
if ct.State.altCoinLink == "" {
return ct.api.CoinLink(coin.Slug)
}
url := ct.State.altCoinLink
url = strings.Replace(url, "{{ID}}", coin.ID, -1)
url = strings.Replace(url, "{{NAME}}", coin.Name, -1)
url = strings.Replace(url, "{{RANK}}", strconv.Itoa(coin.Rank), -1)
url = strings.Replace(url, "{{SLUG}}", coin.Slug, -1)
url = strings.Replace(url, "{{SYMBOL}}", coin.Symbol, -1)
return url
}
// ToggleTableFullscreen toggles the table fullscreen mode
func (ct *Cointop) ToggleTableFullscreen() error {
log.Debug("ToggleTableFullscreen()")
ct.debuglog("ToggleTableFullscreen()")
ct.State.onlyTable = !ct.State.onlyTable
ct.State.onlyChart = false
if !ct.State.onlyTable {
@ -294,11 +265,6 @@ func (ct *Cointop) ToggleTableFullscreen() error {
func (ct *Cointop) SetSelectedView(viewName string) {
ct.State.lastSelectedView = ct.State.selectedView
ct.State.selectedView = viewName
// init sort constraint for the view if it hasn't been seen before
if _, found := ct.State.viewSorts[viewName]; !found {
ct.State.viewSorts[viewName] = &sortConstraint{DefaultSortBy, false}
}
}
// ToggleSelectedView toggles between current table view and last selected table view

@ -6,9 +6,8 @@ import (
"strings"
"unicode/utf8"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
)
// ArrowUp is up arrow unicode character
@ -21,140 +20,101 @@ var ArrowDown = "▼"
type HeaderColumn struct {
Slug string
Label string
ShortLabel string // only columns with a ShortLabel can be scaled?
PlainLabel string
}
// HeaderColumns are the header column widths
var HeaderColumns = map[string]*HeaderColumn{
"rank": {
"rank": &HeaderColumn{
Slug: "rank",
Label: "[r]ank",
PlainLabel: "rank",
},
"name": {
"name": &HeaderColumn{
Slug: "name",
Label: "[n]ame",
PlainLabel: "name",
},
"symbol": {
"symbol": &HeaderColumn{
Slug: "symbol",
Label: "[s]ymbol",
PlainLabel: "symbol",
},
"target_price": {
"target_price": &HeaderColumn{
Slug: "target_price",
Label: "[t]target price",
PlainLabel: "target price",
},
"price": {
"price": &HeaderColumn{
Slug: "price",
Label: "[p]rice",
PlainLabel: "price",
},
"frequency": {
"frequency": &HeaderColumn{
Slug: "frequency",
Label: "frequency",
PlainLabel: "frequency",
},
"holdings": {
"holdings": &HeaderColumn{
Slug: "holdings",
Label: "[h]oldings",
PlainLabel: "holdings",
},
"balance": {
"balance": &HeaderColumn{
Slug: "balance",
Label: "[b]alance",
PlainLabel: "balance",
},
"market_cap": {
"market_cap": &HeaderColumn{
Slug: "market_cap",
Label: "[m]arket cap",
ShortLabel: "[m]cap",
PlainLabel: "market cap",
},
"24h_volume": {
"24h_volume": &HeaderColumn{
Slug: "24h_volume",
Label: "24H [v]olume",
ShortLabel: "24[v]",
PlainLabel: "24H volume",
},
"1h_change": {
"1h_change": &HeaderColumn{
Slug: "1h_change",
Label: "[1]H%",
PlainLabel: "1H%",
},
"24h_change": {
"24h_change": &HeaderColumn{
Slug: "24h_change",
Label: "[2]4H%",
PlainLabel: "24H%",
},
"7d_change": {
"7d_change": &HeaderColumn{
Slug: "7d_change",
Label: "[7]D%",
PlainLabel: "7D%",
},
"30d_change": {
"30d_change": &HeaderColumn{
Slug: "30d_change",
Label: "[3]0D%",
PlainLabel: "30D%",
},
"1y_change": {
Slug: "1y_change",
Label: "1[y]%",
PlainLabel: "1Y%",
},
"total_supply": {
"total_supply": &HeaderColumn{
Slug: "total_supply",
Label: "[t]otal supply",
ShortLabel: "[t]ot",
PlainLabel: "total supply",
},
"available_supply": {
"available_supply": &HeaderColumn{
Slug: "available_supply",
Label: "[a]vailable supply",
ShortLabel: "[a]vl",
PlainLabel: "available supply",
},
"percent_holdings": {
"percent_holdings": &HeaderColumn{
Slug: "percent_holdings",
Label: "[%]holdings",
PlainLabel: "%holdings",
},
"last_updated": {
"last_updated": &HeaderColumn{
Slug: "last_updated",
Label: "last [u]pdated",
PlainLabel: "last updated",
},
"cost_price": {
Slug: "cost_price",
Label: "cost price",
PlainLabel: "cost price",
},
"cost": {
Slug: "cost",
Label: "[!]cost",
PlainLabel: "cost",
},
"pnl": {
Slug: "pnl",
Label: "[@]PNL",
PlainLabel: "PNL",
},
"pnl_percent": {
Slug: "pnl_percent",
Label: "[#]PNL%",
PlainLabel: "PNL%",
},
}
// GetLabel fetch the label to use for the heading (depends on configuration)
func (ct *Cointop) GetLabel(h *HeaderColumn) string {
// TODO: technically this should support nosort
if ct.IsActiveTableCompactNotation() && h.ShortLabel != "" {
return h.ShortLabel
}
return h.Label
}
// TableHeaderView is structure for table header view
@ -162,7 +122,8 @@ type TableHeaderView = ui.View
// NewTableHeaderView returns a new table header view
func NewTableHeaderView() *TableHeaderView {
return ui.NewView("table_header")
var view *TableHeaderView = ui.NewView("table_header")
return view
}
// GetActiveTableHeaders returns the list of active table headers
@ -179,32 +140,15 @@ func (ct *Cointop) GetActiveTableHeaders() []string {
return cols
}
// IsActiveTableCompactNotation returns whether the current view is using compact-notation
func (ct *Cointop) IsActiveTableCompactNotation() bool {
var compact bool
switch ct.State.selectedView {
case PortfolioView:
compact = ct.State.portfolioCompactNotation
case CoinsView:
compact = ct.State.tableCompactNotation
case FavoritesView:
compact = ct.State.favoritesCompactNotation
default:
compact = ct.State.tableCompactNotation
}
return compact
}
// UpdateTableHeader renders the table header
func (ct *Cointop) UpdateTableHeader() error {
log.Debug("UpdateTableHeader()")
ct.debuglog("UpdateTableHeader()")
baseColor := ct.colorscheme.TableHeaderSprintf()
noSort := ct.IsPriceAlertsVisible()
cols := ct.GetActiveTableHeaders()
var headers []string
var columnLookup []string // list of column-names or ""
for i, col := range cols {
hc, ok := HeaderColumns[col]
if !ok {
@ -217,23 +161,23 @@ func (ct *Cointop) UpdateTableHeader() error {
arrow := " "
colorfn := baseColor
if !noSort {
currentSortCons := ct.State.viewSorts[ct.State.selectedView]
if currentSortCons.sortBy == col {
if ct.State.sortBy == col {
colorfn = ct.colorscheme.TableHeaderColumnActiveSprintf()
arrow = ArrowUp
if currentSortCons.sortDesc {
if ct.State.sortDesc {
arrow = ArrowDown
} else {
arrow = ArrowUp
}
}
}
label := ct.GetLabel(hc)
label := hc.Label
if noSort {
label = hc.PlainLabel
}
leftAlign := ct.GetTableColumnAlignLeft(col)
switch col {
case "price", "balance", "pnl", "cost":
label = fmt.Sprintf("%s%s", ct.CurrencySymbol(), label)
case "price", "balance":
label = ct.CurrencySymbol() + label
}
if leftAlign {
label = label + arrow
@ -248,27 +192,15 @@ func (ct *Cointop) UpdateTableHeader() error {
if leftAlign {
padfn = pad.Right
}
padded := padfn(label, width+(1-padLeft), " ")
colStr := fmt.Sprintf(
"%s%s%s",
strings.Repeat(" ", padLeft),
colorfn(padded),
colorfn(padfn(label, width+(1-padLeft), " ")),
strings.Repeat(" ", 1),
)
headers = append(headers, colStr)
// Create a lookup table (pos to column)
for i := 0; i < padLeft; i++ {
columnLookup = append(columnLookup, "")
}
for i := 0; i < utf8.RuneCountInString(padded); i++ {
columnLookup = append(columnLookup, hc.Slug)
}
columnLookup = append(columnLookup, "")
}
ct.State.columnLookup = columnLookup
ct.UpdateUI(func() error {
return ct.Views.TableHeader.Update(strings.Join(headers, ""))
})
@ -276,21 +208,6 @@ func (ct *Cointop) UpdateTableHeader() error {
return nil
}
// TableHeaderMouseLeftClick is called on mouse left click event
func (ct *Cointop) TableHeaderMouseLeftClick() error {
_, x, _, err := ct.g.GetViewRelativeMousePosition(ct.g.CurrentEvent)
if err != nil {
return err
}
// Figure out which column they clicked on
if ct.State.columnLookup[x] != "" {
fn := ct.Sortfn(ct.State.columnLookup[x], false)
return fn(ct.g, ct.Views.Table.Backing())
}
return nil
}
// SetTableColumnAlignLeft sets the column alignment direction for header
func (ct *Cointop) SetTableColumnAlignLeft(header string, alignLeft bool) {
ct.State.tableColumnAlignLeft.Store(header, alignLeft)
@ -313,10 +230,7 @@ func (ct *Cointop) SetTableColumnWidth(header string, width int) {
prev = prevIfc.(int)
} else {
hc := HeaderColumns[header]
if hc == nil {
log.Warnf("SetTableColumnWidth(%s) not found", header)
}
prev = utf8.RuneCountInString(ct.GetLabel(hc)) + 1
prev = utf8.RuneCountInString(hc.Label) + 1
switch header {
case "price", "balance":
prev++

@ -1,13 +1,12 @@
package cointop
import (
"github.com/cointop-sh/cointop/pkg/gocui"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/gocui"
)
// UpdateUI takes a callback which updates the view
func (ct *Cointop) UpdateUI(f func() error) {
log.Debug("UpdateUI()")
ct.debuglog("UpdateUI()")
if ct.g == nil {
return

@ -8,25 +8,16 @@ import (
"strings"
"sync"
"github.com/cointop-sh/cointop/pkg/open"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/blake2b"
"github.com/miguelmota/cointop/pkg/open"
)
// OpenLink opens the url in a browser
func (ct *Cointop) OpenLink() error {
log.Debug("OpenLink()")
ct.debuglog("openLink()")
open.URL(ct.RowLink())
return nil
}
// OpenLink opens the alternate url in a browser
func (ct *Cointop) OpenAltLink() error {
log.Debug("OpenAltLink()")
open.URL(ct.RowAltLink())
return nil
}
// GetBytes returns the interface in bytes form
func GetBytes(key interface{}) ([]byte, error) {
var buf bytes.Buffer
@ -54,7 +45,7 @@ func TruncateString(value string, maxLen int) string {
}
// ClearSyncMap clears a sync.Map
func (ct *Cointop) ClearSyncMap(syncMap *sync.Map) {
func (ct *Cointop) ClearSyncMap(syncMap sync.Map) {
syncMap.Range(func(key interface{}, value interface{}) bool {
syncMap.Delete(key)
return true
@ -74,12 +65,3 @@ func normalizeFloatString(input string, allowNegative bool) string {
return ""
}
func getStructHash(x interface{}) (string, error) {
b, err := GetBytes(x)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", blake2b.Sum256(b)), nil
}

@ -1,110 +0,0 @@
package cointop
import "testing"
func Test_getStructHash(t *testing.T) {
type args struct {
str1 interface{}
str2 interface{}
}
tests := []struct {
name string
args args
wantErr bool
want bool
}{
{
name: "the same structs",
args: args{
str1: struct {
Name string
Properties struct {
P7D int
P10D int
}
}{},
str2: &struct {
Name string
Properties struct {
P7D int
P10D int
}
}{},
},
want: true,
},
{
name: "different structs but have similar fields and different field type",
args: args{
str1: struct {
Name string
Properties struct {
P7D int
P10D int
}
}{},
str2: struct {
Name rune
Properties struct {
P7D int
P10D int
}
}{},
},
want: false,
},
{
name: "different structs and different fields",
args: args{
str1: struct {
Name string
Properties struct {
P7D int
P10D int
}
}{},
str2: struct {
Name string
Age int
Properties struct {
P7D int
P10D int
}
}{},
},
want: false,
},
{
name: "error occurs at str1 when struct is nil",
args: args{
str1: nil,
str2: struct {
Name string
Age int
Properties struct {
P7D int
P10D int
}
}{},
},
wantErr: true,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash1, err1 := getStructHash(tt.args.str1)
hash2, _ := getStructHash(tt.args.str2)
if err1 != nil && !tt.wantErr {
t.Errorf("getStructHash() error = %v, wantErr %v", err1, tt.wantErr)
return
}
if cp := hash1 == hash2; cp != tt.want {
t.Errorf("getStructHash() = %v, want %v", cp, tt.want)
}
})
}
}

@ -3,15 +3,7 @@ title: "Intro"
date: 2020-01-01T00:00:00-00:00
draft: false
---
<h3 align="center">
<br />
<img src="https://user-images.githubusercontent.com/168240/39561871-51cda852-4e5d-11e8-926b-7692d43143e8.png" alt="logo" width="400" />
<br />
<br />
<br />
</h3>
[`cointop`](https://github.com/cointop-sh/cointop) is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time.
[`cointop`](https://github.com/miguelmota/cointop) is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time.
The interface is inspired by [`htop`](https://en.wikipedia.org/wiki/Htop) and shortcut keys are inspired by [`vim`](https://en.wikipedia.org/wiki/Vim_(text_editor)).
@ -19,19 +11,24 @@ The interface is inspired by [`htop`](https://en.wikipedia.org/wiki/Htop) and sh
## Features
- **Shortcut keys**: Vim-inspired shortcut keys, custom key bindings configuration
- **Colorschemes**: Custom colorscheme configuration, 256-color and 24-bit support
- **Favorites**: Save and view favorite coins
- **Portfolio**: Portfolio tracking of holdings, view profit & loss
- **Charts**: Charts for coin price history and global market graphs
- **Search**: Fuzzy searching for finding coins
- **Conversion**: Currency conversion
- **Price Alerts**: Price alerts with desktop notifications
- **Multiple APIs**: Supports multiple coin data APIs; CoinGecko and CoinMarketCap
- **Mouse**: Mouse support
- **Offline**: Offline cache
- **Fast**: Fast sort shortcuts, pagination, chart date range change, auto-refresh
- **Lightweight**: It's very lightweight; can be left running indefinitely
- Quick sort shortcuts
- Custom key bindings configuration
- Vim inspired shortcut keys
- Fast pagination
- Charts for coins and global market graphs
- Quick chart date range change
- Fuzzy searching for finding coins
- Currency conversion
- Save and view favorite coins
- Portfolio tracking of holdings
- 256-color support
- Custom colorschemes
- Help menu
- Offline cache
- Supports multiple coin stat APIs
- Auto-refresh
- Works on macOS, Linux, and Windows
- It's very lightweight; can be left running indefinitely
## In action

@ -5,6 +5,4 @@ draft: false
---
# Changelog
See [CHANGELOG.md](https://github.com/cointop-sh/cointop/blob/master/CHANGELOG.md) on Github for a user-friendly changelog.
See [releases](https://github.com/cointop-sh/cointop/releases) on Github for more detailed commit information of each release.
See [CHANGELOG.md](https://github.com/miguelmota/cointop/blob/master/CHANGELOG.md) on Github.

@ -16,19 +16,15 @@ $ cd ~/.config/cointop
$ git clone git@github.com:cointop-sh/colors.git
```
Note: depending on your system, this may not be the correct location. The "colors" directory needs to go in the same place as your config.toml file.
Then edit your config `~/.config/cointop/config.toml` and set the colorscheme you want to use:
```toml
colorscheme = "cointop"
colorscheme = "<colorscheme>"
```
The colorscheme name is the name of the colorscheme TOML file.
By default, the colorscheme files should go under `~/.config/cointop/colors/`
For example, if you have `matrix.toml` under `~/.config/cointop/colors/matrix.toml` then the `colorscheme` property in `config.toml` should be set to:
For example, if you have `matrix.toml` in `~/.cointop/colors/` then the `colorscheme` property should be set to:
```toml
colorscheme = "matrix"
@ -41,96 +37,3 @@ $ cointop --colorscheme matrix
```
To create your own colorscheme; simply copy an existing [colorscheme](https://github.com/cointop-sh/colors/blob/master/cointop.toml), rename it, and customize the colors.
The default `cointop` colorscheme is shown below:
```toml
colorscheme = "cointop"
base_fg = "white"
base_bg = "black"
chart_fg = "white"
chart_bg = "black"
chart_bold = false
marketbar_fg = "white"
marketbar_bg = "black"
marketbar_bold = false
marketbar_label_active_fg = "cyan"
marketbar_label_active_bg = "black"
marketbar_label_active_bold = false
menu_fg = "white"
menu_bg = "black"
menu_bold = false
menu_header_fg = "black"
menu_header_bg = "green"
menu_header_bold = false
menu_label_fg = "yellow"
menu_label_bg = "black"
menu_label_bold = false
menu_label_active_fg = "yellow"
menu_label_active_bg = "black"
menu_label_active_bold = true
searchbar_fg = "white"
searchbar_bg = "black"
searchbar_bold = false
statusbar_fg = "black"
statusbar_bg = "cyan"
statusbar_bold = false
table_column_price_fg = "cyan"
table_column_price_bg = "black"
table_column_price_bold = false
table_column_change_fg = "white"
table_column_change_bg = "black"
table_column_change_bold = false
table_column_change_down_fg = "red"
table_column_change_down_bg = "black"
table_column_change_down_bold = false
table_column_change_up_fg = "green"
table_column_change_up_bg = "black"
table_column_change_up_bold = false
table_header_fg = "black"
table_header_bg = "green"
table_header_bold = false
table_header_column_active_fg = "black"
table_header_column_active_bg = "cyan"
table_header_column_active_bold = false
table_row_fg = "white"
table_row_bg = "black"
table_row_bold = false
table_row_active_fg = "black"
table_row_active_bg = "cyan"
table_row_active_bold = false
table_row_favorite_fg = "yellow"
table_row_favorite_bg = "black"
table_row_favorite_bold = false
```
Supported colors are:
- `black`
- `blue`
- `cyan`
- `green`
- `magenta`
- `red`
- `white`
- `yellow`
- `default` - system default

@ -39,14 +39,13 @@ You can configure the actions you want for each key in `config.toml`:
```toml
currency = "USD"
default_view = ""
default_chart_range = "1Y"
api = "coingecko"
colorscheme = "cointop"
refresh_rate = 60
[shortcuts]
"$" = "last_page"
0 = "move_to_first_page_first_row"
0 = "first_page"
1 = "sort_column_1h_change"
2 = "sort_column_24h_change"
7 = "sort_column_7d_change"
@ -123,9 +122,6 @@ refresh_rate = 60
[coinmarketcap]
pro_api_key = ""
[coingecko]
pro_api_key = ""
```
## List of actions
@ -136,7 +132,6 @@ Action|Description
----|------|
`first_chart_range`|Select first chart date range (e.g. 24H)
`first_page`|Go to first page
`move_to_first_page_first_row`|Go to first row on the first page
`enlarge_chart`|Increase chart height
`help`|Show help
`hide_currency_convert_menu`|Hide currency convert menu

@ -9,11 +9,11 @@ Pull requests are welcome!
For contributions please create a new branch and submit a pull request for review.
Huge thanks to all the [contributors](https://github.com/cointop-sh/cointop/graphs/contributors) that have made cointop better.
Huge thanks to all the [contributors](https://github.com/miguelmota/cointop/graphs/contributors) that have made cointop better.
## Documentation
Keeping documentation up-to-date is always appreciated! If you'd like to make edits or make additions to the docs, the respective files are located under [`docs/content`](https://github.com/cointop-sh/cointop/tree/master/docs/content)
Keeping documentation up-to-date is always appreciated! If you'd like to make edits or make additions to the docs, the respective files are located under [`docs/content`](https://github.com/miguelmota/cointop/tree/master/docs/content)
Run the documentation locally with:

@ -24,7 +24,7 @@ make deps
Installing from source
```bash
make brew-build
make brew/build
```
## Flatpak
@ -44,7 +44,7 @@ sudo flatpak install flathub org.freedesktop.Sdk.Extension.golang
Building flatpak package
```bash
make flatpak-build
make flatpak/build
```
## Copr
@ -52,18 +52,18 @@ make flatpak-build
Install dependencies
```bash
make copr-install-cli
make rpm-install-deps
make rpm-dirs
make copr/install/cli
make rpm/install/deps
make rpm/dirs
```
Build package
```bash
make rpm-cp-specs
make rpm-download
make rpm-build
make copr-build
make rpm/cp/specs
make rpm/download
make rpm/build
make copr/build
```
## Snap
@ -71,13 +71,5 @@ make copr-build
Building snap
```bash
make snap-build
```
## Docker
Build Docker image
```bash
make docker-build
make snap/build
```

@ -15,8 +15,7 @@ draft: false
## What coins does this support?
This supports any coin supported by the API being used to fetch coin information. There is, however, a limit on the number of coins that
cointop fetches by default. You can increase this by passing `--max-pages` and `--per-page` arguments on the command line.
This supports any coin supported by the API being used to fetch coin information.
## How do I set the API to use?
@ -42,19 +41,6 @@ draft: false
Copy an existing [colorscheme](https://github.com/cointop-sh/colors/blob/master/cointop.toml) to `~/.config/cointop/colors/` and customize the colors. Then run cointop with `--colorscheme <colorscheme>` to use the colorscheme.
You can use any of the 250-odd X11 colors by name. See https://en.wikipedia.org/wiki/X11_color_names (use lower-case and without spaces). You can also include 24-bit colors by using the #rrggbb hex code.
You can also define values in the colorscheme file, and reference them from throughout the file, using the following syntax:
```toml
define_base03 = "#002b36"
menu_header_fg = "$base03"
```
## How do I make the background color transparent?
Change the background color options in the colorscheme file to `default` to use the system default color, eg. `base_bg = "default"`
## Where is the config file located?
The default configuration file is located under `~/.config/cointop/config.toml`
@ -95,9 +81,9 @@ draft: false
## I'm no longer seeing any data!
Run cointop with the `--clean` flag to delete the cache. If you're still not seeing any data, then please [submit an issue](https://github.com/cointop-sh/cointop/issues/new).
Run cointop with the `--clean` flag to delete the cache. If you're still not seeing any data, then please [submit an issue](https://github.com/miguelmota/cointop/issues/new).
## How do I get a CoinMarketCap Pro (Paid) API key?
## How do I get a CoinMarketCap Pro API key?
Create an account on [CoinMarketCap](https://pro.coinmarketcap.com/signup) and visit the [Account](https://pro.coinmarketcap.com/account) page to copy your Pro API key.
@ -122,51 +108,9 @@ draft: false
cointop --coinmarketcap-api-key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
## How do I add my CoinGecko Demo (Free) API key?
Add the API key in the cointop config file:
```toml
[coingecko]
api_key = "CG-xxxxxxxxxxxxxxxxxxxxxxxx"
```
Alternatively, you can export the environment variable `COINGECKO_API_KEY` containing the API key in your `~/.bashrc`
```bash
export COINGECKO_API_KEY=CG-xxxxxxxxxxxxxxxxxxxxxxxx
```
You may also set the API key on start:
```bash
cointop --coingecko-api-key=CG-xxxxxxxxxxxxxxxxxxxxxxxx
```
## How do I add my CoinGecko Pro (Paid) API key?
Add the API key in the cointop config file:
```toml
[coingecko]
pro_api_key = "CG-xxxxxxxxxxxxxxxxxxxxxxxx"
```
Alternatively, you can export the environment variable `COINGECKO_PRO_API_KEY` containing the API key in your `~/.bashrc`
```bash
export COINGECKO_PRO_API_KEY=CG-xxxxxxxxxxxxxxxxxxxxxxxx
```
You may also set the API key on start:
```bash
cointop --coingecko-pro-api-key=CG-xxxxxxxxxxxxxxxxxxxxxxxx
```
## I can I add my own API to cointop?
Fork cointop and add the API that implements the API [interface](https://github.com/cointop-sh/cointop/blob/master/cointop/common/api/interface.go) to [`cointop/cointop/common/api/impl/`](https://github.com/cointop-sh/cointop/tree/master/cointop/common/api/impl). You can use the CoinGecko [implementation](https://github.com/cointop-sh/cointop/blob/master/cointop/common/api/impl/coingecko/coingecko.go) as reference.
Fork cointop and add the API that implements the API [interface](https://github.com/miguelmota/cointop/blob/master/cointop/common/api/interface.go) to [`cointop/cointop/common/api/impl/`](https://github.com/miguelmota/cointop/tree/master/cointop/common/api/impl). You can use the CoinGecko [implementation](https://github.com/miguelmota/cointop/blob/master/cointop/common/api/impl/coingecko/coingecko.go) as reference.
## I installed cointop without errors but the command is not found.
@ -180,9 +124,6 @@ draft: false
## How do I search?
The default key to open search is <kbd>/</kbd>. Type the search query after the `/` in the field and hit <kbd>Enter</kbd>.
Each search starts from the current cursor position. To search for the same term again, hit <kbd>/</kbd> then <kbd>Enter</kbd>.
The default behaviour will start to search by symbol first, then it will continues searching by name if there is no result. To search by only symbol, type the search query after `/s:`. To search by only name, type the search query after `/n:`.
## How do I exit search?
@ -216,8 +157,6 @@ draft: false
Press <kbd>e</kbd> on the highlighted coin to enter holdings and add to your portfolio.
This dialog supports basic expressions including `+` `-` `*` `/` etc.
## How do I edit the holdings of a coin in my portfolio?
Press <kbd>e</kbd> on the highlighted coin to edit the holdings.
@ -234,35 +173,6 @@ draft: false
Your portfolio is autosaved after you edit holdings. You can also press <kbd>ctrl</kbd>+<kbd>s</kbd> to manually save your portfolio holdings to the config file.
## How do I include buy/cost price in my portfolio?
Currently there is no UI for this. If you want to include the cost of your coins in the Portfolio screen, you will need to edit your config.toml
Each coin consists of four values: coin name, coin amount, cost-price, cost-currency.
For example, the following configuration includes 100 ALGO at USD1.95 each; and 0.1 BTC at AUD50100.83 each.
```toml
holdings = [["Algorand", "100", "1.95", "USD"], ["Bitcoin", "0.1", "50100.83", "AUD"]]
```
With this configuration, four new columns are useful:
- `cost_price` the price and currency that the coins were purchased at
- `cost` the cost (in the current currency) of the coins
- `pnl` the PNL of the coins (current value vs original cost)
- `pnl_percent` the PNL of the coins as a fraction of the original cost
With the holdings above, and the currency set to GBP (British Pounds) cointop will look something like this:
![portfolio profit and loss](https://user-images.githubusercontent.com/122371/138361142-8e1f32b5-ca24-471d-a628-06968f07c65f.png)
## How do I hide my portfolio balances (private mode)?
You can run cointop with the `--hide-portfolio-balances` flag to hide portfolio balances or use the keyboard shortcut <kbd>Ctrl</kbd>+<kbd>space</kbd> on the portfolio page to toggle hide/show.
<img width="880" alt="hide portfolio balances" src="https://user-images.githubusercontent.com/122371/133578568-153af3cc-350d-4744-ac89-dd8f5e317d1d.png" />
## I'm getting question marks or weird symbols instead of the correct characters.
Make sure that your terminal has the encoding set to UTF-8 and that your terminal font supports UTF-8.
@ -273,11 +183,11 @@ draft: false
LANG=en_US.utf8 TERM=xterm-256color cointop
```
If you're on Windows (PowerShell, Command Prompt, or WSL), please see the [wiki](https://github.com/cointop-sh/cointop/wiki/Windows-Command-Prompt-and-WSL-Font-Support) for font support instructions.
If you're on Windows (PowerShell, Command Prompt, or WSL), please see the [wiki](https://github.com/miguelmota/cointop/wiki/Windows-Command-Prompt-and-WSL-Font-Support) for font support instructions.
## How do I install Go on Ubuntu?
There's instructions on installing Go on Ubuntu in the [wiki](https://github.com/cointop-sh/cointop/wiki/Installing-Go-on-Ubuntu).
There's instructions on installing Go on Ubuntu in the [wiki](https://github.com/miguelmota/cointop/wiki/Installing-Go-on-Ubuntu).
## I'm getting errors installing the snap in Windows WSL.
@ -302,8 +212,8 @@ draft: false
Here's how to build the executable and run it:
```powershell
> md C:\Users\Josem\go\src\github.com\cointop-sh -ea 0
> git clone https://github.com/cointop-sh/cointop.git
> md C:\Users\Josem\go\src\github.com\miguelmota -ea 0
> git clone https://github.com/miguelmota/cointop.git
> go build -o cointop.exe main.go
> cointop.exe
```
@ -410,33 +320,6 @@ draft: false
In the config file, set `default_view = "default"`
## How do I set the default chart range?
In the config file, set `default_chart_range = "3M"`
Supported date ranges are `All Time`, `YTD`, `1Y`, `6M`, `3M`, `1M`, `7D`, `3D`, `24H`.
## How do I set the table columns?
In the config file, set `columns` value in one of the favorites, portfolio, table sections. The list of available columns can be seen on the help menu - look for "sort_column_XXX".
For example:
```toml
[table]
columns = ["rank", "name", "symbol", "price", "1h_change", "24h_change", "7d_change", "24h_volume", "market_cap", "available_supply", "total_supply", "last_updated"]
```
## What price-change columns are available?
Supported columns relating to price change are `1h_change`, `24h_change`, `7d_change`, `30d_change`, `1y_change`
## How can I use K (thousand), M (million), B (billion), T (trillion) suffixes for shorter numbers?
There is a setting at the top-level of the configuration file called `compact_notation=true` which changes the marketbar values `market cap`, `volume` and `portfolio total value`.
The same setting can be applied at in the `[table]` section to impact the `24h_volume`, `market_cap`, `total_supply`, `available_supply` columns in the main coin view; and in the `[favorites]` section to change the same columns. The setting also changes the column names to be shorter.
## How can use a different config file other than the default?
Run cointop with the `--config` flag, eg `cointop --config="/path/to/config.toml"`, to use the specified file as the config.
@ -453,7 +336,7 @@ draft: false
## I can only view the first page, why isn't the pagination is working?
Sometimes the coin APIs will make updates and break things. If you see this problem please [submit an issue](https://github.com/cointop-sh/cointop/issues/new).
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).
## How can run cointop with just the table?
@ -559,39 +442,10 @@ draft: false
cointop server -k ~/.ssh/id_rsa [...]
```
## How do I fix the error `no matching host key type found. Their offer: ssh-rsa` when trying to SSH?
Use the following flag when connecting to the SSH server:
```bash
ssh -oHostKeyAlgorithms=+ssh-rsa cointop.sh
```
You can also add this config to the `~/.ssh/config` file so you don't have to use the flag every time:
```
Host cointop.sh
HostName cointop.sh
HostKeyAlgorithms=+ssh-rsa
```
## Why doesn't the version number work when I install with `go get`?
The version number is read from the git tag during the build process but this requires the `GO111MODULE` environment variable to be set in order for Go to read the build information:
```bash
GO111MODULE=on go get github.com/cointop-sh/cointop
```
## How can I get more information when something is going wrong?
Cointop creates a logfile at `/tmp/cointop.log`. Normally nothing is written to this, but if you set the environment variable
`DEBUG=1` cointop will write a lot of output describing its operation. Furthermore, if you also set `DEBUG_HTTP=1` it will
emit lots about every HTTP request that cointop makes to coingecko (backend). Developers may ask for this information
to help diagnose any problems you may experience.
```bash
DEBUG=1 DEBUG_HTTP=1 cointop
GO111MODULE=on go get github.com/miguelmota/cointop
```
If you set environment variable `DEBUG_FILE` you can explicitly provide a logfile location, rather than `/tmp/cointop.log`

@ -7,15 +7,15 @@ draft: false
There are multiple ways you can install cointop depending on the platform you're on.
## From source (always latest and recommended)
## From source (always latest and recommeded)
Make sure to have [go](https://golang.org/) (1.17+) installed, then do:
Make sure to have [go](https://golang.org/) (1.12+) installed, then do:
```bash
go install github.com/cointop-sh/cointop@latest
go get github.com/miguelmota/cointop
```
The cointop executable will be under your GOPATH so make sure `$GOPATH/bin` is added to the `$PATH` variable if not already.
The cointop executable will be under `~/go/bin/cointop` so make sure `$GOPATH/bin` is added to the `$PATH` variable if not already.
Now you can run cointop:
@ -25,14 +25,14 @@ cointop
## Binary (all platforms)
You can download the binary from the [releases](https://github.com/cointop-sh/cointop/releases) page.
You can download the binary from the [releases](https://github.com/miguelmota/cointop/releases) page.
```bash
curl -o- https://raw.githubusercontent.com/cointop-sh/cointop/master/install.sh | bash
curl -o- https://raw.githubusercontent.com/miguelmota/cointop/master/install.sh | bash
```
```bash
wget -qO- https://raw.githubusercontent.com/cointop-sh/cointop/master/install.sh | bash
wget -qO- https://raw.githubusercontent.com/miguelmota/cointop/master/install.sh | bash
```
## Homebrew (macOS)
@ -69,7 +69,7 @@ Note: snaps don't work in Windows WSL. See this [issue thread](https://forum.sna
cointop is available as a [copr](https://copr.fedorainfracloud.org/coprs/miguelmota/cointop/) package.
First, enable the repository
First, enable the respository
```bash
sudo dnf copr enable miguelmota/cointop -y
@ -143,11 +143,11 @@ nix-env -iA nixpkgs.cointop
## AppImage (Linux)
You can download the AppImage from the [releases](https://github.com/cointop-sh/cointop/releases) page.
You can download the AppImage from the [releases](https://github.com/miguelmota/cointop/releases) page.
```bash
VERSION=$(curl --silent "https://api.github.com/repos/cointop-sh/cointop/releases/latest" | grep -Po --color=never '"tag_name": ".\K.*?(?=")')
URL="https://github.com/cointop-sh/cointop/releases/download/v$VERSION/cointop-v$VERSION.glibc2.32-x86_64.AppImage"
VERSION=$(curl --silent "https://api.github.com/repos/miguelmota/cointop/releases/latest" | grep -Po --color=never '"tag_name": ".\K.*?(?=")')
URL="https://github.com/miguelmota/cointop/releases/download/v$VERSION/cointop-v$VERSION.glibc2.32-x86_64.AppImage"
wget $URL
```
@ -176,10 +176,10 @@ sudo pkg install cointop
Install [Go](https://golang.org/doc/install) and [git](https://git-scm.com/download/win), then:
```powershell
go get -u github.com/cointop-sh/cointop
go get -u github.com/miguelmota/cointop
```
You'll need additional font support for Windows. Please see the [wiki](https://github.com/cointop-sh/cointop/wiki/Windows-Command-Prompt-and-WSL-Font-Support) for instructions.
You'll need additional font support for Windows. Please see the [wiki](https://github.com/miguelmota/cointop/wiki/Windows-Command-Prompt-and-WSL-Font-Support) for instructions.
## Docker
@ -197,4 +197,4 @@ docker run -v ~/.cache/cointop:/root/.config/cointop -it cointop/cointop
## Binaries
You can find pre-built binaries on the [releases](https://github.com/cointop-sh/cointop/releases) page.
You can find pre-built binaries on the [releases](https://github.com/miguelmota/cointop/releases) page.

@ -7,6 +7,6 @@ draft: false
[![BTC Tip Jar](https://img.shields.io/badge/BTC-tip-yellow.svg?logo=bitcoin&style=flat)](https://www.blockchain.com/btc/address/3KdMW53vUMLPEC33xhHAUx4EFtvmXQF8Kf) `3KdMW53vUMLPEC33xhHAUx4EFtvmXQF8Kf`
[![ETH Tip Jar](https://img.shields.io/badge/ETH-tip-blue.svg?logo=ethereum&style=flat)](https://etherscan.io/address/0xC014b8F6F43f467922E93De62C9216F0538E0F8f) `0xC014b8F6F43f467922E93De62C9216F0538E0F8f`
[![ETH Tip Jar](https://img.shields.io/badge/ETH-tip-blue.svg?logo=ethereum&style=flat)](https://etherscan.io/address/0x0072cdd7c3d9963ba69506ECf50e16E963B35bb1) `0x0072cdd7c3d9963ba69506ECf50e16E963B35bb1`
Thank you for tips! 🙏

@ -10,7 +10,7 @@ draft: false
To update make sure to use the `-u` flag if installed via Go.
```bash
go get -u github.com/cointop-sh/cointop
go get -u github.com/miguelmota/cointop
```
## Homebrew (macOS)
@ -48,7 +48,8 @@ sudo xbps-install -Su cointop
## Flatpak (Linux)
```bash
flatpak update com.github.miguelmota.Cointop
sudo flatpak uninstall com.github.miguelmota.Cointop
sudo flatpak install flathub com.github.miguelmota.Cointop
```
## NixOS (Linux)

@ -1,44 +0,0 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1631561581,
"narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19",
"type": "github"
},
"original": {
"owner": "numtide",
"ref": "7e5bf3925",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1638110343,
"narHash": "sha256-hQaow8sGPyUrXgrqgDRsfA+73uR0vms2goTQNxIAaRQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "942eb9a335b4cd22fa6a7be31c494e53e76f5637",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

@ -1,38 +0,0 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
rec {
packages = flake-utils.lib.flattenTree {
cointop = let lib = pkgs.lib; in
pkgs.buildGo117Module {
pname = "cointop";
version = "1.6.9";
modSha256 = lib.fakeSha256;
vendorSha256 = null;
src = ./.;
meta = {
description = "A fast and lightweight interactive terminal based UI application for tracking cryptocurrencies 🚀";
homepage = "https://cointop.sh/";
license = lib.licenses.mit;
maintainers = [ "johnrichardrinehart" ]; # flake maintainers, not project maintainers
platforms = lib.platforms.linux ++ lib.platforms.darwin;
};
};
};
defaultPackage = packages.cointop;
defaultApp = packages.cointop;
}
);
}

@ -1,46 +1,30 @@
module github.com/cointop-sh/cointop
go 1.17
module github.com/miguelmota/cointop
require (
github.com/BurntSushi/toml v0.4.1
github.com/BurntSushi/toml v0.3.1
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/antonmedv/expr v1.9.0
github.com/creack/pty v1.1.17
github.com/fatih/color v1.13.0
github.com/gdamore/tcell/v2 v2.4.0
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1
github.com/gliderlabs/ssh v0.3.3
github.com/goodsign/monday v1.0.0
github.com/jeandeaual/go-locale v0.0.0-20211014152413-b809787f45c8
github.com/mattn/go-runewidth v0.0.13
github.com/miguelmota/go-coinmarketcap v0.1.8
github.com/mitchellh/go-wordwrap v1.0.1
github.com/olekukonko/tablewriter v0.0.5
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
golang.org/x/text v0.3.7
)
require (
github.com/anaskhan96/soup v1.2.4 // indirect
github.com/anaskhan96/soup v1.1.1 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/gopherjs/gopherjs v0.0.0-20211111143520-d0d5ecc1a356 // indirect
github.com/gopherjs/gopherwasm v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/creack/pty v1.1.11
github.com/fatih/color v1.9.0
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28
github.com/gliderlabs/ssh v0.3.0
github.com/maruel/panicparse v1.5.0
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.9
github.com/miguelmota/go-coinmarketcap v0.1.7
github.com/miguelmota/gocui v0.4.2
github.com/miguelmota/termbox-go v0.0.0-20191229070316-58d4fcbce2a7
github.com/mitchellh/go-wordwrap v1.0.0
github.com/olekukonko/tablewriter v0.0.4
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed // indirect
golang.org/x/text v0.3.3
)
go 1.13

745
go.sum

@ -1,706 +1,207 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
fyne.io/fyne v1.4.2/go.mod h1:xL4c3WmpE/Tvz5CEm5vqsaizU/EeOCm9DYlL2GtTSiM=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anaskhan96/soup v1.0.1/go.mod h1:pT5vs4HXDwA5y4KQCsKvnkpQd3D+joP7IqpiGskfWW0=
github.com/anaskhan96/soup v1.2.4 h1:or+sKs9QbzJGZVTYFmTs2VBateEywoq00a6K14z331E=
github.com/anaskhan96/soup v1.2.4/go.mod h1:6YnEp9A2yywlYdM4EgDz9NEHclocMepEtku7wg6Cq3s=
github.com/anaskhan96/soup v1.1.1 h1:Duux/0htS2Va7XLJ9qIakCSey790hg9OFRm2FwlMTy0=
github.com/anaskhan96/soup v1.1.1/go.mod h1:pT5vs4HXDwA5y4KQCsKvnkpQd3D+joP7IqpiGskfWW0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/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/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fyne-io/mobile v0.1.2-0.20201127155338-06aeb98410cc/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 h1:Xh9mvwEmhbdXlRSsgn+N0zj/NqnKvpeqL08oKDHln2s=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA=
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/gliderlabs/ssh v0.3.0 h1:7GcKy4erEljCE/QeQ2jTVpu+3f3zkpZOxOJjFYkMqYU=
github.com/gliderlabs/ssh v0.3.0/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20211111143520-d0d5ecc1a356 h1:d3wWSjdOuGrMHa8+Tvw3z9EGPzATpzVq1BmGK3+IyeU=
github.com/gopherjs/gopherjs v0.0.0-20211111143520-d0d5ecc1a356/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI=
github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/jeandeaual/go-locale v0.0.0-20211014152413-b809787f45c8 h1:t3zg0eJ2qUP6yqqcwicCBqqaQVKs3ul4n27CAcyh0aw=
github.com/jeandeaual/go-locale v0.0.0-20211014152413-b809787f45c8/go.mod h1:3/uOR/xyUPi69BwdDezaGEixFZOspXUmKujIOg2r8JM=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/maruel/panicparse v1.5.0 h1:etK4QAf/Spw8eyowKbOHRkOfhblp/kahGUy96RvbMjI=
github.com/maruel/panicparse v1.5.0/go.mod h1:aOutY/MUjdj80R0AEVI9qE2zHqig+67t2ffUDDiLzAM=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miguelmota/go-coinmarketcap v0.1.8 h1:rZhB7xs1j7qxxd1zftjADhAv6ECJQVhBom1dh3zURKY=
github.com/miguelmota/go-coinmarketcap v0.1.8/go.mod h1:hBjej1IiB5+pfj+0cZhnxRkAc2bgky8qWLhCJTQ3zjw=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miguelmota/go-coinmarketcap v0.1.7 h1:9kTFWMom73IuGXqacD/LYPiUeX1qLpuLH8BhceHXYt0=
github.com/miguelmota/go-coinmarketcap v0.1.7/go.mod h1:hBjej1IiB5+pfj+0cZhnxRkAc2bgky8qWLhCJTQ3zjw=
github.com/miguelmota/gocui v0.4.2 h1:nMYnYn3RjV7FlWFcidQa9eAkX3kT7XMI6yJMxEkAz6s=
github.com/miguelmota/gocui v0.4.2/go.mod h1:wVtmhuLR+VAS9VRBIJZBNJS9IgH+9QOZ/m/MvRarOZ4=
github.com/miguelmota/termbox-go v0.0.0-20191229070316-58d4fcbce2a7 h1:sZmjSV25xMXIGAaATVuOtC9VtGHMydXpd9OejNaTxQE=
github.com/miguelmota/termbox-go v0.0.0-20191229070316-58d4fcbce2a7/go.mod h1:DRZE481VrAygaB/4DTvG0To/HsucthXAu0sY1Exb7gw=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e h1:Ee+VZw13r9NTOMnwTPs6O5KZ0MJU54hsxu9FpZ4pQ10=
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e/go.mod h1:fSIW/szJHsRts/4U8wlMPhs+YqJC+7NYR+Qqb1uJVpA=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180215212450-dc948dff8834/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/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/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed h1:WBkVNH1zd9jg/dK4HCM4lNANnmd12EHC9z+LmcCG4ns=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

@ -1,6 +1,6 @@
#!/bin/bash
VERSION=$(curl --silent "https://api.github.com/repos/cointop-sh/cointop/releases/latest" | grep -Po --color=never '"tag_name": ".\K.*?(?=")')
VERSION=$(curl --silent "https://api.github.com/repos/miguelmota/cointop/releases/latest" | grep -Po --color=never '"tag_name": ".\K.*?(?=")')
OSNAME="linux"
if [[ $(uname) == 'Darwin' ]]; then
@ -9,7 +9,7 @@ fi
(
cd /tmp
wget https://github.com/cointop-sh/cointop/releases/download/v${VERSION}/cointop_${VERSION}_${OSNAME}_amd64.tar.gz
wget https://github.com/miguelmota/cointop/releases/download/v${VERSION}/cointop_${VERSION}_${OSNAME}_amd64.tar.gz
tar -xvzf cointop_${VERSION}_${OSNAME}_amd64.tar.gz cointop
sudo mv cointop /usr/local/bin/cointop

@ -1,7 +1,7 @@
package main
import (
cmd "github.com/cointop-sh/cointop/cmd/commands"
cmd "github.com/miguelmota/cointop/cmd/commands"
)
func main() {

@ -1,17 +1,10 @@
package api
import (
cg "github.com/cointop-sh/cointop/pkg/api/impl/coingecko"
cmc "github.com/cointop-sh/cointop/pkg/api/impl/coinmarketcap"
cg "github.com/miguelmota/cointop/pkg/api/impl/coingecko"
cmc "github.com/miguelmota/cointop/pkg/api/impl/coinmarketcap"
)
type CoinGeckoConfig struct {
PerPage uint
MaxPages uint
ApiKey string
ProApiKey string
}
// NewCMC new CoinMarketCap API
func NewCMC(apiKey string) Interface {
return cmc.NewCMC(apiKey)
@ -23,11 +16,9 @@ func NewCC() {
}
// NewCG new CoinGecko API
func NewCG(config *CoinGeckoConfig) Interface {
func NewCG(perPage, maxPages uint) Interface {
return cg.NewCoinGecko(&cg.Config{
PerPage: config.PerPage,
MaxPages: config.MaxPages,
ApiKey: config.ApiKey,
ProApiKey: config.ProApiKey,
PerPage: perPage,
MaxPages: maxPages,
})
}

@ -9,11 +9,10 @@ import (
"sync"
"time"
apitypes "github.com/cointop-sh/cointop/pkg/api/types"
"github.com/cointop-sh/cointop/pkg/api/util"
gecko "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3"
"github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types"
geckoTypes "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types"
apitypes "github.com/miguelmota/cointop/pkg/api/types"
util "github.com/miguelmota/cointop/pkg/api/util"
gecko "github.com/miguelmota/cointop/pkg/api/vendors/coingecko/v3"
geckoTypes "github.com/miguelmota/cointop/pkg/api/vendors/coingecko/v3/types"
)
// ErrPingFailed is the error for when pinging the API fails
@ -24,10 +23,8 @@ var ErrNotFound = errors.New("not found")
// Config config
type Config struct {
PerPage uint
MaxPages uint
ApiKey string
ProApiKey string
PerPage uint
MaxPages uint
}
// Service service
@ -36,15 +33,14 @@ type Service struct {
maxResultsPerPage uint
maxPages uint
cacheMap sync.Map
cachedRates *types.ExchangeRatesItem
}
// NewCoinGecko new service
func NewCoinGecko(config *Config) *Service {
maxResultsPerPage := 250 // absolute max
maxResults := uint(0)
maxPages := uint(10)
perPage := uint(100)
var maxResultsPerPage uint = 250 // absolute max
var maxResults uint = 0
var maxPages uint = 10
var perPage uint = 100
if config.PerPage > 0 {
perPage = config.PerPage
}
@ -54,7 +50,7 @@ func NewCoinGecko(config *Config) *Service {
maxPages = uint(math.Ceil(math.Max(float64(maxResults)/float64(maxResultsPerPage), 1)))
}
client := gecko.NewClient(nil, config.ApiKey, config.ProApiKey)
client := gecko.NewClient(nil)
svc := &Service{
client: client,
maxResultsPerPage: uint(math.Min(float64(maxResults), float64(maxResultsPerPage))),
@ -150,45 +146,6 @@ func (s *Service) GetCoinGraphData(convert, symbol, name string, start, end int6
return ret, nil
}
// GetExchangeRates returns the exchange rates from the backend, or a cached copy if requested and available
func (s *Service) GetExchangeRates(cached bool) (*types.ExchangeRatesItem, error) {
if s.cachedRates == nil || !cached {
rates, err := s.client.ExchangeRates()
if err != nil {
return nil, err
}
s.cachedRates = rates
}
return s.cachedRates, nil
}
// GetExchangeRate gets the current exchange rate between two currencies
func (s *Service) GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) {
convertFrom = strings.ToLower(convertFrom)
convertTo = strings.ToLower(convertTo)
if convertFrom == convertTo {
return 1.0, nil
}
rates, err := s.GetExchangeRates(cached)
if err != nil {
return 0, err
}
if rates == nil {
return 0, fmt.Errorf("expected rates, received nil")
}
// Combined rate is convertFrom->BTC->convertTo
fromRate, found := (*rates)[convertFrom]
if !found {
return 0, fmt.Errorf("unsupported currency conversion: %s", convertFrom)
}
toRate, found := (*rates)[convertTo]
if !found {
return 0, fmt.Errorf("unsupported currency conversion: %s", convertTo)
}
rate := toRate.Value / fromRate.Value
return rate, nil
}
// GetGlobalMarketGraphData gets global market graph data
func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int64) (apitypes.MarketGraph, error) {
days := strconv.Itoa(util.CalcDays(start, end))
@ -197,14 +154,7 @@ func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int6
if convertTo == "" {
convertTo = "usd"
}
graphData, err := s.client.GlobalCharts("usd", days)
if err != nil {
return ret, err
}
// This API does not appear to support vs_currency and only returns USD, so use ExchangeRates to convert
// TODO: watch out - this is not cached, so we hit the backend every time!
rate, err := s.GetExchangeRate("usd", convertTo, true)
graphData, err := s.client.GlobalCharts(convertTo, days)
if err != nil {
return ret, err
}
@ -215,7 +165,7 @@ func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int6
for _, item := range *graphData.Stats {
marketCapUSD = append(marketCapUSD, []float64{
float64(item[0]),
float64(item[1]) * rate,
float64(item[1]),
})
}
}
@ -269,13 +219,15 @@ func (s *Service) Price(name string, convert string) (float64, error) {
return 0, ErrNotFound
}
func (s *Service) CoinLink(slug string) string {
// slug is API ID of coin
return fmt.Sprintf("https://www.coingecko.com/en/coins/%s", slug)
// CoinLink returns the URL link for the coin
func (s *Service) CoinLink(name string) string {
ID := s.coinNameToID(name)
return fmt.Sprintf("https://www.coingecko.com/en/coins/%s", ID)
}
// SupportedCurrencies returns a list of supported currencies
func (s *Service) SupportedCurrencies() []string {
// keep these in alphabetical order
return []string{
"AED",
@ -316,7 +268,6 @@ func (s *Service) SupportedCurrencies() []string {
"PLN",
"RUB",
"SAR",
"SATS",
"SEK",
"SGD",
"THB",
@ -341,7 +292,7 @@ func (s *Service) cacheCoinsIDList() error {
if list == nil {
return nil
}
var firstWords [][]string
firstWords := [][]string{}
for _, item := range *list {
keys := []string{
strings.ToLower(item.Name),
@ -392,7 +343,6 @@ func (s *Service) getPaginatedCoinData(convert string, offset int, names []strin
pcp.PCP24h,
pcp.PCP7d,
pcp.PCP30d,
pcp.PCP1y,
}
order := geckoTypes.OrderTypeObject.MarketCapDesc
convertTo := strings.ToLower(convert)
@ -422,7 +372,6 @@ func (s *Service) getPaginatedCoinData(convert string, offset int, names []strin
var percentChange24H float64
var percentChange7D float64
var percentChange30D float64
var percentChange1Y float64
if item.PriceChangePercentage1hInCurrency != nil {
percentChange1H = *item.PriceChangePercentage1hInCurrency
@ -436,9 +385,6 @@ func (s *Service) getPaginatedCoinData(convert string, offset int, names []strin
if item.PriceChangePercentage30dInCurrency != nil {
percentChange30D = *item.PriceChangePercentage30dInCurrency
}
if item.PriceChangePercentage1yInCurrency != nil {
percentChange1Y = *item.PriceChangePercentage1yInCurrency
}
availableSupply := item.CirculatingSupply
totalSupply := item.TotalSupply
@ -459,10 +405,8 @@ func (s *Service) getPaginatedCoinData(convert string, offset int, names []strin
PercentChange24H: util.FormatPercentChange(percentChange24H),
PercentChange7D: util.FormatPercentChange(percentChange7D),
PercentChange30D: util.FormatPercentChange(percentChange30D),
PercentChange1Y: util.FormatPercentChange(percentChange1Y),
Volume24H: util.FormatVolume(item.TotalVolume),
LastUpdated: util.FormatLastUpdated(item.LastUpdated),
Slug: util.FormatSlug(item.ID),
})
}
}

@ -11,8 +11,8 @@ import (
"strings"
"time"
apitypes "github.com/cointop-sh/cointop/pkg/api/types"
"github.com/cointop-sh/cointop/pkg/api/util"
apitypes "github.com/miguelmota/cointop/pkg/api/types"
util "github.com/miguelmota/cointop/pkg/api/util"
cmc "github.com/miguelmota/go-coinmarketcap/pro/v1"
cmcv2 "github.com/miguelmota/go-coinmarketcap/v2"
)
@ -77,7 +77,7 @@ func (s *Service) getPaginatedCoinData(convert string, offset int) ([]apitypes.C
}
ret = append(ret, apitypes.Coin{
ID: util.FormatID(fmt.Sprint(v.ID)),
ID: util.FormatID(v.Name),
Name: util.FormatName(v.Name),
Symbol: util.FormatSymbol(v.Symbol),
Rank: util.FormatRank(v.CMCRank),
@ -90,7 +90,6 @@ func (s *Service) getPaginatedCoinData(convert string, offset int) ([]apitypes.C
PercentChange7D: util.FormatPercentChange(quote.PercentChange7D),
Volume24H: util.FormatVolume(v.Quote[convert].Volume24H),
LastUpdated: util.FormatLastUpdated(v.LastUpdated),
Slug: util.FormatSlug(v.Slug),
})
}
return ret, nil
@ -136,7 +135,7 @@ func (s *Service) GetCoinData(name string, convert string) (apitypes.Coin, error
// GetCoinDataBatch gets all data of specified coins.
func (s *Service) GetCoinDataBatch(names []string, convert string) ([]apitypes.Coin, error) {
var ret []apitypes.Coin
ret := []apitypes.Coin{}
coins, err := s.getPaginatedCoinData(convert, 0)
if err != nil {
return ret, err
@ -298,6 +297,7 @@ func (s *Service) GetGlobalMarketData(convert string) (apitypes.GlobalMarketData
market, err := s.client.GlobalMetrics.LatestQuotes(&cmc.QuoteOptions{
Convert: convert,
})
if err != nil {
return ret, err
}
@ -332,8 +332,9 @@ func (s *Service) Price(name string, convert string) (float64, error) {
}
// CoinLink returns the URL link for the coin
func (s *Service) CoinLink(slug string) string {
return fmt.Sprintf("https://coinmarketcap.com/currencies/%s/", slug)
func (s *Service) CoinLink(name string) string {
slug := util.NameToSlug(name)
return fmt.Sprintf("https://coinmarketcap.com/currencies/%s", slug)
}
// SupportedCurrencies returns a list of supported currencies
@ -429,11 +430,3 @@ func getChartInterval(start, end int64) string {
}
return interval
}
// GetExchangeRate gets the current exchange rate between two currencies
func (s *Service) GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) {
if convertFrom == convertTo {
return 1.0, nil
}
return 0, fmt.Errorf("unsupported currency conversion: %s => %s", convertFrom, convertTo)
}

@ -1,7 +1,7 @@
package api
import (
"github.com/cointop-sh/cointop/pkg/api/types"
types "github.com/miguelmota/cointop/pkg/api/types"
)
// Interface interface
@ -13,8 +13,10 @@ type Interface interface {
GetGlobalMarketData(convert string) (types.GlobalMarketData, error)
GetCoinData(name string, convert string) (types.Coin, error)
GetCoinDataBatch(names []string, convert string) ([]types.Coin, error)
CoinLink(slug string) string
//GetAltcoinMarketGraphData(start int64, end int64) (types.MarketGraph, error)
//GetCoinPriceUSD(coin string) (float64, error)
//GetCoinMarkets(coin string) ([]types.Market, error)
CoinLink(name string) string
SupportedCurrencies() []string
Price(name string, convert string) (float64, error)
GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) // I don't love this caching
}

@ -15,10 +15,7 @@ type Coin struct {
PercentChange24H float64 `json:"percentChange24H"`
PercentChange7D float64 `json:"percentChange7D"`
PercentChange30D float64 `json:"percentChange30D"`
PercentChange1Y float64 `json:"percentChange1Y"`
LastUpdated string `json:"lastUpdated"`
// Slug uses to access the coin's info web page
Slug string `json:"slug"`
}
// GlobalMarketData struct

@ -29,10 +29,6 @@ func FormatName(name string) string {
return name
}
func FormatSlug(slug string) string {
return slug
}
// FormatRank formats the rank value
func FormatRank(rank interface{}) int {
switch v := rank.(type) {
@ -64,7 +60,7 @@ func FormatRank(rank interface{}) int {
// FormatPrice formats the price value
func FormatPrice(price float64, convert string) float64 {
convert = strings.ToUpper(convert)
pricestr := fmt.Sprintf("%.5f", price)
pricestr := fmt.Sprintf("%.2f", price)
if convert == "ETH" || convert == "BTC" || price < 1 {
pricestr = fmt.Sprintf("%.8f", price)
}

@ -9,36 +9,28 @@ import (
"net/url"
"strings"
"os"
"github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/format"
"github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types"
log "github.com/sirupsen/logrus"
"github.com/miguelmota/cointop/pkg/api/vendors/coingecko/format"
"github.com/miguelmota/cointop/pkg/api/vendors/coingecko/v3/types"
)
var baseURL = "https://api.coingecko.com/api/v3"
// Client struct
type Client struct {
httpClient *http.Client
apiKey string
proApiKey string
}
// NewClient create new client object
func NewClient(httpClient *http.Client, apiKey string, proApiKey string) *Client {
func NewClient(httpClient *http.Client) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &Client{httpClient: httpClient, apiKey: apiKey, proApiKey: proApiKey}
return &Client{httpClient: httpClient}
}
// helper
// doReq HTTP client
func doReq(req *http.Request, client *http.Client) ([]byte, error) {
debugHttp := os.Getenv("DEBUG_HTTP") != ""
if debugHttp {
log.Debugf("doReq %s %s", req.Method, req.URL)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
@ -48,33 +40,12 @@ func doReq(req *http.Request, client *http.Client) ([]byte, error) {
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
if debugHttp {
log.Warnf("doReq Got Status '%s' from %s %s", resp.Status, req.Method, req.URL)
log.Debugf("doReq Got Body: %s", body)
}
if 200 != resp.StatusCode {
return nil, fmt.Errorf("%s", body)
}
return body, nil
}
func (c *Client) getApiUrl(path string, params *url.Values) string {
urlParams := url.Values{}
subdomain := "api"
if params != nil {
urlParams = *params
}
if c.apiKey != "" {
urlParams.Add("x_cg_demo_api_key", c.apiKey)
}
if c.proApiKey != "" {
subdomain = "pro-api"
urlParams.Add("x_cg_pro_api_key", c.proApiKey)
}
url := fmt.Sprintf("https://%s.coingecko.com/api/v3%s?%s", subdomain, path, urlParams.Encode())
return url
}
// MakeReq HTTP request helper
func (c *Client) MakeReq(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
@ -93,7 +64,7 @@ func (c *Client) MakeReq(url string) ([]byte, error) {
// Ping /ping endpoint
func (c *Client) Ping() (*types.Ping, error) {
url := c.getApiUrl("/ping", nil)
url := fmt.Sprintf("%s/ping", baseURL)
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -122,14 +93,14 @@ func (c *Client) SimpleSinglePrice(id string, vsCurrency string) (*types.SimpleS
// SimplePrice /simple/price Multiple ID and Currency (ids, vs_currencies)
func (c *Client) SimplePrice(ids []string, vsCurrencies []string) (*map[string]map[string]float32, error) {
params := &url.Values{}
params := url.Values{}
idsParam := strings.Join(ids[:], ",")
vsCurrenciesParam := strings.Join(vsCurrencies[:], ",")
params.Add("ids", idsParam)
params.Add("vs_currencies", vsCurrenciesParam)
url := c.getApiUrl("/simple/price", params)
url := fmt.Sprintf("%s/simple/price?%s", baseURL, params.Encode())
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -146,7 +117,7 @@ func (c *Client) SimplePrice(ids []string, vsCurrencies []string) (*map[string]m
// SimpleSupportedVSCurrencies /simple/supported_vs_currencies
func (c *Client) SimpleSupportedVSCurrencies() (*types.SimpleSupportedVSCurrencies, error) {
url := c.getApiUrl("/simple/supported_vs_currencies", nil)
url := fmt.Sprintf("%s/simple/supported_vs_currencies", baseURL)
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -162,7 +133,7 @@ func (c *Client) SimpleSupportedVSCurrencies() (*types.SimpleSupportedVSCurrenci
// CoinsList /coins/list
func (c *Client) CoinsList() (*types.CoinList, error) {
url := c.getApiUrl("/coins/list", nil)
url := fmt.Sprintf("%s/coins/list", baseURL)
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -181,7 +152,7 @@ func (c *Client) CoinsMarket(vsCurrency string, ids []string, order string, perP
if len(vsCurrency) == 0 {
return nil, fmt.Errorf("vsCurrency is required")
}
params := &url.Values{}
params := url.Values{}
// vsCurrency
params.Add("vs_currency", vsCurrency)
// order
@ -207,7 +178,7 @@ func (c *Client) CoinsMarket(vsCurrency string, ids []string, order string, perP
priceChangePercentageParam := strings.Join(priceChangePercentage[:], ",")
params.Add("price_change_percentage", priceChangePercentageParam)
}
url := c.getApiUrl("/coins/markets", params)
url := fmt.Sprintf("%s/coins/markets?%s", baseURL, params.Encode())
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -226,14 +197,14 @@ func (c *Client) CoinsID(id string, localization bool, tickers bool, marketData
if len(id) == 0 {
return nil, fmt.Errorf("id is required")
}
params := &url.Values{}
params.Add("localization", format.Bool2String(localization))
params := url.Values{}
params.Add("localization", format.Bool2String(sparkline))
params.Add("tickers", format.Bool2String(tickers))
params.Add("market_data", format.Bool2String(marketData))
params.Add("community_data", format.Bool2String(communityData))
params.Add("developer_data", format.Bool2String(developerData))
params.Add("sparkline", format.Bool2String(sparkline))
url := c.getApiUrl(fmt.Sprintf("/coins/%s", id), params)
url := fmt.Sprintf("%s/coins/%s?%s", baseURL, id, params.Encode())
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -252,11 +223,11 @@ func (c *Client) CoinsIDTickers(id string, page int) (*types.CoinsIDTickers, err
if len(id) == 0 {
return nil, fmt.Errorf("id is required")
}
params := &url.Values{}
params := url.Values{}
if page > 0 {
params.Add("page", format.Int2String(page))
}
url := c.getApiUrl(fmt.Sprintf("/coins/%s/tickers", id), params)
url := fmt.Sprintf("%s/coins/%s/tickers?%s", baseURL, id, params.Encode())
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -274,11 +245,11 @@ func (c *Client) CoinsIDHistory(id string, date string, localization bool) (*typ
if len(id) == 0 || len(date) == 0 {
return nil, fmt.Errorf("id and date is required")
}
params := &url.Values{}
params := url.Values{}
params.Add("date", date)
params.Add("localization", format.Bool2String(localization))
url := c.getApiUrl(fmt.Sprintf("/coins/%s/history", id), params)
url := fmt.Sprintf("%s/coins/%s/history?%s", baseURL, id, params.Encode())
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -297,11 +268,11 @@ func (c *Client) CoinsIDMarketChart(id string, vsCurrency string, days string) (
return nil, fmt.Errorf("id, vsCurrency, and days is required")
}
params := &url.Values{}
params := url.Values{}
params.Add("vs_currency", vsCurrency)
params.Add("days", days)
url := c.getApiUrl(fmt.Sprintf("/coins/%s/market_chart", id), params)
url := fmt.Sprintf("%s/coins/%s/market_chart?%s", baseURL, id, params.Encode())
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -320,7 +291,7 @@ func (c *Client) CoinsIDMarketChart(id string, vsCurrency string, days string) (
// CoinsIDContractAddress https://api.coingecko.com/api/v3/coins/{id}/contract/{contract_address}
// func CoinsIDContractAddress(id string, address string) (nil, error) {
// url := c.getApiUrl(fmt.Sprintf("/coins/%s/contract/%s", id, address), nil)
// url := fmt.Sprintf("%s/coins/%s/contract/%s", baseURL, id, address)
// resp, err := request.MakeReq(url)
// if err != nil {
// return nil, err
@ -329,7 +300,7 @@ func (c *Client) CoinsIDMarketChart(id string, vsCurrency string, days string) (
// EventsCountries https://api.coingecko.com/api/v3/events/countries
func (c *Client) EventsCountries() ([]types.EventCountryItem, error) {
url := c.getApiUrl("/events/countries", nil)
url := fmt.Sprintf("%s/events/countries", baseURL)
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -345,7 +316,7 @@ func (c *Client) EventsCountries() ([]types.EventCountryItem, error) {
// EventsTypes https://api.coingecko.com/api/v3/events/types
func (c *Client) EventsTypes() (*types.EventsTypes, error) {
url := c.getApiUrl("/events/types", nil)
url := fmt.Sprintf("%s/events/types", baseURL)
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -361,7 +332,7 @@ func (c *Client) EventsTypes() (*types.EventsTypes, error) {
// ExchangeRates https://api.coingecko.com/api/v3/exchange_rates
func (c *Client) ExchangeRates() (*types.ExchangeRatesItem, error) {
url := c.getApiUrl("/exchange_rates", nil)
url := fmt.Sprintf("%s/exchange_rates", baseURL)
resp, err := c.MakeReq(url)
if err != nil {
return nil, err
@ -376,7 +347,7 @@ func (c *Client) ExchangeRates() (*types.ExchangeRatesItem, error) {
// Global https://api.coingecko.com/api/v3/global
func (c *Client) Global() (*types.Global, error) {
url := c.getApiUrl("/global", nil)
url := fmt.Sprintf("%s/global", baseURL)
resp, err := c.MakeReq(url)
if err != nil {
return nil, err

@ -1,7 +1,9 @@
package chartplot
import (
"github.com/cointop-sh/cointop/pkg/termui"
"math"
"github.com/miguelmota/cointop/pkg/termui"
)
// ChartPlot ...
@ -51,23 +53,13 @@ func (c *ChartPlot) SetBorder(enabled bool) {
func (c *ChartPlot) SetData(data []float64) {
// NOTE: edit `termui.LineChart.shortenFloatVal(float64)` to not
// use exponential notation.
// NOTE: data should be the correct width for rendering - see GetChartDataSize()
c.t.Data = data
}
// SetDataLabels ...
func (c *ChartPlot) SetDataLabels(labels []string) {
c.t.DataLabels = labels
}
// GetChartDataSize ...
func (c *ChartPlot) GetChartDataSize(width int) int {
axisYWidth := 30
return (width * 2) - axisYWidth
}
// GetChartPoints ...
func (c *ChartPlot) GetChartPoints(width int) [][]rune {
axisYWidth := 30
c.t.Data = interpolateData(c.t.Data, (width*2)-axisYWidth)
termui.Body = termui.NewGrid()
termui.Body.Width = width
termui.Body.AddRows(
@ -94,3 +86,24 @@ func (c *ChartPlot) GetChartPoints(width int) [][]rune {
return points
}
func interpolateData(data []float64, width int) []float64 {
var res []float64
if len(data) == 0 {
return res
}
stepFactor := float64(len(data)-1) / float64(width-1)
res = append(res, data[0])
for i := 1; i < width-1; i++ {
step := float64(i) * stepFactor
before := math.Floor(step)
after := math.Ceil(step)
atPoint := step - before
pointBefore := data[int(before)]
pointAfter := data[int(after)]
interpolated := pointBefore + (pointAfter-pointBefore)*atPoint
res = append(res, interpolated)
}
res = append(res, data[len(data)-1])
return res
}

@ -0,0 +1,39 @@
package color
import "github.com/fatih/color"
// Color struct
type Color color.Color
var (
// Bold color
Bold = color.New(color.Bold).SprintFunc()
// Black color
Black = color.New(color.FgBlack).SprintFunc()
// BlackBg color
BlackBg = color.New(color.BgBlack, color.FgWhite).SprintFunc()
// White color
White = color.New(color.FgWhite).SprintFunc()
// WhiteBold bold
WhiteBold = color.New(color.FgWhite, color.Bold).SprintFunc()
// Yellow color
Yellow = color.New(color.FgYellow).SprintFunc()
// YellowBold color
YellowBold = color.New(color.FgYellow, color.Bold).SprintFunc()
// YellowBg color
YellowBg = color.New(color.BgYellow, color.FgBlack).SprintFunc()
// Green color
Green = color.New(color.FgGreen).SprintFunc()
// GreenBg color
GreenBg = color.New(color.BgGreen, color.FgBlack).SprintFunc()
// Red color
Red = color.New(color.FgRed).SprintFunc()
// Cyan color
Cyan = color.New(color.FgCyan).SprintFunc()
// CyanBg color
CyanBg = color.New(color.BgCyan, color.FgBlack).SprintFunc()
// Blue color
Blue = color.New(color.FgBlue).SprintFunc()
// BlueBg color
BlueBg = color.New(color.BgBlue).SprintFunc()
)

@ -1,65 +0,0 @@
package eval
import (
"errors"
"strings"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/ast"
)
// AST Visitor that changes Integer to Float
type patcher struct{}
func (p *patcher) Enter(_ *ast.Node) {}
func (p *patcher) Exit(node *ast.Node) {
n, ok := (*node).(*ast.IntegerNode)
if ok {
ast.Patch(node, &ast.FloatNode{Value: float64(n.Value)})
}
}
// EvaluateExpressionToFloat64 evaulates a simple math expression string to a float64
func EvaluateExpressionToFloat64(input string, env interface{}) (float64, error) {
input = strings.TrimSpace(input)
if input == "" {
return 0, nil
}
program, err := expr.Compile(input, expr.Env(env), expr.Patch(&patcher{}))
if err != nil {
return 0, err
}
result, err := expr.Run(program, env)
if err != nil {
return 0, err
}
f64, ok := result.(float64)
if !ok {
ival, ok := result.(int)
if !ok {
return 0, errors.New("expression did not return numeric type")
}
f64 = float64(ival)
}
return f64, nil
}
func EvaluateExpressionToString(input string, env interface{}) (string, error) {
input = strings.TrimSpace(input)
if input == "" {
return "", nil
}
program, err := expr.Compile(input, expr.Env(env))
if err != nil {
return "", err
}
result, err := expr.Run(program, env)
if err != nil {
return "", err
}
s, ok := result.(string)
if !ok {
return "", errors.New("expression did not return string type")
}
return s, nil
}

@ -16,11 +16,10 @@ import (
)
// DefaultCacheDir ...
var DefaultCacheDir = ":PREFERRED_CACHE_HOME:/cointop"
var DefaultCacheDir = "/tmp"
// FileCache ...
type FileCache struct {
mapLock sync.Mutex
muts map[string]*sync.Mutex
prefix string
cacheDir string
@ -57,17 +56,12 @@ func NewFileCache(config *Config) (*FileCache, error) {
// Set writes item to cache
func (f *FileCache) Set(key string, data interface{}, expire time.Duration) error {
var mu *sync.Mutex
var ok bool
f.mapLock.Lock()
if mu, ok = f.muts[key]; !ok {
if _, ok := f.muts[key]; !ok {
f.muts[key] = new(sync.Mutex)
mu = f.muts[key]
}
f.mapLock.Unlock()
mu.Lock()
defer mu.Unlock()
f.muts[key].Lock()
defer f.muts[key].Unlock()
key = regexp.MustCompile("[^a-zA-Z0-9_-]").ReplaceAllLiteralString(key, "")
if f.prefix != "" {
@ -84,6 +78,9 @@ func (f *FileCache) Set(key string, data interface{}, expire time.Duration) erro
return err
}
var fmutex sync.RWMutex
fmutex.Lock()
defer fmutex.Unlock()
fp, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err

@ -1,64 +0,0 @@
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocui
import (
"github.com/gdamore/tcell/v2"
)
// eventBinding are used to link a given key-press event with a handler.
type eventBinding struct {
viewName string
ev tcell.Event // ignore the Time
handler func(*Gui, *View) error
}
// newKeybinding returns a new eventBinding object for a key event.
func newKeybinding(viewname string, key tcell.Key, ch rune, mod tcell.ModMask, handler func(*Gui, *View) error) (kb *eventBinding) {
kb = &eventBinding{
viewName: viewname,
ev: tcell.NewEventKey(key, ch, mod),
handler: handler,
}
return kb
}
// newKeybinding returns a new eventBinding object for a mouse event.
func newMouseBinding(viewname string, btn tcell.ButtonMask, mod tcell.ModMask, handler func(*Gui, *View) error) (kb *eventBinding) {
kb = &eventBinding{
viewName: viewname,
ev: tcell.NewEventMouse(0, 0, btn, mod),
handler: handler,
}
return kb
}
func (kb *eventBinding) matchEvent(e tcell.Event) bool {
// TODO: check mask not ==mod?
switch tev := e.(type) {
case *tcell.EventKey:
if kbe, ok := kb.ev.(*tcell.EventKey); ok {
if tev.Key() == tcell.KeyRune {
return tev.Key() == kbe.Key() && tev.Rune() == kbe.Rune() && tev.Modifiers() == kbe.Modifiers()
}
return tev.Key() == kbe.Key() && tev.Modifiers() == kbe.Modifiers()
}
case *tcell.EventMouse:
if kbe, ok := kb.ev.(*tcell.EventMouse); ok {
return kbe.Buttons() == tev.Buttons() && kbe.Modifiers() == tev.Modifiers()
}
}
return false
}
// matchView returns if the eventBinding matches the current view.
func (kb *eventBinding) matchView(v *View) bool {
if kb.viewName == "" {
return true
}
return v != nil && kb.viewName == v.name
}

@ -2,23 +2,17 @@ package humanize
import (
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
"github.com/goodsign/monday"
"github.com/jeandeaual/go-locale"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
var cachedSystemLocale = ""
// Numericf produces a string from of the given number with give fixed precision
// in base 10 with thousands separators after every three orders of magnitude
// using thousands and decimal separator according to LC_NUMERIC; defaulting "en".
// using a thousands and decimal spearator according to LC_NUMERIC; defaulting "en".
//
// e.g. Numericf(834142.32, 2) -> "834,142.32"
func Numericf(value float64, precision int) string {
@ -27,76 +21,16 @@ func Numericf(value float64, precision int) string {
// Monetaryf produces a string from of the given number give minimum precision
// in base 10 with thousands separators after every three orders of magnitude
// using thousands and decimal separator according to LC_MONETARY; defaulting "en".
// using thousands and decimal spearator according to LC_MONETARY; defaulting "en".
//
// e.g. Monetaryf(834142.3256, 2) -> "834,142.3256"
func Monetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", false)
}
// FixedMonetaryf produces a fixed-precision monetary-value string. See Monetaryf.
func FixedMonetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", true)
}
// borrowed from go-locale/util.go
func splitLocale(locale string) (string, string) {
// Remove the encoding, if present
formattedLocale := strings.Split(locale, ".")[0]
// Normalize by replacing the hyphens with underscores
formattedLocale = strings.Replace(formattedLocale, "-", "_", -1)
// Split at the underscore
split := strings.Split(formattedLocale, "_")
language := split[0]
territory := ""
if len(split) > 1 {
territory = split[1]
}
return language, territory
}
// GetLocale returns the current locale as defined in IETF BCP 47 (e.g. "en-US").
// The envvar provided is checked first (eg LC_TIME), before the platform-specific defaults.
func getLocale(envvar string) string {
userLocale := "en-US" // default language-REGION
// First try looking up envar directly
envlang, ok := os.LookupEnv(envvar)
if ok {
language, region := splitLocale(envlang)
userLocale = language
if len(region) > 0 {
userLocale = strings.Join([]string{language, region}, "-")
}
} else {
// Then use (cached) system-specific locale
if cachedSystemLocale == "" {
if loc, err := locale.GetLocale(); err == nil {
userLocale = loc
cachedSystemLocale = loc
}
} else {
userLocale = cachedSystemLocale
}
}
return userLocale
}
// formatTimeExplicit formats the given time using the prescribed layout with the provided userLocale
func formatTimeExplicit(time time.Time, layout string, userLocale string) string {
mondayLocale := monday.Locale(strings.Replace(userLocale, "-", "_", 1))
return monday.Format(time, layout, mondayLocale)
}
// FormatTime is a dropin replacement time.Format(layout) that uses system locale + LC_TIME
func FormatTime(time time.Time, layout string) string {
return formatTimeExplicit(time, layout, getLocale("LC_TIME"))
}
// f formats given value, with precision decimal places using thousands and decimal
// f formats given value v, with d decimal places using thousands and decimal
// separator according to language found in given locale environment variable e.
// If fixed is true the decimal places are fixed to the given precision otherwise d is the
// If r is true the decimal places are fixed to the given d otherwise d is the
// minimum of decimal places until the first 0.
func f(value float64, precision int, envvar string, fixed bool) string {
parts := strings.Split(strconv.FormatFloat(value, 'f', -1, 64), ".")
@ -117,47 +51,3 @@ func f(value float64, precision int, envvar string, fixed bool) string {
format := fmt.Sprintf("%%.%df", precision)
return message.NewPrinter(lang).Sprintf(format, value)
}
// Scale returns a scaled-down version of value and a suffix to add (M,B,etc.)
func Scale(value float64) (float64, string) {
type scalingUnit struct {
value float64
suffix string
}
// quadrillion, quintrillion, sextillion, septillion, octillion, nonillion, and decillion
var scales = [...]scalingUnit{
{value: 1e12, suffix: "T"},
{value: 1e9, suffix: "B"},
{value: 1e6, suffix: "M"},
{value: 1e3, suffix: "K"},
}
for _, scale := range scales {
if math.Abs(value) > scale.value {
return value / scale.value, scale.suffix
}
}
return value, ""
}
// ScaleNumericf scales a large number down using a suffix, then formats it with the
// prescribed number of significant digits.
func ScaleNumericf(value float64, digits int) string {
value, suffix := Scale(value)
// Round the scaled value to a certain number of significant figures
var s string
if math.Abs(value) < 1 {
s = Numericf(value, digits)
} else {
numDigits := len(fmt.Sprintf("%.0f", math.Abs(value)))
if numDigits >= digits {
s = Numericf(value, 0)
} else {
s = Numericf(value, digits-numDigits)
}
}
return s + suffix
}

@ -1,94 +0,0 @@
package humanize
import (
"fmt"
"testing"
"time"
)
// TestMonetary tests monetary formatting
func TestMonetary(t *testing.T) {
if Monetaryf(834142.3256, 2) != "834,142.3256" {
t.FailNow()
}
}
func TestScale(t *testing.T) {
scaleTests := map[float64]string{
5.54 * 1e12: "5.5T",
4.44 * 1e9: "4.4B",
3.34 * 1e6: "3.3M",
2.24 * 1e3: "2.2K",
1.1: "1.1",
0.06: "0.1",
0.04: "0.0",
-5.54 * 1e12: "-5.5T",
}
for value, expected := range scaleTests {
volScale, volSuffix := Scale(value)
result := fmt.Sprintf("%.1f%s", volScale, volSuffix)
if result != expected {
t.Fatalf("Expected %f to scale to '%s' but got '%s'\n", value, expected, result)
}
}
}
func TestScaleNumeric(t *testing.T) {
scaleTests := map[float64]string{
5.54 * 1e12: "5.5T",
4.44 * 1e9: "4.4B",
3.34 * 1e6: "3.3M",
2.24 * 1e3: "2.2K",
1.1: "1.1",
0.0611: "0.06",
-5.5432 * 1e12: "-5.5T",
}
for value, expected := range scaleTests {
result := ScaleNumericf(value, 2)
if result != expected {
t.Fatalf("Expected %f to scale to '%s' but got '%s'\n", value, expected, result)
}
}
}
func TestFormatTime(t *testing.T) {
testData := map[string]map[string]string{
"en_GB": {
"Monday 2 January 2006": "Wednesday 12 March 2014",
"Jan 2006": "Mar 2014",
"02 Jan 2006": "12 Mar 2014",
"02/01/2006": "12/03/2014",
},
"en_US": {
"Monday 2 January 2006": "Wednesday 12 March 2014",
"Jan 2006": "Mar 2014",
"02 Jan 2006": "12 Mar 2014",
"02/01/2006": "12/03/2014", // ??
},
"fr_FR": {
"Monday 2 January 2006": "mercredi 12 mars 2014",
"Jan 2006": "mars 2014",
"02 Jan 2006": "12 mars 2014",
"02/01/2006": "12/03/2014",
},
"de_DE": {
"Monday 2 January 2006": "Mittwoch 12 März 2014",
"Jan 2006": "Mär 2014",
"02 Jan 2006": "12 Mär 2014",
"02/01/2006": "12/03/2014",
},
}
testTime := time.Date(2014, 3, 12, 0, 0, 0, 0, time.Local)
for locale, tests := range testData {
for layout, result := range tests {
s := formatTimeExplicit(testTime, layout, locale)
if s != result {
t.Fatalf("Expected layout '%s' in locale %s to render '%s' but got '%s'", layout, locale, result, s)
}
}
}
}

@ -45,7 +45,7 @@ func DamerauLevenshteinDistance(s1, s2 string) int {
// min returns the minimum number of passed int slices.
func min(is ...int) int {
min := math.MaxInt32
min := int(math.MaxInt32)
for _, v := range is {
if min > v {
min = v

@ -53,7 +53,6 @@ func NormalizePath(path string) string {
userHome := UserPreferredHomeDir()
userConfigHome := UserPreferredConfigDir()
userCacheHome := UserPreferredCacheDir()
userTempDir := os.TempDir()
// expand tilde
if strings.HasPrefix(path, "~/") {
@ -63,7 +62,6 @@ func NormalizePath(path string) string {
path = strings.Replace(path, ":HOME:", userHome, -1)
path = strings.Replace(path, ":PREFERRED_CONFIG_HOME:", userConfigHome, -1)
path = strings.Replace(path, ":PREFERRED_CACHE_HOME:", userCacheHome, -1)
path = strings.Replace(path, ":PREFERRED_TEMP_DIR:", userTempDir, -1)
path = strings.Replace(path, "/", string(filepath.Separator), -1)
return filepath.Clean(path)

@ -1,5 +1,4 @@
//go:build !windows
// +build !windows
//+build !windows
package ssh
@ -17,9 +16,9 @@ import (
"time"
"unsafe"
"github.com/cointop-sh/cointop/pkg/pathutil"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
"github.com/miguelmota/cointop/pkg/pathutil"
gossh "golang.org/x/crypto/ssh"
)
@ -47,7 +46,6 @@ type Config struct {
HostKeyFile string
MaxSessions uint
UserConfigType string
ColorsDir string
}
// Server is server struct
@ -62,7 +60,6 @@ type Server struct {
maxSessions uint
sessionCount uint
userConfigType string
colorsDir string
}
// NewServer returns a new server instance
@ -77,10 +74,6 @@ func NewServer(config *Config) *Server {
}
validateUserConfigType(userConfigType)
hostKeyFile = pathutil.NormalizePath(hostKeyFile)
colorsDir := pathutil.NormalizePath("~/.config/cointop/colors")
if config.ColorsDir != "" {
colorsDir = pathutil.NormalizePath(config.ColorsDir)
}
return &Server{
port: config.Port,
address: config.Address,
@ -90,7 +83,6 @@ func NewServer(config *Config) *Server {
hostKeyFile: hostKeyFile,
maxSessions: config.MaxSessions,
userConfigType: userConfigType,
colorsDir: colorsDir,
}
}
@ -161,6 +153,7 @@ func (s *Server) ListenAndServe() error {
}
configPath := fmt.Sprintf("%s/config", configDir)
colorsDir := pathutil.NormalizePath("~/.config/cointop/colors")
cmdCtx, cancelCmd := context.WithCancel(sshSession.Context())
defer cancelCmd()
@ -173,7 +166,7 @@ func (s *Server) ListenAndServe() error {
"--config",
configPath,
"--colors-dir",
s.colorsDir,
colorsDir,
}
if len(cmdUserArgs) > 0 {
@ -197,9 +190,6 @@ func (s *Server) ListenAndServe() error {
cmd := exec.CommandContext(cmdCtx, s.executableBinary, flags...)
cmd.Env = append(sshSession.Environ(), fmt.Sprintf("TERM=%s", ptyReq.Term))
if proxy, ok := os.LookupEnv("HTTPS_PROXY"); ok {
cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", proxy))
}
f, err := pty.Start(cmd)
if err != nil {
@ -242,7 +232,7 @@ func (s *Server) ListenAndServe() error {
err := s.sshServer.SetOption(ssh.HostKeyFile(s.hostKeyFile))
if err != nil {
return fmt.Errorf("error setting HostKeyFile: %s: %v", s.hostKeyFile, err)
return err
}
return s.sshServer.ListenAndServe()

@ -8,8 +8,8 @@ import (
"github.com/acarl005/stripansi"
)
// Left align left
func Left(t string, n int) string {
// AlignLeft align left
func AlignLeft(t string, n int) string {
s := stripansi.Strip(t)
slen := utf8.RuneCountInString(s)
if slen > n {
@ -19,8 +19,8 @@ func Left(t string, n int) string {
return fmt.Sprintf("%s%s", t, strings.Repeat(" ", n-slen))
}
// Right align right
func Right(t string, n int) string {
// AlignRight align right
func AlignRight(t string, n int) string {
s := stripansi.Strip(t)
slen := utf8.RuneCountInString(s)
if slen > n {
@ -30,8 +30,8 @@ func Right(t string, n int) string {
return fmt.Sprintf("%s%s", strings.Repeat(" ", n-slen), t)
}
// Center align center
func Center(t string, n int) string {
// AlignCenter align center
func AlignCenter(t string, n int) string {
s := stripansi.Strip(t)
slen := utf8.RuneCountInString(s)
if slen > n {

@ -8,8 +8,8 @@ import (
"unicode/utf8"
"github.com/acarl005/stripansi"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/table/align"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table/align"
)
// Table table
@ -205,11 +205,11 @@ func (t *Table) Fprint(w io.Writer) {
var s string
switch c.align {
case AlignLeft:
s = align.Left(c.name+" ", c.width)
s = align.AlignLeft(c.name+" ", c.width)
case AlignRight:
s = align.Right(c.name+" ", c.width)
s = align.AlignRight(c.name+" ", c.width)
case AlignCenter:
s = align.Center(c.name+" ", c.width)
s = align.AlignCenter(c.name+" ", c.width)
}
fmt.Fprintf(w, "%s", s)
@ -237,11 +237,11 @@ func (t *Table) Fprint(w io.Writer) {
var s string
switch c.align {
case AlignLeft:
s = align.Left(v, c.width)
s = align.AlignLeft(v, c.width)
case AlignRight:
s = align.Right(v, c.width)
s = align.AlignRight(v, c.width)
case AlignCenter:
s = align.Center(v, c.width)
s = align.AlignCenter(v, c.width)
}
fmt.Fprintf(w, "%s", s)

@ -5,12 +5,13 @@
package termui
import (
"errors"
"fmt"
"path"
"strconv"
"sync"
"time"
"github.com/gdamore/tcell/v2"
"github.com/miguelmota/termbox-go"
)
type Event struct {
@ -28,86 +29,82 @@ type EvtKbd struct {
KeyStr string
}
func evtKbd(e tcell.EventKey) EvtKbd {
func evtKbd(e termbox.Event) EvtKbd {
ek := EvtKbd{}
k := string(e.Rune())
k := string(e.Ch)
pre := ""
mod := ""
if e.Modifiers() == tcell.ModAlt {
if e.Mod == termbox.ModAlt {
mod = "M-"
}
if e.Rune() == 0 {
// Doesn't appear to be used by cointop
// TODO: FIXME
// if e.Key > 0xFFFF-12 {
// k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
// } else if e.Key > 0xFFFF-25 {
// ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
// k = ks[0xFFFF-int(e.Key)-12]
// }
// TODO: FIXME
// if e.Key <= 0x7F {
// pre = "C-"
// k = fmt.Sprintf("%v", 'a'-1+int(e.Key))
// kmap := map[termbox.Key][2]string{
// termbox.KeyCtrlSpace: {"C-", "<space>"}, // TODO: FIXME
// termbox.KeyBackspace: {"", "<backspace>"},
// termbox.KeyTab: {"", "<tab>"},
// termbox.KeyEnter: {"", "<enter>"},
// termbox.KeyEsc: {"", "<escape>"},
// termbox.KeyCtrlBackslash: {"C-", "\\"},
// termbox.KeyCtrlSlash: {"C-", "/"},
// termbox.KeySpace: {"", "<space>"},
// termbox.KeyCtrl8: {"C-", "8"}, // TODO: FIXME
// }
// if sk, ok := kmap[e.Key]; ok {
// pre = sk[0]
// k = sk[1]
// }
// }
if e.Ch == 0 {
if e.Key > 0xFFFF-12 {
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
} else if e.Key > 0xFFFF-25 {
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
k = ks[0xFFFF-int(e.Key)-12]
}
if e.Key <= 0x7F {
pre = "C-"
k = fmt.Sprintf("%v", 'a'-1+int(e.Key))
kmap := map[termbox.Key][2]string{
termbox.KeyCtrlSpace: {"C-", "<space>"},
termbox.KeyBackspace: {"", "<backspace>"},
termbox.KeyTab: {"", "<tab>"},
termbox.KeyEnter: {"", "<enter>"},
termbox.KeyEsc: {"", "<escape>"},
termbox.KeyCtrlBackslash: {"C-", "\\"},
termbox.KeyCtrlSlash: {"C-", "/"},
termbox.KeySpace: {"", "<space>"},
termbox.KeyCtrl8: {"C-", "8"},
}
if sk, ok := kmap[e.Key]; ok {
pre = sk[0]
k = sk[1]
}
}
}
ek.KeyStr = pre + mod + k
return ek
}
func crtTermboxEvt(e tcell.Event) Event {
ne := Event{From: "/sys", Time: e.When().Unix()}
switch tev := e.(type) {
case *tcell.EventResize:
func crtTermboxEvt(e termbox.Event) Event {
systypemap := map[termbox.EventType]string{
termbox.EventKey: "keyboard",
termbox.EventResize: "window",
termbox.EventMouse: "mouse",
termbox.EventError: "error",
termbox.EventInterrupt: "interrupt",
}
ne := Event{From: "/sys", Time: time.Now().Unix()}
typ := e.Type
ne.Type = systypemap[typ]
switch typ {
case termbox.EventKey:
kbd := evtKbd(e)
ne.Path = "/sys/kbd/" + kbd.KeyStr
ne.Data = kbd
case termbox.EventResize:
wnd := EvtWnd{}
wnd.Width, wnd.Height = tev.Size()
wnd.Width = e.Width
wnd.Height = e.Height
ne.Path = "/sys/wnd/resize"
ne.Data = wnd
ne.Type = "window"
// log.Debugf("XXX Resized to %d,%d", wnd.Width, wnd.Height)
return ne
case *tcell.EventMouse:
case termbox.EventError:
err := EvtErr(e.Err)
ne.Path = "/sys/err"
ne.Data = err
case termbox.EventMouse:
m := EvtMouse{}
m.X, m.Y = tev.Position()
m.X = e.MouseX
m.Y = e.MouseY
ne.Path = "/sys/mouse"
ne.Data = m
ne.Type = "mouse"
return ne
case *tcell.EventKey:
kbd := evtKbd(*tev)
ne.Path = "/sys/kbd/" + kbd.KeyStr
ne.Data = kbd
ne.Type = "keyboard"
return ne
case *tcell.EventError:
ne.Path = "/sys/err"
ne.Data = errors.New(tev.Error())
ne.Type = "error"
return ne
case *tcell.EventInterrupt:
ne.Type = "interrupt"
default:
ne.Type = "" // TODO: unhandled event?
}
return ne
}
@ -125,18 +122,17 @@ type EvtMouse struct {
type EvtErr error
// func hookTermboxEvt() {
// log.Debugf("XXX hookTermboxEvt")
// for {
// e := termbox.PollEvent()
// log.Debugf("XXX event %s", e)
// for _, c := range sysEvtChs {
// func(ch chan Event) {
// ch <- crtTermboxEvt(e)
// }(c)
// }
// }
// }
func hookTermboxEvt() {
for {
e := termbox.PollEvent()
for _, c := range sysEvtChs {
func(ch chan Event) {
ch <- crtTermboxEvt(e)
}(c)
}
}
}
func NewSysEvtCh() chan Event {
ec := make(chan Event)
@ -227,9 +223,9 @@ func findMatch(mux map[string]func(Event), path string) string {
}
// ResetHandlers Remove all existing defined Handlers from the map
// Remove all existing defined Handlers from the map
func (es *EvtStream) ResetHandlers() {
for Path := range es.Handlers {
for Path, _ := range es.Handlers {
delete(es.Handlers, Path)
}
return

@ -21,7 +21,7 @@ import (
g.PercentColor = termui.ColorBlue
*/
const ColorUndef = Attribute(^uint16(0))
const ColorUndef Attribute = Attribute(^uint16(0))
type Gauge struct {
Block

@ -8,6 +8,8 @@ import (
"regexp"
"strings"
tm "github.com/miguelmota/termbox-go"
rw "github.com/mattn/go-runewidth"
)
@ -16,7 +18,7 @@ import (
// Attribute is printable cell's color and style.
type Attribute uint16
// 8 basic colors
// 8 basic clolrs
const (
ColorDefault Attribute = iota
ColorBlack
@ -29,8 +31,8 @@ const (
ColorWhite
)
// NumberOfColors ...
const NumberOfColors = 8
//Have a constant that defines number of colors
const NumberofColors = 8
// Text style
const (
@ -46,9 +48,9 @@ var (
/* ----------------------- End ----------------------------- */
// func toTmAttr(x Attribute) tm.Attribute {
// return tm.Attribute(x)
// }
func toTmAttr(x Attribute) tm.Attribute {
return tm.Attribute(x)
}
func str2runes(s string) []rune {
return []rune(s)

@ -205,7 +205,7 @@ func shortenFloatVal(x float64) string {
return fmt.Sprintf("%.4fB", x/1e9)
}
if x > 1e6 {
return fmt.Sprintf("%.4fM", x/1e6)
return fmt.Sprintf("%.4fB", x/1e6)
}
return fmt.Sprintf("%.4f", x)
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save