Merge branch 'master' into master

pull/318/head
владимир иванович архипов 12 months ago committed by GitHub
commit 17f1e4e32f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,40 @@
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: /.*/

@ -4,8 +4,7 @@ on:
push: push:
branches: [ master ] branches: [ master ]
pull_request: pull_request:
branches: [ master ] branches: [ mas....
jobs: jobs:
build: build:

6
.gitignore vendored

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

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

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

@ -4,6 +4,37 @@ 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/), 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). 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 ## [1.6.8] - 2021-09-13
### Fixed ### Fixed
- Hide holdings amount when using command hide flag - Hide holdings amount when using command hide flag
@ -55,50 +86,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Config option to keep row focus on sort - Config option to keep row focus on sort
## [1.6.1] - 2021-02-12 ## [1.6.1] - 2021-02-12
### Added
- Multiple coin support in price command
### Fixed ### Fixed
- Chart data interpolation - Chart data interpolation
- CoinMarketCap graph data endpoint - CoinMarketCap graph data endpoint
## [1.6.0] - 2021-02-12
### Added ### Added
- Multiple coin support in price command - Configurable table columns
- Basic price alerts
## [1.6.0] - 2021-02-12
### Fixed ### Fixed
- Coin chart lookup - Coin chart lookup
- Dynamic column widths - Dynamic column widths
## [1.5.5] - 2020-11-15
### Added ### Added
- Configurable table columns - Currency convesion option to holdings command
- Basic price alerts - Sort by percent holdings shortcut
## [1.5.5] - 2020-11-15
### Fixed ### Fixed
- Termux cache directory - Termux cache directory
- Open command on Windows - Open command on Windows
## [1.5.4] - 2020-08-24
### Added ### Added
- Currency convesion option to holdings command - Colorschemes directory flag
- Sort by percent holdings shortcut
## [1.5.4] - 2020-08-24
### Fixed ### Fixed
- Rank order for low market cap coins - Rank order for low market cap coins
### Added
- Colorschemes directory flag
## [1.5.3] - 2020-08-14 ## [1.5.3] - 2020-08-14
### Fixed ### Fixed
- Build error - Build error
## [1.5.2] - 2020-08-13 ## [1.5.2] - 2020-08-13
### Fixed
- `XDG_CONFIG_HOME` config path
### Added ### Added
- Holdings command with sorting and filter options - Holdings command with sorting and filter options
- Bitcoin dominance command - Bitcoin dominance command
### Fixed
- `XDG_CONFIG_HOME` config path
## [1.5.1] - 2020-08-05 ## [1.5.1] - 2020-08-05
### Fixed ### Fixed
- Version typo - Version typo
@ -124,24 +155,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Increase number of page results from CoinGecko - Increase number of page results from CoinGecko
## [1.4.5] - 2020-02-18 ## [1.4.5] - 2020-02-18
### Fixed
- Convert to chosen currency for market data
### Added ### Added
- VND currency conversion - VND currency conversion
### Fixed
- Convert to chosen currency for market data
## [1.4.4] - 2019-12-31 ## [1.4.4] - 2019-12-31
### Fixed ### Fixed
- Flathub app release version - Flathub app release version
## [1.4.3] - 2019-12-29 ## [1.4.3] - 2019-12-29
### Added
- Tab keybinding
### Fixed ### Fixed
- Chart update bug fixes - Chart update bug fixes
- Marketbar currency bug fixes - Marketbar currency bug fixes
### Added
- Tab keybinding
## [1.4.2] - 2019-12-29 ## [1.4.2] - 2019-12-29
### Fixed ### Fixed
- Fix keybinding issue on FreeBSD - Fix keybinding issue on FreeBSD
@ -209,25 +240,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Release archive to contain latest source code - Release archive to contain latest source code
## [1.1.4] - 2019-04-21 ## [1.1.4] - 2019-04-21
### Changed
- CoinMarketCap legacy V2 API to Pro V1 API
### Added ### Added
- Config option to use CoinMarketCap Pro V1 API KEY - Config option to use CoinMarketCap Pro V1 API KEY
### Changed
- CoinMarketCap legacy V2 API to Pro V1 API
## [1.1.3] - 2019-02-25 ## [1.1.3] - 2019-02-25
### Fixed ### Fixed
- Vendor dependencies - Vendor dependencies
## [1.1.2] - 2018-12-30 ## [1.1.2] - 2018-12-30
### Fixed
- Paginate CoinMarketCap V1 API responses due to their backward-incompatible update
### Added ### Added
- `-clean` flag to clean cache - `-clean` flag to clean cache
- `-reset` flag to clean cache and delete config - `-reset` flag to clean cache and delete config
- `-config` flag to use a different specified config file - `-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 ## [1.1.1] - 2018-12-26
### Changed ### Changed
- Use go modules instead of dep - Use go modules instead of dep

@ -2,15 +2,15 @@ FROM golang:alpine AS build
ARG VERSION ARG VERSION
RUN wget \ RUN wget \
--output-document "/cointop-$VERSION.tar.gz" \ --output-document "/cointop-$VERSION.tar.gz" \
"https://github.com/miguelmota/cointop/archive/$VERSION.tar.gz" \ "https://github.com/cointop-sh/cointop/archive/refs/tags/$VERSION.tar.gz" \
&& wget \ && wget \
--output-document "/cointop-colors-master.tar.gz" \ --output-document "/cointop-colors-master.tar.gz" \
"https://github.com/cointop-sh/colors/archive/master.tar.gz" \ "https://github.com/cointop-sh/colors/archive/master.tar.gz" \
&& mkdir --parents \ && mkdir --parents \
"$GOPATH/src/github.com/miguelmota/cointop" \ "$GOPATH/src/github.com/cointop-sh/cointop" \
"/usr/local/share/cointop/colors" \ "/usr/local/share/cointop/colors" \
&& tar \ && tar \
--directory "$GOPATH/src/github.com/miguelmota/cointop" \ --directory "$GOPATH/src/github.com/cointop-sh/cointop" \
--extract \ --extract \
--file "/cointop-$VERSION.tar.gz" \ --file "/cointop-$VERSION.tar.gz" \
--strip-components 1 \ --strip-components 1 \
@ -22,8 +22,8 @@ RUN wget \
&& rm \ && rm \
"/cointop-$VERSION.tar.gz" \ "/cointop-$VERSION.tar.gz" \
/cointop-colors-master.tar.gz \ /cointop-colors-master.tar.gz \
&& cd "$GOPATH/src/github.com/miguelmota/cointop" \ && cd "$GOPATH/src/github.com/cointop-sh/cointop" \
&& CGO_ENABLED=0 go install -ldflags "-s -w -X 'github.com/miguelmota/cointop/cointop.version=$VERSION'" \ && CGO_ENABLED=0 go install -ldflags "-s -w -X 'github.com/cointop-sh/cointop/cointop.version=$VERSION'" \
&& cd "$GOPATH" \ && cd "$GOPATH" \
&& rm -r src/github.com \ && rm -r src/github.com \
&& apk add --no-cache upx \ && apk add --no-cache upx \

@ -1,4 +1,5 @@
VERSION = $$(git describe --abbrev=0 --tags) 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)) VERSION_DATE = $$(git log -1 --pretty='%ad' --date=format:'%Y-%m-%d' $(VERSION))
COMMIT_REV = $$(git rev-list -n 1 $(VERSION)) COMMIT_REV = $$(git rev-list -n 1 $(VERSION))
MAINTAINER = "Miguel Mota" MAINTAINER = "Miguel Mota"
@ -30,18 +31,18 @@ debug:
.PHONY: build .PHONY: build
build: build:
go build -ldflags "-X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop main.go go build -ldflags "-X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/cointop main.go
# http://macappstore.org/upx # http://macappstore.org/upx
build-mac: clean-mac build-mac: clean-mac
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/macos/cointop && upx bin/macos/cointop 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
build-linux: clean-linux build-linux: clean-linux
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 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
build-multiple: clean build-multiple: clean
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop64 && upx bin/cointop64 && \ 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/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop32 && upx bin/cointop32 env GOARCH=386 go build -ldflags "-s -w -X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/cointop32 && upx bin/cointop32
install: build install: build
sudo mv bin/cointop /usr/local/bin sudo mv bin/cointop /usr/local/bin
@ -96,7 +97,7 @@ snap-clean:
snap-stage: snap-stage:
# https://github.com/elopio/go/issues/2 # https://github.com/elopio/go/issues/2
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 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
snap-install: snap-install:
sudo apt install snapd sudo apt install snapd
@ -176,7 +177,7 @@ rpm-dirs:
chmod -R a+rwx ~/rpmbuild chmod -R a+rwx ~/rpmbuild
rpm-download: rpm-download:
wget https://github.com/miguelmota/cointop/archive/$(VERSION).tar.gz -O ~/rpmbuild/SOURCES/$(VERSION).tar.gz wget https://github.com/cointop-sh/cointop/archive/$(VERSION).tar.gz -O ~/rpmbuild/SOURCES/$(VERSION).tar.gz
copr-install-cli: copr-install-cli:
sudo dnf install -y copr-cli sudo dnf install -y copr-cli
@ -210,7 +211,7 @@ brew-test:
brew test cointop.rb brew test cointop.rb
brew-tap: brew-tap:
brew tap cointop/cointop https://github.com/miguelmota/cointop brew tap cointop/cointop https://github.com/cointop-sh/cointop
brew-untap: brew-untap:
brew untap cointop/cointop brew untap cointop/cointop
@ -228,12 +229,23 @@ release:
rm -rf dist rm -rf dist
VERSION=$(VERSION) goreleaser VERSION=$(VERSION) goreleaser
docker-login:
docker login
docker-login-ci:
docker login -u $(DOCKER_USER) -p $(DOCKER_PASS)
docker-build: docker-build:
docker build --build-arg VERSION=$(VERSION) --build-arg MAINTAINER=$(MAINTAINER) -t cointop/cointop . docker build --build-arg VERSION=$(VERSION) --build-arg MAINTAINER=$(MAINTAINER) -t cointop/cointop .
docker-tag: docker-tag:
docker tag cointop/cointop:latest cointop/cointop:$(VERSION) 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-run: docker-run:
docker run -it cointop/cointop docker run -it cointop/cointop
@ -241,13 +253,19 @@ docker-push:
docker push cointop/cointop:$(VERSION) docker push cointop/cointop:$(VERSION)
docker push cointop/cointop:latest 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-tag docker-push
docker-run-ssh: 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 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: ssh-server:
go run cmd/cointop/cointop.go server -p 2222 go run cmd/cointop/cointop.go server -p 2222 -k ~/.ssh/demo/id_rsa
ssh-client: ssh-client:
ssh localhost -p 2222 ssh localhost -p 2222

@ -10,14 +10,14 @@
> Coin tracking for hackers > Coin tracking for hackers
[![License](http://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/miguelmota/cointop/master/LICENSE) [![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/miguelmota/cointop.svg?branch=master)](https://travis-ci.org/miguelmota/cointop) [![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/miguelmota/cointop?)](https://goreportcard.com/report/github.com/miguelmota/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/miguelmota/cointop?status.svg)](https://godoc.org/github.com/miguelmota/cointop) [![GoDoc](https://godoc.org/github.com/cointop-sh/cointop?status.svg)](https://godoc.org/github.com/cointop-sh/cointop)
[![Mentioned in Awesome Terminals](https://awesome.re/mentioned-badge.svg)](https://github.com/k4m4/terminals-are-sexy) [![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) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing)
[`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. [`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.
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)). 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,6 +25,8 @@ The interface is inspired by [`htop`](https://en.wikipedia.org/wiki/Htop) and sh
## Demo ## Demo
This connects to an instance of Cointop using SSH:
```bash ```bash
ssh cointop.sh ssh cointop.sh
``` ```
@ -35,16 +37,18 @@ In action
## Table of Contents ## Table of Contents
Documentation has been moved to [docs.cointop.sh](https://docs.cointop.sh/)
- [Features](#features) - [Features](#features)
- [Documentation](#documentation) - [Documentation](https://docs.cointop.sh/)
- [Install](#install) - [Install](https://docs.cointop.sh/install)
- [Update](#update) - [Update](https://docs.cointop.sh/update)
- [Getting started](#getting-started) - [Getting started](https://docs.cointop.sh/getting-started)
- [Shortcuts](#shortcuts) - [Shortcuts](https://docs.cointop.sh/shortcuts)
- [Colorschemes](#colorschemes) - [Colorschemes](https://docs.cointop.sh/colorschemes)
- [Config](#config) - [Config](https://docs.cointop.sh/config)
- [SSH server](#ssh-server) - [SSH server](https://docs.cointop.sh/ssh)
- [FAQ](#faq) - [FAQ](https://docs.cointop.sh/faq)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Social](#social) - [Social](#social)
- [Mentioned in](#mentioned-in) - [Mentioned in](#mentioned-in)
@ -53,64 +57,25 @@ In action
## Features ## Features
- Quick sort shortcuts - **Shortcut keys**: Vim-inspired shortcut keys, custom key bindings configuration
- Custom key bindings configuration - **Colorschemes**: Custom colorscheme configuration, 256-color and 24-bit support
- Vim inspired shortcut keys - **Favorites**: Save and view favorite coins
- Fast pagination - **Portfolio**: Portfolio tracking of holdings, view profit & loss
- Charts for coins and global market graphs - **Charts**: Charts for coin price history and global market graphs
- Quick chart date range change - **Search**: Fuzzy searching for finding coins
- Fuzzy searching for finding coins - **Conversion**: Currency conversion
- Currency conversion - **Price Alerts**: Price alerts with desktop notifications
- Save and view favorite coins - **Multiple APIs**: Supports multiple coin data APIs; CoinGecko and CoinMarketCap
- Portfolio tracking of holdings - **Mouse**: Mouse support
- 256-color support - **Offline**: Offline cache
- Custom colorschemes - **Fast**: Fast sort shortcuts, pagination, chart date range change, auto-refresh
- Help menu - **Lightweight**: It's very lightweight; can be left running indefinitely
- 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 ## Contributing
See [docs.cointop.sh/contributing](https://docs.cointop.sh/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/miguelmota/cointop/graphs/contributors) that made cointop better._ _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 ## Social
@ -129,10 +94,12 @@ 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` [![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/0x0072cdd7c3d9963ba69506ECf50e16E963B35bb1) `0x0072cdd7c3d9963ba69506ECf50e16E963B35bb1` [![ETH Tip Jar](https://img.shields.io/badge/ETH-tip-blue.svg?logo=ethereum&style=flat)](https://etherscan.io/address/0x9ed3D6793a6b74d8c9A998f5C4b50a25947D53aF) `0x9ed3D6793a6b74d8c9A998f5C4b50a25947D53aF`
Thank you for tips! 🙏 Thank you for tips! 🙏
## License ## License
Released under the [Apache 2.0](./LICENSE) license. Released under the [Apache 2.0](./LICENSE) license.
© [Miguel Mota](https://github.com/miguelmota)

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

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

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

@ -3,7 +3,7 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/miguelmota/cointop/cointop" "github.com/cointop-sh/cointop/cointop"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -17,7 +17,7 @@ func HoldingsCmd() *cobra.Command {
var config string var config string
var sortBy string var sortBy string
var sortDesc bool var sortDesc bool
var format string = "table" var format = "table"
var humanReadable bool var humanReadable bool
var filter []string var filter []string
var cols []string var cols []string

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

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

@ -5,7 +5,7 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/miguelmota/cointop/cointop" "github.com/cointop-sh/cointop/cointop"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -60,21 +60,27 @@ See git.io/cointop for more info.`,
return nil return nil
} }
// NOTE: if reset flag enabled, reset and run cointop // wipe before starting program
if reset { if reset || clean {
if err := cointop.Reset(&cointop.ResetConfig{ ct, err := cointop.NewCointop(&cointop.Config{
Log: !silent, CacheDir: cacheDir,
}); err != nil { ConfigFilepath: config,
})
if err != nil {
return err return err
} }
} if reset {
if err := ct.Reset(&cointop.ResetConfig{
// NOTE: if clean flag enabled, clean and run cointop Log: !silent,
if clean { }); err != nil {
if err := cointop.Clean(&cointop.CleanConfig{ return err
Log: !silent, }
}); err != nil { } else if clean {
return err if err := ct.Clean(&cointop.CleanConfig{
Log: !silent,
}); err != nil {
return err
}
} }
} }

@ -8,21 +8,21 @@ import (
"strings" "strings"
"time" "time"
cssh "github.com/miguelmota/cointop/pkg/ssh" cssh "github.com/cointop-sh/cointop/pkg/ssh"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// ServerCmd ... // ServerCmd ...
func ServerCmd() *cobra.Command { func ServerCmd() *cobra.Command {
var port uint = 22 port := uint(22)
var address string = "0.0.0.0" address := "0.0.0.0"
var idleTimeout uint = 0 idleTimeout := uint(0)
var maxTimeout uint = 0 maxTimeout := uint(0)
var maxSessions uint = 0 maxSessions := uint(0)
var executableBinary string = "cointop" executableBinary := "cointop"
var hostKeyFile string = cssh.DefaultHostKeyFile hostKeyFile := cssh.DefaultHostKeyFile
var userConfigType string = cssh.UserConfigTypePublicKey userConfigType := cssh.UserConfigTypePublicKey
var colorsDir string = os.Getenv("COINTOP_COLORS_DIR") colorsDir := os.Getenv("COINTOP_COLORS_DIR")
serverCmd := &cobra.Command{ serverCmd := &cobra.Command{
Use: "server", Use: "server",

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

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

@ -4,6 +4,7 @@ package cointop
func ActionsMap() map[string]bool { func ActionsMap() map[string]bool {
return map[string]bool{ return map[string]bool{
"first_page": true, "first_page": true,
"move_to_first_page_first_row": true,
"help": true, "help": true,
"toggle_show_help": true, "toggle_show_help": true,
"close_help": true, "close_help": true,
@ -56,6 +57,21 @@ func ActionsMap() map[string]bool {
"toggle_show_portfolio": true, "toggle_show_portfolio": true,
"enlarge_chart": true, "enlarge_chart": true,
"shorten_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,
} }
} }

@ -7,10 +7,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/miguelmota/cointop/pkg/chartplot" "github.com/cointop-sh/cointop/pkg/chartplot"
"github.com/miguelmota/cointop/pkg/timedata" "github.com/cointop-sh/cointop/pkg/timedata"
"github.com/miguelmota/cointop/pkg/timeutil" "github.com/cointop-sh/cointop/pkg/timeutil"
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -25,8 +25,7 @@ type ChartView = ui.View
// NewChartView returns a new chart view // NewChartView returns a new chart view
func NewChartView() *ChartView { func NewChartView() *ChartView {
var view *ChartView = ui.NewView("chart") return ui.NewView("chart")
return view
} }
var chartLock sync.Mutex var chartLock sync.Mutex
@ -50,17 +49,17 @@ func ChartRanges() []string {
// ChartRangesMap returns map of chart range time ranges // ChartRangesMap returns map of chart range time ranges
func ChartRangesMap() map[string]time.Duration { func ChartRangesMap() map[string]time.Duration {
return map[string]time.Duration{ return map[string]time.Duration{
"All Time": time.Duration(24 * 7 * 4 * 12 * 5 * time.Hour), "All Time": 10 * 365 * 24 * time.Hour,
"YTD": time.Duration(1 * time.Second), // this will be calculated "YTD": 1 * time.Second, // this will be calculated
"1Y": time.Duration(24 * 7 * 4 * 12 * time.Hour), "1Y": 365 * 24 * time.Hour,
"6M": time.Duration(24 * 7 * 4 * 6 * time.Hour), "6M": 365 / 2 * 24 * time.Hour,
"3M": time.Duration(24 * 7 * 4 * 3 * time.Hour), "3M": 365 / 4 * 24 * time.Hour,
"1M": time.Duration(24 * 7 * 4 * time.Hour), "1M": 365 / 12 * 24 * time.Hour,
"7D": time.Duration(24 * 7 * time.Hour), "7D": 24 * 7 * time.Hour,
"3D": time.Duration(24 * 3 * time.Hour), "3D": 24 * 3 * time.Hour,
"24H": time.Duration(24 * time.Hour), "24H": 24 * time.Hour,
"6H": time.Duration(6 * time.Hour), "6H": 6 * time.Hour,
"1H": time.Duration(1 * time.Hour), "1H": 1 * time.Hour,
} }
} }
@ -103,7 +102,7 @@ func (ct *Cointop) UpdateChart() error {
return nil return nil
} }
// ChartPoints calculates the the chart points // ChartPoints calculates the chart points
func (ct *Cointop) ChartPoints(symbol string, name string) error { func (ct *Cointop) ChartPoints(symbol string, name string) error {
log.Debug("ChartPoints()") log.Debug("ChartPoints()")
maxX := ct.ChartWidth() maxX := ct.ChartWidth()
@ -119,7 +118,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange] rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange]
if ct.State.selectedChartRange == "YTD" { if ct.State.selectedChartRange == "YTD" {
ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix()) ytd := time.Now().Unix() - timeutil.BeginningOfYear().Unix()
rangeseconds = time.Duration(ytd) * time.Second rangeseconds = time.Duration(ytd) * time.Second
} }
@ -172,23 +171,28 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
} }
} }
// Resample cachedata var labels []string
timeQuantum := timedata.CalculateTimeQuantum(cacheData)
newStart := time.Unix(start, 0).Add(timeQuantum)
newEnd := time.Unix(end, 0).Add(-timeQuantum)
timeData := timedata.ResampleTimeSeriesData(cacheData, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX))
// Extract just the values from the data
var data []float64 var data []float64
for i := range timeData { timeQuantum := timedata.CalculateTimeQuantum(cacheData) // will be 0 if <2 points
value := timeData[i][1] if timeQuantum > 0 {
if math.IsNaN(value) { // Resample cachedata
value = 0.0 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)
} }
data = append(data, value)
} }
chart.SetData(data) chart.SetData(data)
chart.SetDataLabels(labels)
ct.State.chartPoints = chart.GetChartPoints(maxX) ct.State.chartPoints = chart.GetChartPoints(maxX)
return nil return nil
@ -211,7 +215,7 @@ func (ct *Cointop) PortfolioChart() error {
selectedChartRange := ct.State.selectedChartRange // cache here selectedChartRange := ct.State.selectedChartRange // cache here
rangeseconds := ct.chartRangesMap[selectedChartRange] rangeseconds := ct.chartRangesMap[selectedChartRange]
if selectedChartRange == "YTD" { if selectedChartRange == "YTD" {
ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix()) ytd := time.Now().Unix() - timeutil.BeginningOfYear().Unix()
rangeseconds = time.Duration(ytd) * time.Second rangeseconds = time.Duration(ytd) * time.Second
} }
@ -280,39 +284,53 @@ func (ct *Cointop) PortfolioChart() error {
break // use the first one break // use the first one
} }
} }
newStart := time.Unix(start, 0).Add(timeQuantum)
newEnd := time.Unix(end, 0).Add(-timeQuantum)
// Resample and sum data // If there is data, resample and sum
var data []float64 var data []float64
for _, cacheData := range allCacheData { var labels []string
coinData := timedata.ResampleTimeSeriesData(cacheData.data, float64(newStart.UnixMilli()), float64(newEnd.UnixMilli()), chart.GetChartDataSize(maxX)) if timeQuantum > 0 {
// sum (excluding NaN) newStart := time.Unix(start, 0).Add(timeQuantum)
for i := range coinData { newEnd := time.Unix(end, 0).Add(-timeQuantum)
price := coinData[i][1]
if math.IsNaN(price) { // Resample and sum data
price = 0.0 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 := cacheData.coin.Holdings * price // sum (excluding NaN)
if i < len(data) { for i := range coinData {
data[i] += sum price := coinData[i][1]
} else { if math.IsNaN(price) {
data = append(data, sum) 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 // Scale Portfolio Balances to hide value
if ct.State.hidePortfolioBalances { if ct.State.hidePortfolioBalances {
var lastPrice = data[len(data)-1] scalePrice := 0.0
if lastPrice > 0.0 { for _, price := range data {
for i, price := range data { if price > scalePrice {
data[i] = 100 * price / lastPrice scalePrice = price
}
}
if scalePrice > 0.0 {
for i, price := range data {
data[i] = 100 * price / scalePrice
}
} }
} }
} }
chart.SetData(data) chart.SetData(data)
chart.SetDataLabels(labels)
ct.State.chartPoints = chart.GetChartPoints(maxX) ct.State.chartPoints = chart.GetChartPoints(maxX)
return nil return nil
@ -328,6 +346,10 @@ func (ct *Cointop) ShortenChart() error {
ct.State.chartHeight = candidate ct.State.chartHeight = candidate
ct.State.lastChartHeight = ct.State.chartHeight ct.State.lastChartHeight = ct.State.chartHeight
if err := ct.Save(); err != nil {
return err
}
go ct.UpdateChart() go ct.UpdateChart()
return nil return nil
} }
@ -342,6 +364,10 @@ func (ct *Cointop) EnlargeChart() error {
ct.State.chartHeight = candidate ct.State.chartHeight = candidate
ct.State.lastChartHeight = ct.State.chartHeight ct.State.lastChartHeight = ct.State.chartHeight
if err := ct.Save(); err != nil {
return err
}
go ct.UpdateChart() go ct.UpdateChart()
return nil return nil
} }
@ -439,8 +465,8 @@ func (ct *Cointop) ShowChartLoader() error {
func (ct *Cointop) ChartWidth() int { func (ct *Cointop) ChartWidth() int {
log.Debug("ChartWidth()") log.Debug("ChartWidth()")
w := ct.Width() w := ct.Width()
max := 175 max := ct.State.maxChartWidth
if w > max { if max > 0 && w > max {
return max return max
} }

@ -1,6 +1,8 @@
package cointop package cointop
import log "github.com/sirupsen/logrus" import (
log "github.com/sirupsen/logrus"
)
// Coin is the row structure // Coin is the row structure
type Coin struct { type Coin struct {
@ -23,8 +25,10 @@ type Coin struct {
// for favorites // for favorites
Favorite bool Favorite bool
// for portfolio // for portfolio
Holdings float64 Holdings float64
Balance float64 Balance float64
BuyPrice float64
BuyCurrency string
} }
// AllCoins returns a slice of all the coins // AllCoins returns a slice of all the coins
@ -90,3 +94,36 @@ func (ct *Cointop) CoinByID(id string) *Coin {
} }
return nil 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" "strconv"
"time" "time"
"github.com/miguelmota/cointop/pkg/humanize" "github.com/cointop-sh/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/table" "github.com/cointop-sh/cointop/pkg/table"
) )
// SupportedCoinTableHeaders are all the supported coin table header columns // SupportedCoinTableHeaders are all the supported coin table header columns
@ -67,8 +67,8 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
if ct.IsFavoritesVisible() { if ct.IsFavoritesVisible() {
headers = ct.GetFavoritesTableHeaders() headers = ct.GetFavoritesTableHeaders()
} }
ct.ClearSyncMap(ct.State.tableColumnWidths) ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft) ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
for _, coin := range ct.State.coins { for _, coin := range ct.State.coins {
if coin == nil { if coin == nil {
continue continue
@ -82,7 +82,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
star := " " star := " "
rankcolor := ct.colorscheme.TableRow rankcolor := ct.colorscheme.TableRow
if coin.Favorite { if coin.Favorite {
star = "*" star = ct.State.favoriteChar
rankcolor = ct.colorscheme.TableRowFavorite rankcolor = ct.colorscheme.TableRowFavorite
} }
rank := fmt.Sprintf("%s%6v ", star, coin.Rank) rank := fmt.Sprintf("%s%6v ", star, coin.Rank)
@ -136,6 +136,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "24h_volume": case "24h_volume":
text := humanize.Monetaryf(coin.Volume24H, 0) text := humanize.Monetaryf(coin.Volume24H, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.Volume24H, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -243,6 +246,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "market_cap": case "market_cap":
text := humanize.Monetaryf(coin.MarketCap, 0) text := humanize.Monetaryf(coin.MarketCap, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.MarketCap, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -255,6 +261,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "total_supply": case "total_supply":
text := humanize.Numericf(coin.TotalSupply, 0) text := humanize.Numericf(coin.TotalSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.TotalSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -267,6 +276,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "available_supply": case "available_supply":
text := humanize.Numericf(coin.AvailableSupply, 0) text := humanize.Numericf(coin.AvailableSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.AvailableSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -279,7 +291,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
}) })
case "last_updated": case "last_updated":
unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") lastUpdated := humanize.FormatTime(time.Unix(unix, 0), "15:04:05 Jan 02")
ct.SetTableColumnWidthFromString(header, lastUpdated) ct.SetTableColumnWidthFromString(header, lastUpdated)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,

@ -9,14 +9,15 @@ import (
"sync" "sync"
"time" "time"
"github.com/miguelmota/cointop/pkg/api" "github.com/cointop-sh/cointop/pkg/api"
"github.com/miguelmota/cointop/pkg/api/types" "github.com/cointop-sh/cointop/pkg/api/types"
"github.com/miguelmota/cointop/pkg/cache" "github.com/cointop-sh/cointop/pkg/cache"
"github.com/miguelmota/cointop/pkg/filecache" "github.com/cointop-sh/cointop/pkg/filecache"
"github.com/miguelmota/cointop/pkg/pathutil" "github.com/cointop-sh/cointop/pkg/gocui"
"github.com/miguelmota/cointop/pkg/table" "github.com/cointop-sh/cointop/pkg/pathutil"
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/table"
"github.com/miguelmota/gocui" "github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -34,6 +35,11 @@ type Views struct {
Input *InputView Input *InputView
} }
type sortConstraint struct {
sortBy string
sortDesc bool
}
// State is the state preferences of cointop // State is the state preferences of cointop
type State struct { type State struct {
allCoins []*Coin allCoins []*Coin
@ -46,12 +52,12 @@ type State struct {
convertMenuVisible bool convertMenuVisible bool
defaultView string defaultView string
defaultChartRange string defaultChartRange string
maxChartWidth int
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. columnLookup []string
favoritesBySymbol map[string]bool
favorites map[string]bool favorites map[string]bool
favoritesTableColumns []string favoritesTableColumns []string
favoriteChar string
helpVisible bool helpVisible bool
hideMarketbar bool hideMarketbar bool
hideChart bool hideChart bool
@ -70,13 +76,13 @@ type State struct {
refreshRate time.Duration refreshRate time.Duration
running bool running bool
searchFieldVisible bool searchFieldVisible bool
lastSearchQuery string
selectedCoin *Coin selectedCoin *Coin
selectedChartRange string selectedChartRange string
selectedView string selectedView string
lastSelectedView string lastSelectedView string
shortcutKeys map[string]string shortcutKeys map[string]string
sortDesc bool viewSorts map[string]*sortConstraint
sortBy string
tableOffsetX int tableOffsetX int
onlyTable bool onlyTable bool
onlyChart bool onlyChart bool
@ -87,6 +93,13 @@ type State struct {
priceAlerts *PriceAlerts priceAlerts *PriceAlerts
priceAlertEditID string priceAlertEditID string
priceAlertNewID string priceAlertNewID string
compactNotation bool
tableCompactNotation bool
favoritesCompactNotation bool
portfolioCompactNotation bool
enableMouse bool
altCoinLink string
} }
// Cointop cointop // Cointop cointop
@ -120,8 +133,10 @@ type Cointop struct {
// PortfolioEntry is portfolio entry // PortfolioEntry is portfolio entry
type PortfolioEntry struct { type PortfolioEntry struct {
Coin string Coin string
Holdings float64 Holdings float64
BuyPrice float64
BuyCurrency string
} }
// Portfolio is portfolio structure // Portfolio is portfolio structure
@ -179,14 +194,29 @@ var DefaultCurrency = "USD"
// DefaultChartRange ... // DefaultChartRange ...
var DefaultChartRange = "1Y" var DefaultChartRange = "1Y"
// DefaultCompactNotation ...
var DefaultCompactNotation = false
// DefaultEnableMouse ...
var DefaultEnableMouse = true
// DefaultAltCoinLink ...
var DefaultAltCoinLink = ""
// DefaultMaxChartWidth ...
var DefaultMaxChartWidth = 175
// DefaultChartHeight ...
var DefaultChartHeight = 10
// DefaultSortBy ... // DefaultSortBy ...
var DefaultSortBy = "rank" var DefaultSortBy = "rank"
// DefaultPerPage ... // DefaultPerPage ...
var DefaultPerPage uint = 100 var DefaultPerPage = uint(100)
// MaxPages // DefaultMaxPages ...
var DefaultMaxPages uint = 35 var DefaultMaxPages = uint(10)
// DefaultColorscheme ... // DefaultColorscheme ...
var DefaultColorscheme = "cointop" var DefaultColorscheme = "cointop"
@ -197,15 +227,11 @@ var DefaultConfigFilepath = pathutil.NormalizePath(":PREFERRED_CONFIG_HOME:/coin
// DefaultCacheDir ... // DefaultCacheDir ...
var DefaultCacheDir = filecache.DefaultCacheDir var DefaultCacheDir = filecache.DefaultCacheDir
// DefaultColorsDir ... // DefaultFavoriteChar ...
var DefaultColorsDir = fmt.Sprintf("%s/colors", DefaultConfigFilepath) var DefaultFavoriteChar = "*"
// NewCointop initializes cointop // NewCointop initializes cointop
func NewCointop(config *Config) (*Cointop, error) { func NewCointop(config *Config) (*Cointop, error) {
if os.Getenv("DEBUG") != "" {
log.SetLevel(log.DebugLevel)
}
if config == nil { if config == nil {
config = &Config{} config = &Config{}
} }
@ -240,15 +266,15 @@ func NewCointop(config *Config) (*Cointop, error) {
limiter: time.NewTicker(2 * time.Second).C, limiter: time.NewTicker(2 * time.Second).C,
filecache: nil, filecache: nil,
State: &State{ State: &State{
allCoins: []*Coin{}, allCoins: []*Coin{},
cacheDir: DefaultCacheDir, cacheDir: DefaultCacheDir,
coinsTableColumns: DefaultCoinTableHeaders, coinsTableColumns: DefaultCoinTableHeaders,
currencyConversion: DefaultCurrency, currencyConversion: DefaultCurrency,
defaultChartRange: DefaultChartRange, defaultChartRange: DefaultChartRange,
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility. maxChartWidth: DefaultMaxChartWidth,
favoritesBySymbol: make(map[string]bool),
favorites: make(map[string]bool), favorites: make(map[string]bool),
favoritesTableColumns: DefaultCoinTableHeaders, favoritesTableColumns: DefaultCoinTableHeaders,
favoriteChar: DefaultFavoriteChar,
hideMarketbar: config.HideMarketbar, hideMarketbar: config.HideMarketbar,
hideChart: config.HideChart, hideChart: config.HideChart,
hideTable: config.HideTable, hideTable: config.HideTable,
@ -262,15 +288,18 @@ func NewCointop(config *Config) (*Cointop, error) {
refreshRate: 60 * time.Second, refreshRate: 60 * time.Second,
selectedChartRange: DefaultChartRange, selectedChartRange: DefaultChartRange,
shortcutKeys: DefaultShortcuts(), shortcutKeys: DefaultShortcuts(),
sortBy: DefaultSortBy, selectedView: CoinsView,
page: 0, page: 0,
perPage: int(perPage), perPage: int(perPage),
viewSorts: map[string]*sortConstraint{
CoinsView: {DefaultSortBy, false},
},
portfolio: &Portfolio{ portfolio: &Portfolio{
Entries: make(map[string]*PortfolioEntry), Entries: make(map[string]*PortfolioEntry),
}, },
portfolioTableColumns: DefaultPortfolioTableHeaders, portfolioTableColumns: DefaultPortfolioTableHeaders,
chartHeight: 10, chartHeight: DefaultChartHeight,
lastChartHeight: 10, lastChartHeight: DefaultChartHeight,
tableOffsetX: 0, tableOffsetX: 0,
tableColumnWidths: sync.Map{}, tableColumnWidths: sync.Map{},
tableColumnAlignLeft: sync.Map{}, tableColumnAlignLeft: sync.Map{},
@ -278,6 +307,12 @@ func NewCointop(config *Config) (*Cointop, error) {
Entries: make([]*PriceAlert, 0), Entries: make([]*PriceAlert, 0),
SoundEnabled: true, SoundEnabled: true,
}, },
compactNotation: DefaultCompactNotation,
enableMouse: DefaultEnableMouse,
altCoinLink: DefaultAltCoinLink,
tableCompactNotation: DefaultCompactNotation,
favoritesCompactNotation: DefaultCompactNotation,
portfolioCompactNotation: DefaultCompactNotation,
}, },
Views: &Views{ Views: &Views{
Chart: NewChartView(), Chart: NewChartView(),
@ -290,7 +325,8 @@ func NewCointop(config *Config) (*Cointop, error) {
Input: NewInputView(), Input: NewInputView(),
}, },
} }
ct.initlog()
ct.setLogConfiguration()
err := ct.SetupConfig() err := ct.SetupConfig()
if err != nil { if err != nil {
@ -398,7 +434,7 @@ func NewCointop(config *Config) (*Cointop, error) {
ct.filecache.Get(coinscachekey, &allCoinsSlugMap) ct.filecache.Get(coinscachekey, &allCoinsSlugMap)
} }
// fix for https://github.com/miguelmota/cointop/issues/59 // fix for https://github.com/cointop-sh/cointop/issues/59
// can remove this after everyone has cleared their cache // can remove this after everyone has cleared their cache
for _, v := range allCoinsSlugMap { for _, v := range allCoinsSlugMap {
// Some APIs returns rank 0 for new coins // Some APIs returns rank 0 for new coins
@ -425,25 +461,10 @@ func NewCointop(config *Config) (*Cointop, error) {
if max > 100 { if max > 100 {
max = 100 max = 100
} }
ct.Sort(ct.State.sortBy, ct.State.sortDesc, ct.State.allCoins, false) ct.Sort(ct.State.viewSorts[ct.State.selectedView], ct.State.allCoins, false)
ct.State.coins = ct.State.allCoins[0:max] 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 var globaldata []float64
chartcachekey := ct.CompositeCacheKey("globaldata", "", "", ct.State.selectedChartRange) chartcachekey := ct.CompositeCacheKey("globaldata", "", "", ct.State.selectedChartRange)
if ct.filecache != nil { if ct.filecache != nil {
@ -475,14 +496,13 @@ func (ct *Cointop) Run() error {
return err return err
} }
ui.SetFgColor(ct.colorscheme.BaseFg()) ui.SetStyle(ct.colorscheme.BaseStyle())
ui.SetBgColor(ct.colorscheme.BaseBg())
ct.ui = ui ct.ui = ui
ct.g = ui.GetGocui() ct.g = ui.GetGocui()
defer ui.Close() defer ui.Close()
ui.SetInputEsc(true) ui.SetInputEsc(true)
ui.SetMouse(true) ui.SetMouse(ct.State.enableMouse)
ui.SetHighlight(true) ui.SetHighlight(true)
ui.SetManagerFunc(ct.layout) ui.SetManagerFunc(ct.layout)
if err := ct.SetKeybindings(); err != nil { if err := ct.SetKeybindings(); err != nil {
@ -510,18 +530,19 @@ type CleanConfig struct {
} }
// Clean removes cache files // Clean removes cache files
func Clean(config *CleanConfig) error { func (ct *Cointop) Clean(config *CleanConfig) error {
if config == nil { if config == nil {
config = &CleanConfig{} config = &CleanConfig{}
} }
cacheCleaned := false
cacheDir := DefaultCacheDir cacheDir := DefaultCacheDir
if config.CacheDir != "" { if config.CacheDir != "" {
cacheDir = pathutil.NormalizePath(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) { if _, err := os.Stat(cacheDir); !os.IsNotExist(err) {
files, err := ioutil.ReadDir(cacheDir) files, err := ioutil.ReadDir(cacheDir)
if err != nil { if err != nil {
@ -559,12 +580,12 @@ type ResetConfig struct {
} }
// Reset removes configuration and cache files // Reset removes configuration and cache files
func Reset(config *ResetConfig) error { func (ct *Cointop) Reset(config *ResetConfig) error {
if config == nil { if config == nil {
config = &ResetConfig{} config = &ResetConfig{}
} }
if err := Clean(&CleanConfig{ if err := ct.Clean(&CleanConfig{
CacheDir: config.CacheDir, CacheDir: config.CacheDir,
Log: config.Log, Log: config.Log,
}); err != nil { }); err != nil {

@ -2,12 +2,11 @@ package cointop
import ( import (
"fmt" "fmt"
"strconv" "strings"
"sync" "sync"
fcolor "github.com/fatih/color" fcolor "github.com/fatih/color"
gocui "github.com/miguelmota/gocui" "github.com/gdamore/tcell/v2"
xtermcolor "github.com/tomnomnom/xtermcolor"
) )
// TODO: fix hex color support // TODO: fix hex color support
@ -18,7 +17,7 @@ type ColorschemeColors map[string]interface{}
// ISprintf is a sprintf interface // ISprintf is a sprintf interface
type ISprintf func(...interface{}) string type ISprintf func(...interface{}) string
// colorCache is a map of color string names to sprintf functions // ColorCache is a map of color string names to sprintf functions
type ColorCache map[string]ISprintf type ColorCache map[string]ISprintf
// Colorscheme is the struct for colorscheme // Colorscheme is the struct for colorscheme
@ -50,19 +49,40 @@ var BgColorschemeColorsMap = map[string]fcolor.Attribute{
"yellow": fcolor.BgYellow, "yellow": fcolor.BgYellow,
} }
var GocuiColorschemeColorsMap = map[string]gocui.Attribute{ // See more: vendor/github.com/mattn/go-colorable/colorable_windows.go:905
"black": gocui.ColorBlack, // any new color for the below mapping should be compatible with this above list
"blue": gocui.ColorBlue,
"cyan": gocui.ColorCyan, // TcellColorschemeColorsMap map colorscheme names to tcell colors
"green": gocui.ColorGreen, var TcellColorschemeColorsMap = map[string]tcell.Color{
"magenta": gocui.ColorMagenta, "black": tcell.ColorBlack,
"red": gocui.ColorRed, "blue": tcell.ColorNavy,
"white": gocui.ColorWhite, "cyan": tcell.ColorTeal,
"yellow": gocui.ColorYellow, "green": tcell.ColorGreen,
"magenta": tcell.ColorPurple,
"red": tcell.ColorMaroon,
"white": tcell.ColorSilver,
"yellow": tcell.ColorOlive,
} }
// NewColorscheme ... // NewColorscheme ...
func NewColorscheme(colors ColorschemeColors) *Colorscheme { 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):]]
}
}
}
return &Colorscheme{ return &Colorscheme{
colors: colors, colors: colors,
cache: make(ColorCache), cache: make(ColorCache),
@ -70,14 +90,8 @@ func NewColorscheme(colors ColorschemeColors) *Colorscheme {
} }
} }
// BaseFg ... func (c *Colorscheme) BaseStyle() tcell.Style {
func (c *Colorscheme) BaseFg() gocui.Attribute { return c.Style("base")
return c.GocuiFgColor("base")
}
// BaseBg ...
func (c *Colorscheme) BaseBg() gocui.Attribute {
return c.GocuiBgColor("base")
} }
// Chart ... // Chart ...
@ -245,17 +259,42 @@ func (c *Colorscheme) ToSprintf(name string) ISprintf {
return cached return cached
} }
// TODO: use c.Style(name)?
var attrs []fcolor.Attribute var attrs []fcolor.Attribute
if v, ok := c.colors[name+"_fg"].(string); ok { 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) 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 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) 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 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) attrs = append(attrs, bold)
@ -275,42 +314,42 @@ func (c *Colorscheme) Color(name string, a ...interface{}) string {
return c.ToSprintf(name)(a...) return c.ToSprintf(name)(a...)
} }
func (c *Colorscheme) GocuiFgColor(name string) gocui.Attribute { func (c *Colorscheme) Style(name string) tcell.Style {
var attrs []gocui.Attribute st := tcell.StyleDefault
if v, ok := c.colors[name+"_fg"].(string); ok { st = st.Foreground(c.tcellColor(name + "_fg"))
if fg, ok := c.ToGocuiAttr(v); ok { st = st.Background(c.tcellColor(name + "_bg"))
attrs = append(attrs, fg)
}
}
if v, ok := c.colors[name+"_bold"].(bool); ok { if v, ok := c.colors[name+"_bold"].(bool); ok {
if v { st = st.Bold(v)
attrs = append(attrs, gocui.AttrBold)
}
} }
if v, ok := c.colors[name+"_underline"].(bool); ok { if v, ok := c.colors[name+"_underline"].(bool); ok {
if v { st = st.Underline(v)
attrs = append(attrs, gocui.AttrUnderline)
}
} }
if len(attrs) > 0 { // TODO: Blink Dim Italic Reverse Strikethrough
var combined gocui.Attribute return st
for _, v := range attrs { }
combined = combined ^ v
} // tcellColor can supply for types of color name: specific mapped name, tcell color name, hex
return combined // Examples: black, honeydew, #000000
func (c *Colorscheme) tcellColor(name string) tcell.Color {
v, ok := c.colors[name].(string)
if !ok {
return tcell.ColorDefault
} }
return gocui.ColorDefault if color, found := TcellColorschemeColorsMap[v]; found {
} return color
}
func (c *Colorscheme) GocuiBgColor(name string) gocui.Attribute { color := tcell.GetColor(v)
if v, ok := c.colors[name+"_bg"].(string); ok { if color != tcell.ColorDefault {
if bg, ok := c.ToGocuiAttr(v); ok { return color
return bg
}
} }
return gocui.ColorDefault // find closest X11 color to RGB
// if code, ok := HexToAnsi(v); ok {
// return tcell.PaletteColor(int(code) & 0xff)
// }
return color
} }
func (c *Colorscheme) ToFgAttr(v string) (fcolor.Attribute, bool) { func (c *Colorscheme) ToFgAttr(v string) (fcolor.Attribute, bool) {
@ -318,9 +357,10 @@ func (c *Colorscheme) ToFgAttr(v string) (fcolor.Attribute, bool) {
return attr, true return attr, true
} }
if code, ok := HexToAnsi(v); ok { // find closest X11 color to RGB
return fcolor.Attribute(code), true // if code, ok := HexToAnsi(v); ok {
} // return fcolor.Attribute(code), true
// }
return 0, false return 0, false
} }
@ -330,55 +370,20 @@ func (c *Colorscheme) ToBgAttr(v string) (fcolor.Attribute, bool) {
return attr, true return attr, true
} }
if code, ok := HexToAnsi(v); ok { // find closest X11 color to RGB
return fcolor.Attribute(code), true // if code, ok := HexToAnsi(v); ok {
} // return fcolor.Attribute(code), true
// }
return 0, false return 0, false
} }
// toBoldAttr converts a boolean to an Attribute type // ToBoldAttr converts a boolean to an Attribute type
func (c *Colorscheme) ToBoldAttr(v bool) (fcolor.Attribute, bool) { func (c *Colorscheme) ToBoldAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Bold, v return fcolor.Bold, v
} }
// toUnderlineAttr converts a boolean to an Attribute type // ToUnderlineAttr converts a boolean to an Attribute type
func (c *Colorscheme) ToUnderlineAttr(v bool) (fcolor.Attribute, bool) { func (c *Colorscheme) ToUnderlineAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Underline, v 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
}
if code, ok := HexToAnsi(v); ok {
return gocui.Attribute(code), true
}
return 0, false
}
// HexToAnsi converts a hex color string to a uint8 ansi code
func HexToAnsi(h string) (uint8, bool) {
if h == "" {
return 0, false
}
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,19 +11,20 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
"github.com/miguelmota/cointop/pkg/pathutil" "github.com/cointop-sh/cointop/pkg/pathutil"
"github.com/miguelmota/cointop/pkg/toml" "github.com/cointop-sh/cointop/pkg/toml"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// FilePerm is the default file permissions // FilePerm is the default file permissions
var FilePerm = os.FileMode(0644) var FilePerm = os.FileMode(0o644)
// ErrInvalidPriceAlert is error for invalid price alert value // ErrInvalidPriceAlert is error for invalid price alert value
var ErrInvalidPriceAlert = errors.New("invalid price alert value") var ErrInvalidPriceAlert = errors.New("invalid price alert value")
// PossibleConfigPaths are the the possible config file paths. // PossibleConfigPaths are the possible config file paths.
// NOTE: this is to support previous default config filepaths // NOTE: this is to support previous default config filepaths
var PossibleConfigPaths = []string{ var PossibleConfigPaths = []string{
":PREFERRED_CONFIG_HOME:/cointop/config.toml", ":PREFERRED_CONFIG_HOME:/cointop/config.toml",
@ -46,57 +47,44 @@ type ConfigFileConfig struct {
API interface{} `toml:"api"` API interface{} `toml:"api"`
Colorscheme interface{} `toml:"colorscheme"` Colorscheme interface{} `toml:"colorscheme"`
RefreshRate interface{} `toml:"refresh_rate"` RefreshRate interface{} `toml:"refresh_rate"`
CoinStructHash interface{} `toml:"coin_struct_version"`
CacheDir interface{} `toml:"cache_dir"` 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"` Table map[string]interface{} `toml:"table"`
Chart map[string]interface{} `toml:"chart"`
} }
// SetupConfig loads config file // SetupConfig loads config file
func (ct *Cointop) SetupConfig() error { func (ct *Cointop) SetupConfig() error {
log.Debug("SetupConfig()") type loadConfigFunc func() error
if err := ct.CreateConfigIfNotExists(); err != nil { loaders := []loadConfigFunc{
return err ct.CreateConfigIfNotExists,
} ct.ParseConfig,
if err := ct.ParseConfig(); err != nil { ct.loadTableConfig,
return err ct.loadChartConfig,
} ct.loadShortcutsFromConfig,
if err := ct.loadTableConfig(); err != nil { ct.loadFavoritesFromConfig,
return err ct.loadCurrencyFromConfig,
} ct.loadDefaultViewFromConfig,
if err := ct.loadShortcutsFromConfig(); err != nil { ct.loadDefaultChartRangeFromConfig,
return err ct.loadAPIKeysFromConfig,
} ct.loadAPIChoiceFromConfig,
if err := ct.loadFavoritesFromConfig(); err != nil { ct.loadColorschemeFromConfig,
return err ct.loadRefreshRateFromConfig,
} ct.loadCacheDirFromConfig,
if err := ct.loadCurrencyFromConfig(); err != nil { ct.loadCompactNotationFromConfig,
return err ct.loadEnableMouseFromConfig,
} ct.loadAltCoinLinkFromConfig,
if err := ct.loadDefaultViewFromConfig(); err != nil { ct.loadPriceAlertsFromConfig,
return err ct.loadPortfolioFromConfig,
} }
if err := ct.loadDefaultChartRangeFromConfig(); err != nil {
return err for _, f := range loaders {
} if err := f(); err != nil {
if err := ct.loadAPIKeysFromConfig(); err != nil { return err
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 return nil
@ -147,7 +135,7 @@ func (ct *Cointop) ConfigFilePath() string {
return pathutil.NormalizePath(ct.configFilepath) return pathutil.NormalizePath(ct.configFilepath)
} }
// ConfigPath return the config file path // MakeConfigDir creates the directory for the config file
func (ct *Cointop) MakeConfigDir() error { func (ct *Cointop) MakeConfigDir() error {
log.Debug("MakeConfigDir()") log.Debug("MakeConfigDir()")
path := ct.ConfigDirPath() path := ct.ConfigDirPath()
@ -231,49 +219,40 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
return favoritesIfc[i].(string) < favoritesIfc[j].(string) return favoritesIfc[i].(string) < favoritesIfc[j].(string)
}) })
var favoritesBySymbolIfc []interface{}
favoritesMapIfc := map[string]interface{}{ favoritesMapIfc := map[string]interface{}{
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility. "names": favoritesIfc,
"symbols": favoritesBySymbolIfc, "columns": ct.State.favoritesTableColumns,
"names": favoritesIfc, "character": ct.State.favoriteChar,
"compact_notation": ct.State.favoritesCompactNotation,
} }
var favoritesColumnsIfc interface{} = ct.State.favoritesTableColumns
favoritesMapIfc["columns"] = favoritesColumnsIfc
portfolioIfc := map[string]interface{}{}
var holdingsIfc [][]string var holdingsIfc [][]string
for name := range ct.State.portfolio.Entries { for name := range ct.State.portfolio.Entries {
entry, ok := ct.State.portfolio.Entries[name] entry, ok := ct.State.portfolio.Entries[name]
if !ok || entry.Coin == "" { if !ok || entry.Coin == "" {
continue continue
} }
var amount string = strconv.FormatFloat(entry.Holdings, 'f', -1, 64) tuple := []string{
var coinName string = entry.Coin entry.Coin,
var tuple []string = []string{coinName, amount} strconv.FormatFloat(entry.Holdings, 'f', -1, 64),
strconv.FormatFloat(entry.BuyPrice, 'f', -1, 64),
entry.BuyCurrency,
}
holdingsIfc = append(holdingsIfc, tuple) holdingsIfc = append(holdingsIfc, tuple)
} }
sort.Slice(holdingsIfc, func(i, j int) bool { sort.Slice(holdingsIfc, func(i, j int) bool {
return holdingsIfc[i][0] < holdingsIfc[j][0] return holdingsIfc[i][0] < holdingsIfc[j][0]
}) })
portfolioIfc["holdings"] = holdingsIfc portfolioIfc := map[string]interface{}{
"holdings": holdingsIfc,
var columnsIfc interface{} = ct.State.portfolioTableColumns "columns": ct.State.portfolioTableColumns,
portfolioIfc["columns"] = columnsIfc "compact_notation": ct.State.portfolioCompactNotation,
}
var currencyIfc interface{} = ct.State.currencyConversion
var defaultViewIfc interface{} = ct.State.defaultView
var defaultChartRangeIfc interface{} = ct.State.defaultChartRange
var colorschemeIfc interface{} = ct.colorschemeName
var refreshRateIfc interface{} = uint(ct.State.refreshRate.Seconds())
var cacheDirIfc interface{} = ct.State.cacheDir
cmcIfc := map[string]interface{}{ cmcIfc := map[string]interface{}{
"pro_api_key": ct.apiKeys.cmc, "pro_api_key": ct.apiKeys.cmc,
} }
var apiChoiceIfc interface{} = ct.apiChoice
var priceAlertsIfc []interface{} var priceAlertsIfc []interface{}
for _, priceAlert := range ct.State.priceAlerts.Entries { for _, priceAlert := range ct.State.priceAlerts.Entries {
if priceAlert.Expired { if priceAlert.Expired {
@ -291,26 +270,38 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
//"sound": ct.State.priceAlerts.SoundEnabled, //"sound": ct.State.priceAlerts.SoundEnabled,
} }
var coinsTableColumnsIfc interface{} = ct.State.coinsTableColumns tableMapIfc := map[string]interface{}{
tableMapIfc := map[string]interface{}{} "columns": ct.State.coinsTableColumns,
tableMapIfc["columns"] = coinsTableColumnsIfc "keep_row_focus_on_sort": ct.State.keepRowFocusOnSort,
var keepRowFocusOnSortIfc interface{} = ct.State.keepRowFocusOnSort "compact_notation": ct.State.tableCompactNotation,
tableMapIfc["keep_row_focus_on_sort"] = keepRowFocusOnSortIfc }
chartMapIfc := map[string]interface{}{
"max_width": ct.State.maxChartWidth,
"height": ct.State.chartHeight,
}
currentCoinHash, _ := getStructHash(Coin{})
var inputs = &ConfigFileConfig{ inputs := &ConfigFileConfig{
API: apiChoiceIfc, API: ct.apiChoice,
Colorscheme: colorschemeIfc, Colorscheme: ct.colorschemeName,
CoinMarketCap: cmcIfc, CoinMarketCap: cmcIfc,
Currency: currencyIfc, Currency: ct.State.currencyConversion,
DefaultView: defaultViewIfc, DefaultView: ct.State.defaultView,
DefaultChartRange: defaultChartRangeIfc, DefaultChartRange: ct.State.defaultChartRange,
Favorites: favoritesMapIfc, Favorites: favoritesMapIfc,
RefreshRate: refreshRateIfc, RefreshRate: uint(ct.State.refreshRate.Seconds()),
Shortcuts: shortcutsIfcs, Shortcuts: shortcutsIfcs,
Portfolio: portfolioIfc, Portfolio: portfolioIfc,
PriceAlerts: priceAlertsMapIfc, PriceAlerts: priceAlertsMapIfc,
CacheDir: cacheDirIfc, CacheDir: ct.State.cacheDir,
Table: tableMapIfc, Table: tableMapIfc,
Chart: chartMapIfc,
CoinStructHash: currentCoinHash,
CompactNotation: ct.State.compactNotation,
EnableMouse: ct.State.enableMouse,
AltCoinLink: ct.State.altCoinLink,
} }
var b bytes.Buffer var b bytes.Buffer
@ -335,6 +326,27 @@ func (ct *Cointop) loadTableConfig() error {
if ok { if ok {
ct.State.keepRowFocusOnSort = keepRowFocusOnSortIfc.(bool) 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 return nil
} }
@ -367,14 +379,44 @@ func (ct *Cointop) loadTableColumnsFromConfig() error {
// LoadShortcutsFromConfig loads keyboard shortcuts from config file to struct // LoadShortcutsFromConfig loads keyboard shortcuts from config file to struct
func (ct *Cointop) loadShortcutsFromConfig() error { func (ct *Cointop) loadShortcutsFromConfig() error {
log.Debug("loadShortcutsFromConfig()") 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)
for k, ifc := range ct.config.Shortcuts { for k, ifc := range ct.config.Shortcuts {
if v, ok := ifc.(string); ok { if v, ok := ifc.(string); ok {
if !ct.ActionExists(v) { if !ct.ActionExists(v) {
log.Debugf("Shortcut '%s'=>%s is not a valid action", k, v)
continue continue
} }
ct.State.shortcutKeys[k] = v 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 = config
return nil return nil
} }
@ -467,6 +509,36 @@ func (ct *Cointop) loadCacheDirFromConfig() error {
return nil 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
}
return nil
}
// 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 nil
}
// 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
}
return nil
}
// LoadAPIChoiceFromConfig loads API choices from config file to struct // LoadAPIChoiceFromConfig loads API choices from config file to struct
func (ct *Cointop) loadAPIChoiceFromConfig() error { func (ct *Cointop) loadAPIChoiceFromConfig() error {
log.Debug("loadAPIKeysFromConfig()") log.Debug("loadAPIKeysFromConfig()")
@ -482,18 +554,21 @@ func (ct *Cointop) loadAPIChoiceFromConfig() error {
func (ct *Cointop) loadFavoritesFromConfig() error { func (ct *Cointop) loadFavoritesFromConfig() error {
log.Debug("loadFavoritesFromConfig()") log.Debug("loadFavoritesFromConfig()")
for k, valueIfc := range ct.config.Favorites { 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{}) ifcs, ok := valueIfc.([]interface{})
if !ok { if !ok {
continue continue
} }
switch k { 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": case "names":
for _, ifc := range ifcs { for _, ifc := range ifcs {
if v, ok := ifc.(string); ok { if v, ok := ifc.(string); ok {
@ -542,33 +617,9 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
} }
} }
} else if key == "holdings" { } else if key == "holdings" {
holdingsIfc, ok := valueIfc.([]interface{}) // Defer until the end to work around premature-save issue
if !ok { } else if key == "compact_notation" {
continue ct.State.portfolioCompactNotation = valueIfc.(bool)
}
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 { } else {
// Backward compatibility < v1.6.0 // Backward compatibility < v1.6.0
holdings, err := ct.InterfaceToFloat64(valueIfc) holdings, err := ct.InterfaceToFloat64(valueIfc)
@ -576,12 +627,62 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
return err return err
} }
if err := ct.SetPortfolioEntry(key, holdings); err != nil { if err := ct.SetPortfolioEntry(key, holdings, 0.0, ""); err != nil {
return err 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 return nil
} }
@ -645,7 +746,7 @@ func (ct *Cointop) loadPriceAlertsFromConfig() error {
return nil return nil
} }
// GetColorschemeColors loads colors from colorsheme file to struct // GetColorschemeColors loads colors from colorscheme file to struct
func (ct *Cointop) GetColorschemeColors() (map[string]interface{}, error) { func (ct *Cointop) GetColorschemeColors() (map[string]interface{}, error) {
log.Debug("GetColorschemeColors()") log.Debug("GetColorschemeColors()")
var colors map[string]interface{} var colors map[string]interface{}

@ -3,15 +3,18 @@ package cointop
import ( import (
"errors" "errors"
"fmt" "fmt"
"regexp"
"sort" "sort"
"strings" "strings"
color "github.com/miguelmota/cointop/pkg/color" fcolor "github.com/fatih/color"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/mattn/go-runewidth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// FiatCurrencyNames is a mpa of currency symbols to names. // FiatCurrencyNames is a map of currency symbols to names.
// Keep these in alphabetical order. // Keep these in alphabetical order.
var FiatCurrencyNames = map[string]string{ var FiatCurrencyNames = map[string]string{
"AUD": "Australian Dollar", "AUD": "Australian Dollar",
@ -175,9 +178,10 @@ func (ct *Cointop) UpdateConvertMenu() error {
} }
shortcut := string(alphanumericcharacters[i]) shortcut := string(alphanumericcharacters[i])
if key == ct.State.currencyConversion { if key == ct.State.currencyConversion {
shortcut = ct.colorscheme.MenuLabelActive(color.Bold("*")) Bold := fcolor.New(fcolor.Bold).SprintFunc()
key = ct.colorscheme.Menu(color.Bold(key)) shortcut = ct.colorscheme.MenuLabelActive(Bold("*"))
currency = ct.colorscheme.MenuLabelActive(color.Bold(currency)) key = ct.colorscheme.Menu(Bold(key))
currency = ct.colorscheme.MenuLabelActive(Bold(currency))
} else { } else {
key = ct.colorscheme.Menu(key) key = ct.colorscheme.Menu(key)
currency = ct.colorscheme.MenuLabel(currency) currency = ct.colorscheme.MenuLabel(currency)
@ -231,6 +235,10 @@ func (ct *Cointop) SetCurrencyConverstion(convert string) error {
func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error { func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
log.Debug("SetCurrencyConverstionFn()") log.Debug("SetCurrencyConverstionFn()")
return func() error { return func() error {
if !ct.State.convertMenuVisible {
return nil
}
ct.HideConvertMenu() ct.HideConvertMenu()
if err := ct.SetCurrencyConverstion(convert); err != nil { if err := ct.SetCurrencyConverstion(convert); err != nil {
@ -240,7 +248,7 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
if err := ct.Save(); err != nil { if err := ct.Save(); err != nil {
return err return err
} }
go ct.UpdateCurrentPageCoins()
go ct.RefreshAll() go ct.RefreshAll()
return nil return nil
} }
@ -249,7 +257,14 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
// CurrencySymbol returns the symbol for the currency conversion // CurrencySymbol returns the symbol for the currency conversion
func (ct *Cointop) CurrencySymbol() string { func (ct *Cointop) CurrencySymbol() string {
log.Debug("CurrencySymbol()") log.Debug("CurrencySymbol()")
return CurrencySymbol(ct.State.currencyConversion) symbol := CurrencySymbol(ct.State.currencyConversion)
width := runewidth.StringWidth(symbol)
if width > 1 {
symbol = pad.Right(symbol, width, " ")
}
return symbol
} }
// ShowConvertMenu shows the convert menu view // ShowConvertMenu shows the convert menu view
@ -293,3 +308,40 @@ func CurrencySymbol(currency string) string {
return "?" 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,17 +1,31 @@
package cointop package cointop
import ( import (
"fmt"
"os" "os"
"github.com/cointop-sh/cointop/pkg/pathutil"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func (ct *Cointop) initlog() { func (ct *Cointop) setLogConfiguration() {
filename := "/tmp/cointop.log" 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)
}
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil { if err != nil {
panic(err) panic(err)
} }
log.SetOutput(f) log.SetOutput(f)
ct.logfile = f ct.logfile = f
fmt.Printf("Writing debug log to %s\n", filename)
} }

@ -20,6 +20,7 @@ func DefaultShortcuts() map[string]string {
"ctrl+d": "page_down", "ctrl+d": "page_down",
"ctrl+f": "open_search", "ctrl+f": "open_search",
"ctrl+n": "next_page", "ctrl+n": "next_page",
"ctrl+o": "open_alt_link",
"ctrl+p": "previous_page", "ctrl+p": "previous_page",
"ctrl+r": "refresh", "ctrl+r": "refresh",
"ctrl+R": "refresh", "ctrl+R": "refresh",
@ -36,7 +37,7 @@ func DefaultShortcuts() map[string]string {
"alt+right": "sort_right_column", "alt+right": "sort_right_column",
"F1": "help", "F1": "help",
"F5": "refresh", "F5": "refresh",
"0": "first_page", "0": "move_to_first_page_first_row",
"1": "sort_column_1h_change", "1": "sort_column_1h_change",
"2": "sort_column_24h_change", "2": "sort_column_24h_change",
"3": "sort_column_30d_change", "3": "sort_column_30d_change",
@ -85,5 +86,8 @@ func DefaultShortcuts() map[string]string {
"<": "scroll_left", "<": "scroll_left",
"+": "show_price_alert_add_menu", "+": "show_price_alert_add_menu",
"\\\\": "toggle_table_fullscreen", "\\\\": "toggle_table_fullscreen",
"!": "sort_column_cost",
"@": "sort_column_pnl",
"#": "sort_column_pnl_percent",
} }
} }

@ -3,7 +3,7 @@ package cointop
import ( import (
"fmt" "fmt"
"github.com/miguelmota/cointop/pkg/api" "github.com/cointop-sh/cointop/pkg/api"
) )
// DominanceConfig is the config options for the dominance command // DominanceConfig is the config options for the dominance command

@ -56,7 +56,7 @@ func (ct *Cointop) ToggleShowFavorites() error {
// GetFavoritesSlice returns coin favorites as slice // GetFavoritesSlice returns coin favorites as slice
func (ct *Cointop) GetFavoritesSlice() []*Coin { func (ct *Cointop) GetFavoritesSlice() []*Coin {
log.Debug("GetFavoritesSlice()") log.Debug("GetFavoritesSlice()")
sliced := []*Coin{} var sliced []*Coin
for i := range ct.State.allCoins { for i := range ct.State.allCoins {
coin := ct.State.allCoins[i] coin := ct.State.allCoins[i]
if coin.Favorite { if coin.Favorite {
@ -64,7 +64,7 @@ func (ct *Cointop) GetFavoritesSlice() []*Coin {
} }
} }
sort.Slice(sliced, func(i, j int) bool { sort.SliceStable(sliced, func(i, j int) bool {
return sliced[i].MarketCap > sliced[j].MarketCap return sliced[i].MarketCap > sliced[j].MarketCap
}) })

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"sort" "sort"
"github.com/miguelmota/cointop/pkg/pad" "github.com/cointop-sh/cointop/pkg/pad"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

@ -2,168 +2,103 @@ package cointop
import ( import (
"strings" "strings"
"unicode"
"github.com/miguelmota/gocui" "github.com/cointop-sh/cointop/pkg/gocui"
"github.com/gdamore/tcell/v2"
log "github.com/sirupsen/logrus"
) )
// 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 // ParseKeys returns string keyboard key as gocui key type
func (ct *Cointop) ParseKeys(s string) (interface{}, gocui.Modifier) { 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?
var key interface{} var key interface{}
mod := gocui.ModNone mod := tcell.ModNone
split := strings.Split(s, "+")
if len(split) > 1 { // translate legacy and special names for keys
m := strings.ToLower(strings.TrimSpace(split[0])) keyName := keyMap(strings.TrimSpace(s))
k := strings.ToLower(strings.TrimSpace(split[1])) if len(keyName) > 1 {
if m == "alt" { keyName = strings.Replace(keyName, "+", "-", -1)
mod = gocui.ModAlt
s = k split := strings.Split(keyName, "-")
} else if m == "ctrl" { if len(split) > 1 {
switch k { m := strings.ToLower(strings.TrimSpace(split[0]))
case "0": k := strings.TrimSpace(split[1])
key = '0' k = keyMap(k)
case "1": if k == " " {
key = '1' k = "Space" // fix mod+space
case "2": }
key = gocui.KeyCtrl2
case "3": if m == "alt" {
key = gocui.KeyCtrl3 mod = tcell.ModAlt
case "4": keyName = k
key = gocui.KeyCtrl4 } else if m == "ctrl" {
case "5": // let the lookup handle it
key = gocui.KeyCtrl5 keyName = m + "-" + k
case "6": } else {
key = gocui.KeyCtrl6 keyName = m + "-" + k
case "7": }
key = gocui.KeyCtrl7 // TODO: other mods?
case "8": }
key = gocui.KeyCtrl8 }
case "9":
key = '9' // First try looking up keyname directly
case "a": lcKeyName := strings.ToLower(keyName)
key = gocui.KeyCtrlA for key, name := range tcell.KeyNames {
case "b": if strings.ToLower(name) == lcKeyName {
key = gocui.KeyCtrlB if strings.HasPrefix(name, "Ctrl-") {
case "c": mod = tcell.ModCtrl
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 return key, mod
} }
} }
if len(s) == 1 { // Then try one-rune variants
r := []rune(s) if len(keyName) == 1 {
r := []rune(keyName)
key = r[0] key = r[0]
return key, mod return key, mod
} }
s = strings.ToLower(s) if key == nil {
switch s { log.Debugf("Could not map key '%s' to key", 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 = '\\'
} }
return key, mod return key, mod
} }
@ -197,6 +132,8 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
fn = ct.Keyfn(ct.NavigateLastLine) fn = ct.Keyfn(ct.NavigateLastLine)
case "open_link": case "open_link":
fn = ct.Keyfn(ct.OpenLink) fn = ct.Keyfn(ct.OpenLink)
case "open_alt_link":
fn = ct.Keyfn(ct.OpenAltLink)
case "refresh": case "refresh":
fn = ct.Keyfn(ct.Refresh) fn = ct.Keyfn(ct.Refresh)
case "sort_column_asc": case "sort_column_asc":
@ -218,6 +155,8 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
view = "help" view = "help"
case "first_page": case "first_page":
fn = ct.Keyfn(ct.FirstPage) fn = ct.Keyfn(ct.FirstPage)
case "move_to_first_page_first_row":
fn = ct.Keyfn(ct.NavigateToFirstPageFirstRow)
case "sort_column_1h_change": case "sort_column_1h_change":
fn = ct.Sortfn("1h_change", true) fn = ct.Sortfn("1h_change", true)
case "sort_column_24h_change": case "sort_column_24h_change":
@ -323,11 +262,18 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
fn = ct.Keyfn(ct.CursorDownOrNextPage) fn = ct.Keyfn(ct.CursorDownOrNextPage)
case "move_up_or_previous_page": case "move_up_or_previous_page":
fn = ct.Keyfn(ct.CursorUpOrPreviousPage) 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: default:
fn = ct.Keyfn(ct.Noop) fn = ct.Keyfn(ct.Noop)
} }
ct.SetKeybindingMod(key, mod, fn, view) ct.SetKeybindingMod(key, mod, fn, view)
return nil return nil
} }
@ -340,61 +286,91 @@ func (ct *Cointop) SetKeybindings() error {
} }
// keys to force quit // keys to force quit
ct.SetKeybindingMod(gocui.KeyCtrlC, gocui.ModNone, ct.Keyfn(ct.Quit), "") ct.SetKeybindingMod(tcell.KeyCtrlC, tcell.ModNone, ct.Keyfn(ct.Quit), "")
ct.SetKeybindingMod(gocui.KeyCtrlZ, gocui.ModNone, ct.Keyfn(ct.Quit), "") ct.SetKeybindingMod(tcell.KeyCtrlZ, tcell.ModNone, ct.Keyfn(ct.Quit), "")
// searchfield keys // searchfield keys
ct.SetKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.Keyfn(ct.DoSearch), ct.Views.SearchField.Name()) ct.SetKeybindingMod(tcell.KeyEnter, tcell.ModNone, ct.Keyfn(ct.DoSearch), ct.Views.SearchField.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.CancelSearch), ct.Views.SearchField.Name()) ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.CancelSearch), ct.Views.SearchField.Name())
// keys to quit help when open // keys to quit help when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name()) ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name()) ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
// keys to quit portfolio update menu when open // keys to quit portfolio update menu when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name()) ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name()) ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
// keys to quit convert menu when open // keys to quit convert menu when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name()) ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name()) ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
// keys to update portfolio holdings // keys to update portfolio holdings
ct.SetKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.Keyfn(ct.EnterKeyPressHandler), ct.Views.Input.Name()) 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("/") key, mod := ct.ParseKeys("/")
ct.DeleteKeybindingMod(key, mod, "") ct.DeleteKeybindingMod(key, mod, "")
// mouse events // mouse events
ct.SetKeybindingMod(gocui.MouseRelease, gocui.ModNone, ct.Keyfn(ct.MouseRelease), "") ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.MouseLeftClick), ct.Views.Table.Name()) // click to focus
ct.SetKeybindingMod(gocui.MouseLeft, gocui.ModNone, ct.Keyfn(ct.MouseLeftClick), "")
ct.SetKeybindingMod(gocui.MouseMiddle, gocui.ModNone, ct.Keyfn(ct.MouseMiddleClick), "") // clicking table headers sorts table
ct.SetKeybindingMod(gocui.MouseRight, gocui.ModNone, ct.Keyfn(ct.MouseRightClick), "") ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.TableHeaderMouseLeftClick), ct.Views.TableHeader.Name())
ct.SetKeybindingMod(gocui.MouseWheelUp, gocui.ModNone, ct.Keyfn(ct.MouseWheelUp), "") ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.StatusbarMouseLeftClick), ct.Views.Statusbar.Name())
ct.SetKeybindingMod(gocui.MouseWheelDown, gocui.ModNone, ct.Keyfn(ct.MouseWheelDown), "") // 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())
// character key press to select option // character key press to select option
// TODO: use scrolling table // TODO: use scrolling table
keys := ct.SortedSupportedCurrencyConversions() keys := ct.SortedSupportedCurrencyConversions()
for i, k := range keys { for i, k := range keys {
ct.SetKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.Keyfn(ct.SetCurrencyConverstionFn(k)), ct.Views.Menu.Name()) ct.SetKeybindingMod(alphanumericcharacters[i], tcell.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 return nil
} }
// SetKeybindingMod sets the keybinding modifier key // SetKeybindingMod sets the keybinding modifier key
func (ct *Cointop) SetKeybindingMod(key interface{}, mod gocui.Modifier, callback func(g *gocui.Gui, v *gocui.View) error, view string) error { func (ct *Cointop) SetKeybindingMod(key interface{}, mod tcell.ModMask, callback func(g *gocui.Gui, v *gocui.View) error, view string) error {
// TODO: take EventKey?
var err error var err error
switch t := key.(type) { switch t := key.(type) {
case gocui.Key: case tcell.Key:
err = ct.g.SetKeybinding(view, t, mod, callback) err = ct.g.SetKeybinding(view, t, 0, mod, callback)
case rune: case rune:
err = ct.g.SetKeybinding(view, t, mod, callback) 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 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 ... // DeleteKeybinding ...
func (ct *Cointop) DeleteKeybinding(shortcutKey string) error { func (ct *Cointop) DeleteKeybinding(shortcutKey string) error {
key, mod := ct.ParseKeys(shortcutKey) key, mod := ct.ParseKeys(shortcutKey)
@ -402,13 +378,14 @@ func (ct *Cointop) DeleteKeybinding(shortcutKey string) error {
} }
// DeleteKeybindingMod ... // DeleteKeybindingMod ...
func (ct *Cointop) DeleteKeybindingMod(key interface{}, mod gocui.Modifier, view string) error { func (ct *Cointop) DeleteKeybindingMod(key interface{}, mod tcell.ModMask, view string) error {
// TODO: take EventKey
var err error var err error
switch t := key.(type) { switch t := key.(type) {
case gocui.Key: case tcell.Key:
err = ct.g.DeleteKeybinding(view, t, mod) err = ct.g.DeleteKeybinding(view, t, 0, mod)
case rune: case rune:
err = ct.g.DeleteKeybinding(view, t, mod) err = ct.g.DeleteKeybinding(view, tcell.KeyRune, t, mod)
} }
return err return err
} }

@ -58,8 +58,7 @@ func (ct *Cointop) layout() error {
} else { } else {
if err := ct.ui.SetView(ct.Views.Marketbar, 0, topOffset-1, maxX, marketbarHeight+1); err != nil { if err := ct.ui.SetView(ct.Views.Marketbar, 0, topOffset-1, maxX, marketbarHeight+1); err != nil {
ct.Views.Marketbar.SetFrame(false) ct.Views.Marketbar.SetFrame(false)
ct.Views.Marketbar.SetFgColor(ct.colorscheme.GocuiFgColor(ct.Views.Marketbar.Name())) ct.Views.Marketbar.SetStyle(ct.colorscheme.Style(ct.Views.Marketbar.Name()))
ct.Views.Marketbar.SetBgColor(ct.colorscheme.GocuiBgColor(ct.Views.Marketbar.Name()))
go func() { go func() {
ct.UpdateMarketbar() ct.UpdateMarketbar()
_, found := ct.cache.Get(ct.Views.Marketbar.Name()) _, found := ct.cache.Get(ct.Views.Marketbar.Name())
@ -92,8 +91,7 @@ func (ct *Cointop) layout() error {
if err := ct.ui.SetView(ct.Views.Chart, 0, chartTopOffset, maxX, topOffset+chartHeight); err != nil { if err := ct.ui.SetView(ct.Views.Chart, 0, chartTopOffset, maxX, topOffset+chartHeight); err != nil {
ct.Views.Chart.Clear() ct.Views.Chart.Clear()
ct.Views.Chart.SetFrame(false) ct.Views.Chart.SetFrame(false)
ct.Views.Chart.SetFgColor(ct.colorscheme.GocuiFgColor(ct.Views.Chart.Name())) ct.Views.Chart.SetStyle(ct.colorscheme.Style(ct.Views.Chart.Name()))
ct.Views.Chart.SetBgColor(ct.colorscheme.GocuiBgColor(ct.Views.Chart.Name()))
go func() { go func() {
ct.UpdateChart() ct.UpdateChart()
cachekey := ct.CompositeCacheKey("globaldata", "", "", ct.State.selectedChartRange) cachekey := ct.CompositeCacheKey("globaldata", "", "", ct.State.selectedChartRange)
@ -124,8 +122,7 @@ func (ct *Cointop) layout() error {
topOffset = topOffset + chartHeight topOffset = topOffset + chartHeight
if err := ct.ui.SetView(ct.Views.TableHeader, tableOffsetX, topOffset-1, maxX, topOffset+1); err != nil { if err := ct.ui.SetView(ct.Views.TableHeader, tableOffsetX, topOffset-1, maxX, topOffset+1); err != nil {
ct.Views.TableHeader.SetFrame(false) ct.Views.TableHeader.SetFrame(false)
ct.Views.TableHeader.SetFgColor(ct.colorscheme.GocuiFgColor(ct.Views.TableHeader.Name())) ct.Views.TableHeader.SetStyle(ct.colorscheme.Style(ct.Views.TableHeader.Name()))
ct.Views.TableHeader.SetBgColor(ct.colorscheme.GocuiBgColor(ct.Views.TableHeader.Name()))
go ct.UpdateTableHeader() go ct.UpdateTableHeader()
} }
@ -133,8 +130,7 @@ func (ct *Cointop) layout() error {
if err := ct.ui.SetView(ct.Views.Table, tableOffsetX, topOffset-1, maxX, maxY-statusbarHeight); err != nil { if err := ct.ui.SetView(ct.Views.Table, tableOffsetX, topOffset-1, maxX, maxY-statusbarHeight); err != nil {
ct.Views.Table.SetFrame(false) ct.Views.Table.SetFrame(false)
ct.Views.Table.SetHighlight(true) ct.Views.Table.SetHighlight(true)
ct.Views.Table.SetSelFgColor(ct.colorscheme.GocuiFgColor("table_row_active")) ct.Views.Table.SetSelStyle(ct.colorscheme.Style("table_row_active"))
ct.Views.Table.SetSelBgColor(ct.colorscheme.GocuiBgColor("table_row_active"))
_, found := ct.cache.Get("allCoinsSlugMap") _, found := ct.cache.Get("allCoinsSlugMap")
if found { if found {
ct.cache.Delete("allCoinsSlugMap") ct.cache.Delete("allCoinsSlugMap")
@ -149,8 +145,7 @@ func (ct *Cointop) layout() error {
if !ct.State.hideStatusbar { if !ct.State.hideStatusbar {
if err := ct.ui.SetView(ct.Views.Statusbar, 0, maxY-statusbarHeight-1, maxX, maxY); err != nil { if err := ct.ui.SetView(ct.Views.Statusbar, 0, maxY-statusbarHeight-1, maxX, maxY); err != nil {
ct.Views.Statusbar.SetFrame(false) ct.Views.Statusbar.SetFrame(false)
ct.Views.Statusbar.SetFgColor(ct.colorscheme.GocuiFgColor(ct.Views.Statusbar.Name())) ct.Views.Statusbar.SetStyle(ct.colorscheme.Style(ct.Views.Statusbar.Name()))
ct.Views.Statusbar.SetBgColor(ct.colorscheme.GocuiBgColor(ct.Views.Statusbar.Name()))
go ct.UpdateStatusbar("") go ct.UpdateStatusbar("")
} }
} else { } else {
@ -166,22 +161,19 @@ func (ct *Cointop) layout() error {
ct.Views.SearchField.SetEditable(true) ct.Views.SearchField.SetEditable(true)
ct.Views.SearchField.SetWrap(true) ct.Views.SearchField.SetWrap(true)
ct.Views.SearchField.SetFrame(false) ct.Views.SearchField.SetFrame(false)
ct.Views.SearchField.SetFgColor(ct.colorscheme.GocuiFgColor("searchbar")) ct.Views.SearchField.SetStyle(ct.colorscheme.Style("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 { if err := ct.ui.SetView(ct.Views.Menu, 1, 1, maxX-1, maxY-1); err != nil {
ct.Views.Menu.SetFrame(false) ct.Views.Menu.SetFrame(false)
ct.Views.Menu.SetFgColor(ct.colorscheme.GocuiFgColor("menu")) ct.Views.Menu.SetStyle(ct.colorscheme.Style("menu"))
ct.Views.Menu.SetBgColor(ct.colorscheme.GocuiBgColor("menu"))
} }
if err := ct.ui.SetView(ct.Views.Input, 3, 6, 30, 8); err != nil { if err := ct.ui.SetView(ct.Views.Input, 3, 6, 30, 8); err != nil {
ct.Views.Input.SetFrame(true) ct.Views.Input.SetFrame(true)
ct.Views.Input.SetEditable(true) ct.Views.Input.SetEditable(true)
ct.Views.Input.SetWrap(true) ct.Views.Input.SetWrap(true)
ct.Views.Input.SetFgColor(ct.colorscheme.GocuiFgColor("menu")) ct.Views.Input.SetStyle(ct.colorscheme.Style("menu"))
ct.Views.Input.SetBgColor(ct.colorscheme.GocuiBgColor("menu"))
// run only once on init. // run only once on init.
// this bit of code should be at the bottom // this bit of code should be at the bottom

@ -4,12 +4,14 @@ import (
"sync" "sync"
"time" "time"
types "github.com/miguelmota/cointop/pkg/api/types" "github.com/cointop-sh/cointop/pkg/api/types"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var coinslock sync.Mutex var (
var updatecoinsmux sync.Mutex coinslock sync.Mutex
updatecoinsmux sync.Mutex
)
// UpdateCoins updates coins view // UpdateCoins updates coins view
func (ct *Cointop) UpdateCoins() error { func (ct *Cointop) UpdateCoins() error {
@ -27,9 +29,12 @@ func (ct *Cointop) UpdateCoins() error {
log.Debug("UpdateCoins() soft cache hit") log.Debug("UpdateCoins() soft cache hit")
} }
// cache miss // cache miss or coin struct has been changed from the last time
if allCoinsSlugMap == nil { isCacheMissed := allCoinsSlugMap == nil
log.Debug("UpdateCoins() cache miss") currentCoinHash, _ := getStructHash(Coin{})
isCoinStructHashChanged := currentCoinHash != ct.config.CoinStructHash
if isCacheMissed || isCoinStructHashChanged {
log.Debug("UpdateCoins() cache miss or coin struct has changed")
ch := make(chan []types.Coin) ch := make(chan []types.Coin)
err = ct.api.GetAllCoinData(ct.State.currencyConversion, ch) err = ct.api.GetAllCoinData(ct.State.currencyConversion, ch)
if err != nil { if err != nil {
@ -46,6 +51,22 @@ func (ct *Cointop) UpdateCoins() error {
return nil 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 // ProcessCoinsMap processes coins map
func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) { func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) {
log.Debug("ProcessCoinsMap()") log.Debug("ProcessCoinsMap()")
@ -69,7 +90,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
for _, v := range coins { for _, v := range coins {
k := v.Name k := v.Name
// Fix for https://github.com/miguelmota/cointop/issues/59 // Fix for https://github.com/cointop-sh/cointop/issues/59
// some APIs returns rank 0 for new coins // some APIs returns rank 0 for new coins
// or coins with low market cap data so we need to put them // or coins with low market cap data so we need to put them
// at the end of the list // at the end of the list
@ -94,6 +115,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
PercentChange30D: v.PercentChange30D, PercentChange30D: v.PercentChange30D,
PercentChange1Y: v.PercentChange1Y, PercentChange1Y: v.PercentChange1Y,
LastUpdated: v.LastUpdated, LastUpdated: v.LastUpdated,
Slug: v.Slug,
}) })
if ilast != nil { if ilast != nil {
last, _ := ilast.(*Coin) last, _ := ilast.(*Coin)
@ -114,7 +136,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
}) })
if len(ct.State.allCoins) < size { if len(ct.State.allCoins) < size {
list := []*Coin{} var list []*Coin
for _, v := range coins { for _, v := range coins {
k := v.Name k := v.Name
icoin, _ := ct.State.allCoinsSlugMap.Load(k) icoin, _ := ct.State.allCoinsSlugMap.Load(k)
@ -146,6 +168,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
c.PercentChange1Y = cm.PercentChange1Y c.PercentChange1Y = cm.PercentChange1Y
c.LastUpdated = cm.LastUpdated c.LastUpdated = cm.LastUpdated
c.Favorite = cm.Favorite c.Favorite = cm.Favorite
c.Slug = cm.Slug
} }
} }
@ -154,7 +177,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
} }
time.AfterFunc(10*time.Millisecond, func() { time.AfterFunc(10*time.Millisecond, func() {
ct.Sort(ct.State.sortBy, ct.State.sortDesc, ct.State.coins, true) ct.Sort(ct.State.viewSorts[ct.State.selectedView], ct.State.coins, true)
ct.UpdateTable() ct.UpdateTable()
}) })
} }

@ -6,11 +6,12 @@ import (
"strings" "strings"
"time" "time"
types "github.com/miguelmota/cointop/pkg/api/types" fcolor "github.com/fatih/color"
"github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/humanize" "github.com/cointop-sh/cointop/pkg/api/types"
"github.com/miguelmota/cointop/pkg/pad" "github.com/cointop-sh/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -19,8 +20,7 @@ type MarketbarView = ui.View
// NewMarketbarView returns a new marketbar view // NewMarketbarView returns a new marketbar view
func NewMarketbarView() *MarketbarView { func NewMarketbarView() *MarketbarView {
var view *MarketbarView = ui.NewView("marketbar") return ui.NewView("marketbar")
return view
} }
// UpdateMarketbar updates the market bar view // UpdateMarketbar updates the market bar view
@ -29,7 +29,9 @@ func (ct *Cointop) UpdateMarketbar() error {
maxX := ct.Width() maxX := ct.Width()
logo := "cointop" logo := "cointop"
if ct.colorschemeName == "cointop" { if ct.colorschemeName == "cointop" {
logo = fmt.Sprintf("%s%s%s%s", color.Green(""), color.Cyan(""), color.Green(""), color.Cyan("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"))
} }
var content string var content string
@ -41,6 +43,9 @@ func (ct *Cointop) UpdateMarketbar() error {
total = math.Round(total*1e2) / 1e2 total = math.Round(total*1e2) / 1e2
totalstr = humanize.Monetaryf(total, 2) totalstr = humanize.Monetaryf(total, 2)
} }
if ct.State.compactNotation {
totalstr = humanize.ScaleNumericf(total, 3)
}
timeframe := ct.State.selectedChartRange timeframe := ct.State.selectedChartRange
chartname := ct.SelectedCoinName() chartname := ct.SelectedCoinName()
@ -54,7 +59,7 @@ func (ct *Cointop) UpdateMarketbar() error {
var percentChange24H float64 var percentChange24H float64
for _, p := range ct.GetPortfolioSlice() { for _, p := range ct.GetPortfolioSlice() {
n := ((p.Balance / total) * p.PercentChange24H) n := (p.Balance / total) * p.PercentChange24H
if math.IsNaN(n) { if math.IsNaN(n) {
continue continue
} }
@ -89,8 +94,9 @@ func (ct *Cointop) UpdateMarketbar() error {
} }
content = fmt.Sprintf( content = fmt.Sprintf(
"%sTotal Portfolio Value: %s • 24H: %s", "%sTotal Portfolio Value %s: %s • 24H: %s",
chartInfo, chartInfo,
ct.State.currencyConversion,
ct.colorscheme.MarketBarLabelActive(totalstr), ct.colorscheme.MarketBarLabelActive(totalstr),
percentChange24Hstr, percentChange24Hstr,
) )
@ -139,7 +145,7 @@ func (ct *Cointop) UpdateMarketbar() error {
chartInfo := "" chartInfo := ""
if !ct.State.hideChart { if !ct.State.hideChart {
chartInfo = fmt.Sprintf( chartInfo = fmt.Sprintf(
"[ Chart: %s %s ] ", "[ Chart: %s %s] ",
ct.colorscheme.MarketBarLabelActive(chartname), ct.colorscheme.MarketBarLabelActive(chartname),
timeframe, timeframe,
) )
@ -154,12 +160,20 @@ func (ct *Cointop) UpdateMarketbar() error {
separator2 = "\n" + offset 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( content = fmt.Sprintf(
"%sGlobal ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%", "%sGlobal %s ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%",
chartInfo, chartInfo,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.TotalMarketCapUSD, 0)), ct.State.currencyConversion,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), marketCapStr),
separator1, separator1,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.Total24HVolumeUSD, 0)), fmt.Sprintf("%s%s", ct.CurrencySymbol(), volumeStr),
separator2, separator2,
market.BitcoinPercentageOfMarketCap, market.BitcoinPercentageOfMarketCap,
) )

@ -1,7 +1,7 @@
package cointop package cointop
import ( import (
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -10,8 +10,7 @@ type MenuView = ui.View
// NewMenuView returns a new menu view // NewMenuView returns a new menu view
func NewMenuView() *MenuView { func NewMenuView() *MenuView {
var view *MenuView = ui.NewView("menu") return ui.NewView("menu")
return view
} }
// HideMenu hides the menu view // HideMenu hides the menu view

@ -309,6 +309,13 @@ func (ct *Cointop) PrevPageTop() error {
return nil 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 // FirstPage navigates to the first page
func (ct *Cointop) FirstPage() error { func (ct *Cointop) FirstPage() error {
log.Debug("FirstPage()") log.Debug("FirstPage()")
@ -409,13 +416,18 @@ func (ct *Cointop) GoToPageRowIndex(idx int) error {
// GoToGlobalIndex navigates to the selected row index of all page rows // GoToGlobalIndex navigates to the selected row index of all page rows
func (ct *Cointop) GoToGlobalIndex(idx int) error { func (ct *Cointop) GoToGlobalIndex(idx int) error {
log.Debug("GoToGlobalIndex()") log.Debugf("GoToGlobalIndex(%d)", idx)
target := ct.State.allCoins[idx]
l := ct.TableRowsLen() l := ct.TableRowsLen()
atpage := idx / l atpage := idx / l
ct.SetPage(atpage) ct.SetPage(atpage)
rowIndex := (idx % l)
ct.HighlightRow(rowIndex)
ct.UpdateTable() 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 return nil
} }
@ -537,7 +549,7 @@ func (ct *Cointop) TableScrollLeft() error {
return nil return nil
} }
// TableScrollRight scrolls the the table to the right // TableScrollRight scrolls the table to the right
func (ct *Cointop) TableScrollRight() error { func (ct *Cointop) TableScrollRight() error {
ct.State.tableOffsetX-- 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))
@ -548,34 +560,9 @@ func (ct *Cointop) TableScrollRight() error {
return nil return nil
} }
// MouseRelease is called on mouse releae event
func (ct *Cointop) MouseRelease() error {
return nil
}
// MouseLeftClick is called on mouse left click event // MouseLeftClick is called on mouse left click event
func (ct *Cointop) MouseLeftClick() error { func (ct *Cointop) MouseLeftClick() error {
return nil return ct.g.SetCursorFromCurrentMouseEvent()
}
// 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 // TableRowsLen returns the number of table row entries

@ -12,11 +12,11 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/miguelmota/cointop/pkg/asciitable" "github.com/cointop-sh/cointop/pkg/asciitable"
"github.com/miguelmota/cointop/pkg/eval" "github.com/cointop-sh/cointop/pkg/eval"
"github.com/miguelmota/cointop/pkg/humanize" "github.com/cointop-sh/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/pad" "github.com/cointop-sh/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table" "github.com/cointop-sh/cointop/pkg/table"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -35,6 +35,10 @@ var SupportedPortfolioTableHeaders = []string{
"1y_change", "1y_change",
"percent_holdings", "percent_holdings",
"last_updated", "last_updated",
"cost_price",
"cost",
"pnl",
"pnl_percent",
} }
// DefaultPortfolioTableHeaders are the default portfolio table header columns // DefaultPortfolioTableHeaders are the default portfolio table header columns
@ -49,12 +53,23 @@ var DefaultPortfolioTableHeaders = []string{
"24h_change", "24h_change",
"7d_change", "7d_change",
"percent_holdings", "percent_holdings",
"cost_price",
"cost",
"pnl",
"pnl_percent",
"last_updated", "last_updated",
} }
// HiddenBalanceChars are the characters to show when hidding balances // HiddenBalanceChars are the characters to show when hidding balances
var HiddenBalanceChars = "********" var HiddenBalanceChars = "********"
var costColumns = map[string]bool{
"cost_price": true,
"cost": true,
"pnl": true,
"pnl_percent": true,
}
// ValidPortfolioTableHeader returns the portfolio table headers // ValidPortfolioTableHeader returns the portfolio table headers
func (ct *Cointop) ValidPortfolioTableHeader(name string) bool { func (ct *Cointop) ValidPortfolioTableHeader(name string) bool {
for _, v := range SupportedPortfolioTableHeaders { for _, v := range SupportedPortfolioTableHeaders {
@ -78,8 +93,27 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
t := table.NewTable().SetWidth(maxX) t := table.NewTable().SetWidth(maxX)
var rows [][]*table.RowCell var rows [][]*table.RowCell
headers := ct.GetPortfolioTableHeaders() headers := ct.GetPortfolioTableHeaders()
ct.ClearSyncMap(ct.State.tableColumnWidths) ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft) 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
}
for _, coin := range ct.State.coins { for _, coin := range ct.State.coins {
leftMargin := 1 leftMargin := 1
rightMargin := 1 rightMargin := 1
@ -89,7 +123,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
case "rank": case "rank":
star := ct.colorscheme.TableRow(" ") star := ct.colorscheme.TableRow(" ")
if coin.Favorite { if coin.Favorite {
star = ct.colorscheme.TableRowFavorite("*") star = ct.colorscheme.TableRowFavorite(ct.State.favoriteChar)
} }
rank := fmt.Sprintf("%s%v", star, ct.colorscheme.TableRow(fmt.Sprintf("%6v ", coin.Rank))) rank := fmt.Sprintf("%s%v", star, ct.colorscheme.TableRow(fmt.Sprintf("%6v ", coin.Rank)))
ct.SetTableColumnWidth(header, 8) ct.SetTableColumnWidth(header, 8)
@ -290,7 +324,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
}) })
case "last_updated": case "last_updated":
unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") lastUpdated := humanize.FormatTime(time.Unix(unix, 0), "15:04:05 Jan 02")
ct.SetTableColumnWidthFromString(header, lastUpdated) ct.SetTableColumnWidthFromString(header, lastUpdated)
ct.SetTableColumnAlignLeft(header, false) ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells, rowCells = append(rowCells,
@ -301,6 +335,117 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
Color: ct.colorscheme.TableRow, Color: ct.colorscheme.TableRow,
Text: lastUpdated, 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,
})
} }
} }
@ -456,8 +601,12 @@ func (ct *Cointop) SetPortfolioHoldings() error {
} }
shouldDelete := holdings == 0 shouldDelete := holdings == 0
// TODO: add fields to form, parse here
buyPrice := 0.0
buyCurrency := ""
idx := ct.GetPortfolioCoinIndex(coin) idx := ct.GetPortfolioCoinIndex(coin)
if err := ct.SetPortfolioEntry(coin.Name, holdings); err != nil { if err := ct.SetPortfolioEntry(coin.Name, holdings, buyPrice, buyCurrency); err != nil {
return err return err
} }
@ -482,7 +631,7 @@ func (ct *Cointop) SetPortfolioHoldings() error {
// PortfolioEntry returns a portfolio entry // PortfolioEntry returns a portfolio entry
func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) { func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
//log.Debug("PortfolioEntry()") // too many // log.Debug("PortfolioEntry()") // too many
if c == nil { if c == nil {
return &PortfolioEntry{}, true return &PortfolioEntry{}, true
} }
@ -492,22 +641,18 @@ func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
var ok bool var ok bool
key := strings.ToLower(c.Name) key := strings.ToLower(c.Name)
if p, ok = ct.State.portfolio.Entries[key]; !ok { if p, ok = ct.State.portfolio.Entries[key]; !ok {
// NOTE: if not found then try the symbol p = &PortfolioEntry{
key := strings.ToLower(c.Symbol) Coin: c.Name,
if p, ok = ct.State.portfolio.Entries[key]; !ok { Holdings: 0,
p = &PortfolioEntry{
Coin: c.Name,
Holdings: 0,
}
isNew = true
} }
isNew = true
} }
return p, isNew return p, isNew
} }
// SetPortfolioEntry sets a portfolio entry // SetPortfolioEntry sets a portfolio entry
func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error { func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64, buyPrice float64, buyCurrency string) error {
log.Debug("SetPortfolioEntry()") log.Debug("SetPortfolioEntry()")
ic, _ := ct.State.allCoinsSlugMap.Load(strings.ToLower(coin)) ic, _ := ct.State.allCoinsSlugMap.Load(strings.ToLower(coin))
c, _ := ic.(*Coin) c, _ := ic.(*Coin)
@ -515,8 +660,10 @@ func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
if isNew { if isNew {
key := strings.ToLower(coin) key := strings.ToLower(coin)
ct.State.portfolio.Entries[key] = &PortfolioEntry{ ct.State.portfolio.Entries[key] = &PortfolioEntry{
Coin: coin, Coin: coin,
Holdings: holdings, Holdings: holdings,
BuyPrice: buyPrice,
BuyCurrency: buyCurrency,
} }
} else { } else {
p.Holdings = holdings p.Holdings = holdings
@ -555,31 +702,21 @@ func (ct *Cointop) PortfolioEntriesCount() int {
// GetPortfolioSlice returns portfolio entries as a slice // GetPortfolioSlice returns portfolio entries as a slice
func (ct *Cointop) GetPortfolioSlice() []*Coin { func (ct *Cointop) GetPortfolioSlice() []*Coin {
log.Debug("GetPortfolioSlice()") log.Debug("GetPortfolioSlice()")
sliced := []*Coin{} var sliced []*Coin
if ct.PortfolioEntriesCount() == 0 { if ct.PortfolioEntriesCount() == 0 {
return sliced return sliced
} }
OUTER: for _, p := range ct.State.portfolio.Entries {
for i := range ct.State.allCoins { coinIfc, _ := ct.State.allCoinsSlugMap.Load(p.Coin)
coin := ct.State.allCoins[i] coin, ok := coinIfc.(*Coin)
p, isNew := ct.PortfolioEntry(coin) if !ok {
if isNew { log.Errorf("Could not find coin %s", p.Coin)
continue continue
} }
// check not already found
updateSlice := -1
for j := range sliced {
if coin.Symbol == sliced[j].Symbol {
if coin.Rank >= sliced[j].Rank {
continue OUTER // skip updates from lower-ranked coins
}
updateSlice = j // update this later
break
}
}
coin.Holdings = p.Holdings coin.Holdings = p.Holdings
coin.BuyPrice = p.BuyPrice
coin.BuyCurrency = p.BuyCurrency
balance := coin.Price * p.Holdings balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance) balancestr := fmt.Sprintf("%.2f", balance)
if ct.State.currencyConversion == "ETH" || ct.State.currencyConversion == "BTC" { if ct.State.currencyConversion == "ETH" || ct.State.currencyConversion == "BTC" {
@ -587,15 +724,10 @@ OUTER:
} }
balance, _ = strconv.ParseFloat(balancestr, 64) balance, _ = strconv.ParseFloat(balancestr, 64)
coin.Balance = balance coin.Balance = balance
if updateSlice == -1 { sliced = append(sliced, coin)
sliced = append(sliced, coin)
} else {
sliced[updateSlice] = coin
}
} }
sort.Slice(sliced, func(i, j int) bool { sort.SliceStable(sliced, func(i, j int) bool {
return sliced[i].Balance > sliced[j].Balance return sliced[i].Balance > sliced[j].Balance
}) })
@ -698,7 +830,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
return fmt.Errorf("the option %q is not a valid column name", sortBy) return fmt.Errorf("the option %q is not a valid column name", sortBy)
} }
ct.Sort(sortBy, sortDesc, holdings, true) ct.Sort(&sortConstraint{sortBy: sortBy, sortDesc: sortDesc}, holdings, true)
} }
if _, ok := outputFormats[format]; !ok { if _, ok := outputFormats[format]; !ok {
@ -709,7 +841,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
records := make([][]string, len(holdings)) records := make([][]string, len(holdings))
symbol := ct.CurrencySymbol() symbol := ct.CurrencySymbol()
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"} headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings", "cost_price", "cost", "pnl", "pnl_percent"}
if len(filterCols) > 0 { if len(filterCols) > 0 {
for _, col := range filterCols { for _, col := range filterCols {
valid := false valid := false
@ -806,6 +938,70 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
if hideBalances { if hideBalances {
item[i] = HiddenBalanceChars 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 records[i] = item
@ -983,7 +1179,7 @@ func (ct *Cointop) PrintHoldings24HChange(options *TablePrintOptions) error {
} }
} }
n := ((entry.Balance / total) * entry.PercentChange24H) n := (entry.Balance / total) * entry.PercentChange24H
if math.IsNaN(n) { if math.IsNaN(n) {
continue continue
} }

@ -5,8 +5,8 @@ import (
"math" "math"
"strings" "strings"
"github.com/miguelmota/cointop/pkg/api" "github.com/cointop-sh/cointop/pkg/api"
"github.com/miguelmota/cointop/pkg/humanize" "github.com/cointop-sh/cointop/pkg/humanize"
) )
// PriceConfig is the config options for the coin price method // PriceConfig is the config options for the coin price method

@ -8,10 +8,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/miguelmota/cointop/pkg/humanize" "github.com/cointop-sh/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/notifier" "github.com/cointop-sh/cointop/pkg/notifier"
"github.com/miguelmota/cointop/pkg/pad" "github.com/cointop-sh/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table" "github.com/cointop-sh/cointop/pkg/table"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -48,8 +48,8 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table {
t := table.NewTable().SetWidth(maxX) t := table.NewTable().SetWidth(maxX)
var rows [][]*table.RowCell var rows [][]*table.RowCell
headers := ct.GetPriceAlertsTableHeaders() headers := ct.GetPriceAlertsTableHeaders()
ct.ClearSyncMap(ct.State.tableColumnWidths) ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft) ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
for _, entry := range ct.State.priceAlerts.Entries { for _, entry := range ct.State.priceAlerts.Entries {
if entry.Expired { if entry.Expired {
continue continue
@ -477,7 +477,7 @@ func (ct *Cointop) SetPriceAlert(coinName string, operator string, targetPrice f
func (ct *Cointop) RemovePriceAlert(id string) error { func (ct *Cointop) RemovePriceAlert(id string) error {
log.Debug("RemovePriceAlert()") log.Debug("RemovePriceAlert()")
for i, entry := range ct.State.priceAlerts.Entries { for i, entry := range ct.State.priceAlerts.Entries {
if entry.ID == ct.State.priceAlertEditID { if entry.ID == id {
ct.State.priceAlerts.Entries = append(ct.State.priceAlerts.Entries[:i], ct.State.priceAlerts.Entries[i+1:]...) ct.State.priceAlerts.Entries = append(ct.State.priceAlerts.Entries[:i], ct.State.priceAlerts.Entries[i+1:]...)
} }
} }

@ -3,7 +3,7 @@ package cointop
import ( import (
"os" "os"
"github.com/miguelmota/gocui" "github.com/cointop-sh/cointop/pkg/gocui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

@ -4,8 +4,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/miguelmota/cointop/pkg/levenshtein" "github.com/cointop-sh/cointop/pkg/levenshtein"
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -14,8 +14,7 @@ type SearchFieldView = ui.View
// NewSearchFieldView returns a new search field view // NewSearchFieldView returns a new search field view
func NewSearchFieldView() *SearchFieldView { func NewSearchFieldView() *SearchFieldView {
var view *SearchFieldView = ui.NewView("searchfield") return ui.NewView("searchfield")
return view
} }
// InputView is structure for help view // InputView is structure for help view
@ -23,8 +22,7 @@ type InputView = ui.View
// NewInputView returns a new help view // NewInputView returns a new help view
func NewInputView() *InputView { func NewInputView() *InputView {
var view *InputView = ui.NewView("input") return ui.NewView("input")
return view
} }
// OpenSearch opens the search field // OpenSearch opens the search field
@ -68,7 +66,7 @@ func (ct *Cointop) DoSearch() error {
if n == 0 { if n == 0 {
return nil return nil
} }
q := string(b) q := strings.TrimSpace(string(b[:n]))
// remove slash // remove slash
regex := regexp.MustCompile(`/(.*)`) regex := regexp.MustCompile(`/(.*)`)
matches := regex.FindStringSubmatch(q) matches := regex.FindStringSubmatch(q)
@ -80,26 +78,68 @@ func (ct *Cointop) DoSearch() error {
// Search performs the search and filtering // Search performs the search and filtering
func (ct *Cointop) Search(q string) error { func (ct *Cointop) Search(q string) error {
log.Debug("Search()") 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.
q = strings.TrimSpace(strings.ToLower(q)) 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 idx := -1
min := -1 min := -1
var hasprefixidx []int var hasprefixidx []int
var hasprefixdist []int var hasprefixdist []int
for i := range ct.State.allCoins {
// 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++ {
coin := ct.State.allCoins[i] coin := ct.State.allCoins[i]
name := strings.ToLower(coin.Name) name := strings.ToLower(coin.Name)
symbol := strings.ToLower(coin.Symbol) symbol := strings.ToLower(coin.Symbol)
// if query matches symbol, return immediately // if query matches symbol, return immediately
if symbol == q { if canSearchSymbol && symbol == q {
ct.GoToGlobalIndex(i) ct.GoToGlobalIndex(i)
return nil return nil
} }
if !canSearchName {
continue
}
// if query matches name, return immediately // if query matches name, return immediately
if name == q { if name == q {
ct.GoToGlobalIndex(i) ct.GoToGlobalIndex(i)
return nil return nil
} }
// store index with the smallest levenshtein // store index with the smallest levenshtein
dist := levenshtein.DamerauLevenshteinDistance(name, q) dist := levenshtein.DamerauLevenshteinDistance(name, q)
if min == -1 || dist <= min { if min == -1 || dist <= min {
@ -114,15 +154,22 @@ func (ct *Cointop) Search(q string) error {
} }
} }
} }
if !canSearchName {
return nil
}
// go to row if prefix match // go to row if prefix match
if len(hasprefixidx) > 0 && hasprefixidx[0] != -1 && min > 0 { if len(hasprefixidx) > 0 && hasprefixidx[0] != -1 && min > 0 {
ct.GoToGlobalIndex(hasprefixidx[0]) ct.GoToGlobalIndex(hasprefixidx[0])
return nil return nil
} }
// go to row if levenshtein distance is small enough // go to row if levenshtein distance is small enough
if idx > -1 && min <= 6 { if idx > -1 && min <= 6 {
ct.GoToGlobalIndex(idx) ct.GoToGlobalIndex(idx)
return nil return nil
} }
return nil return nil
} }

@ -4,27 +4,27 @@ import (
"sort" "sort"
"sync" "sync"
"github.com/miguelmota/gocui" "github.com/cointop-sh/cointop/pkg/gocui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var sortlock sync.Mutex var sortlock sync.Mutex
// Sort sorts the list of coins // Sort sorts the list of coins
func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bool) { func (ct *Cointop) Sort(sortCons *sortConstraint, list []*Coin, renderHeaders bool) {
log.Debug("Sort()") log.Debug("Sort()")
sortlock.Lock() sortlock.Lock()
defer sortlock.Unlock() defer sortlock.Unlock()
ct.State.sortBy = sortBy
ct.State.sortDesc = desc ct.State.viewSorts[ct.State.selectedView] = sortCons
if list == nil { if list == nil {
return return
} }
if len(list) < 2 { if len(list) < 2 {
return return
} }
sort.Slice(list[:], func(i, j int) bool { sort.SliceStable(list[:], func(i, j int) bool {
if ct.State.sortDesc { if sortCons.sortDesc {
i, j = j, i i, j = j, i
} }
a := list[i] a := list[i]
@ -35,7 +35,7 @@ func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bo
if b == nil { if b == nil {
return false return false
} }
switch sortBy { switch sortCons.sortBy {
case "rank": case "rank":
return a.Rank < b.Rank return a.Rank < b.Rank
case "name": case "name":
@ -68,6 +68,14 @@ func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bo
return a.AvailableSupply < b.AvailableSupply return a.AvailableSupply < b.AvailableSupply
case "last_updated": case "last_updated":
return a.LastUpdated < b.LastUpdated 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: default:
return a.Rank < b.Rank return a.Rank < b.Rank
} }
@ -81,7 +89,7 @@ func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bo
// SortAsc sorts list of coins in ascending order // SortAsc sorts list of coins in ascending order
func (ct *Cointop) SortAsc() error { func (ct *Cointop) SortAsc() error {
log.Debug("SortAsc()") log.Debug("SortAsc()")
ct.State.sortDesc = false ct.State.viewSorts[ct.State.selectedView].sortDesc = false
ct.UpdateTable() ct.UpdateTable()
return nil return nil
} }
@ -89,7 +97,7 @@ func (ct *Cointop) SortAsc() error {
// SortDesc sorts list of coins in descending order // SortDesc sorts list of coins in descending order
func (ct *Cointop) SortDesc() error { func (ct *Cointop) SortDesc() error {
log.Debug("SortDesc()") log.Debug("SortDesc()")
ct.State.sortDesc = true ct.State.viewSorts[ct.State.selectedView].sortDesc = true
ct.UpdateTable() ct.UpdateTable()
return nil return nil
} }
@ -104,7 +112,10 @@ func (ct *Cointop) SortPrevCol() error {
k = 0 k = 0
} }
nextsortBy := cols[k] nextsortBy := cols[k]
ct.Sort(nextsortBy, ct.State.sortDesc, ct.State.coins, true)
curSortConst := ct.State.viewSorts[ct.State.selectedView]
curSortConst.sortBy = nextsortBy
ct.Sort(curSortConst, ct.State.coins, true)
ct.UpdateTable() ct.UpdateTable()
return nil return nil
} }
@ -120,7 +131,9 @@ func (ct *Cointop) SortNextCol() error {
k = l - 1 k = l - 1
} }
nextsortBy := cols[k] nextsortBy := cols[k]
ct.Sort(nextsortBy, ct.State.sortDesc, ct.State.coins, true) curSortCons := ct.State.viewSorts[ct.State.selectedView]
curSortCons.sortBy = nextsortBy
ct.Sort(curSortCons, ct.State.coins, true)
ct.UpdateTable() ct.UpdateTable()
return nil return nil
} }
@ -128,11 +141,15 @@ func (ct *Cointop) SortNextCol() error {
// SortToggle toggles the sort order // SortToggle toggles the sort order
func (ct *Cointop) SortToggle(sortBy string, desc bool) error { func (ct *Cointop) SortToggle(sortBy string, desc bool) error {
log.Debug("SortToggle()") log.Debug("SortToggle()")
if ct.State.sortBy == sortBy { curSortCons := ct.State.viewSorts[ct.State.selectedView]
desc = !ct.State.sortDesc if curSortCons.sortBy == sortBy {
curSortCons.sortDesc = !curSortCons.sortDesc
} else {
curSortCons.sortBy = sortBy
curSortCons.sortDesc = desc
} }
ct.Sort(sortBy, desc, ct.State.coins, true) ct.Sort(curSortCons, ct.State.coins, true)
ct.UpdateTable() ct.UpdateTable()
return nil return nil
} }
@ -161,7 +178,7 @@ func (ct *Cointop) GetSortColIndex() int {
log.Debug("GetSortColIndex()") log.Debug("GetSortColIndex()")
cols := ct.GetActiveTableHeaders() cols := ct.GetActiveTableHeaders()
for i, col := range cols { for i, col := range cols {
if ct.State.sortBy == col { if ct.State.viewSorts[ct.State.selectedView].sortBy == col {
return i return i
} }
} }

@ -2,11 +2,13 @@ package cointop
import ( import (
"fmt" "fmt"
"regexp"
"strings"
"unicode/utf8" "unicode/utf8"
"github.com/miguelmota/cointop/pkg/open" "github.com/cointop-sh/cointop/pkg/open"
"github.com/miguelmota/cointop/pkg/pad" "github.com/cointop-sh/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -15,8 +17,7 @@ type StatusbarView = ui.View
// NewStatusbarView returns a new statusbar view // NewStatusbarView returns a new statusbar view
func NewStatusbarView() *StatusbarView { func NewStatusbarView() *StatusbarView {
var view *StatusbarView = ui.NewView("statusbar") return ui.NewView("statusbar")
return view
} }
// UpdateStatusbar updates the statusbar view // UpdateStatusbar updates the statusbar view
@ -84,3 +85,51 @@ func (ct *Cointop) RefreshRowLink() error {
return nil 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
}

@ -3,9 +3,10 @@ package cointop
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -14,8 +15,7 @@ type TableView = ui.View
// NewTableView returns a new table view // NewTableView returns a new table view
func NewTableView() *TableView { func NewTableView() *TableView {
var view *TableView = ui.NewView("table") return ui.NewView("table")
return view
} }
const dots = "..." const dots = "..."
@ -81,16 +81,10 @@ func (ct *Cointop) UpdateTable() error {
} else if ct.IsPortfolioVisible() { } else if ct.IsPortfolioVisible() {
ct.State.coins = ct.GetPortfolioSlice() ct.State.coins = ct.GetPortfolioSlice()
} else { } 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.State.coins = ct.GetTableCoinsSlice()
} }
ct.Sort(ct.State.sortBy, ct.State.sortDesc, ct.State.coins, true) ct.Sort(ct.State.viewSorts[ct.State.selectedView], ct.State.coins, true)
go ct.RefreshTable() go ct.RefreshTable()
return nil return nil
} }
@ -98,7 +92,7 @@ func (ct *Cointop) UpdateTable() error {
// GetTableCoinsSlice returns a slice of the table rows // GetTableCoinsSlice returns a slice of the table rows
func (ct *Cointop) GetTableCoinsSlice() []*Coin { func (ct *Cointop) GetTableCoinsSlice() []*Coin {
log.Debug("GetTableCoinsSlice()") log.Debug("GetTableCoinsSlice()")
sliced := []*Coin{} var sliced []*Coin
start := ct.State.page * ct.State.perPage start := ct.State.page * ct.State.perPage
end := start + ct.State.perPage end := start + ct.State.perPage
allCoins := ct.AllCoins() allCoins := ct.AllCoins()
@ -198,7 +192,27 @@ func (ct *Cointop) RowLink() string {
return "" return ""
} }
return ct.api.CoinLink(coin.Name) // 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()")
coin := ct.HighlightedRowCoin()
if coin == nil {
return ""
}
return ct.GetAltCoinLink(coin)
} }
// RowLinkShort returns a shortened version of the row url link // RowLinkShort returns a shortened version of the row url link
@ -225,6 +239,20 @@ func (ct *Cointop) RowLinkShort() string {
return "" 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 // ToggleTableFullscreen toggles the table fullscreen mode
func (ct *Cointop) ToggleTableFullscreen() error { func (ct *Cointop) ToggleTableFullscreen() error {
log.Debug("ToggleTableFullscreen()") log.Debug("ToggleTableFullscreen()")
@ -266,6 +294,11 @@ func (ct *Cointop) ToggleTableFullscreen() error {
func (ct *Cointop) SetSelectedView(viewName string) { func (ct *Cointop) SetSelectedView(viewName string) {
ct.State.lastSelectedView = ct.State.selectedView ct.State.lastSelectedView = ct.State.selectedView
ct.State.selectedView = viewName 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 // ToggleSelectedView toggles between current table view and last selected table view

@ -6,8 +6,8 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/miguelmota/cointop/pkg/pad" "github.com/cointop-sh/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui" "github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -21,106 +21,140 @@ var ArrowDown = "▼"
type HeaderColumn struct { type HeaderColumn struct {
Slug string Slug string
Label string Label string
ShortLabel string // only columns with a ShortLabel can be scaled?
PlainLabel string PlainLabel string
} }
// HeaderColumns are the header column widths // HeaderColumns are the header column widths
var HeaderColumns = map[string]*HeaderColumn{ var HeaderColumns = map[string]*HeaderColumn{
"rank": &HeaderColumn{ "rank": {
Slug: "rank", Slug: "rank",
Label: "[r]ank", Label: "[r]ank",
PlainLabel: "rank", PlainLabel: "rank",
}, },
"name": &HeaderColumn{ "name": {
Slug: "name", Slug: "name",
Label: "[n]ame", Label: "[n]ame",
PlainLabel: "name", PlainLabel: "name",
}, },
"symbol": &HeaderColumn{ "symbol": {
Slug: "symbol", Slug: "symbol",
Label: "[s]ymbol", Label: "[s]ymbol",
PlainLabel: "symbol", PlainLabel: "symbol",
}, },
"target_price": &HeaderColumn{ "target_price": {
Slug: "target_price", Slug: "target_price",
Label: "[t]target price", Label: "[t]target price",
PlainLabel: "target price", PlainLabel: "target price",
}, },
"price": &HeaderColumn{ "price": {
Slug: "price", Slug: "price",
Label: "[p]rice", Label: "[p]rice",
PlainLabel: "price", PlainLabel: "price",
}, },
"frequency": &HeaderColumn{ "frequency": {
Slug: "frequency", Slug: "frequency",
Label: "frequency", Label: "frequency",
PlainLabel: "frequency", PlainLabel: "frequency",
}, },
"holdings": &HeaderColumn{ "holdings": {
Slug: "holdings", Slug: "holdings",
Label: "[h]oldings", Label: "[h]oldings",
PlainLabel: "holdings", PlainLabel: "holdings",
}, },
"balance": &HeaderColumn{ "balance": {
Slug: "balance", Slug: "balance",
Label: "[b]alance", Label: "[b]alance",
PlainLabel: "balance", PlainLabel: "balance",
}, },
"market_cap": &HeaderColumn{ "market_cap": {
Slug: "market_cap", Slug: "market_cap",
Label: "[m]arket cap", Label: "[m]arket cap",
ShortLabel: "[m]cap",
PlainLabel: "market cap", PlainLabel: "market cap",
}, },
"24h_volume": &HeaderColumn{ "24h_volume": {
Slug: "24h_volume", Slug: "24h_volume",
Label: "24H [v]olume", Label: "24H [v]olume",
ShortLabel: "24[v]",
PlainLabel: "24H volume", PlainLabel: "24H volume",
}, },
"1h_change": &HeaderColumn{ "1h_change": {
Slug: "1h_change", Slug: "1h_change",
Label: "[1]H%", Label: "[1]H%",
PlainLabel: "1H%", PlainLabel: "1H%",
}, },
"24h_change": &HeaderColumn{ "24h_change": {
Slug: "24h_change", Slug: "24h_change",
Label: "[2]4H%", Label: "[2]4H%",
PlainLabel: "24H%", PlainLabel: "24H%",
}, },
"7d_change": &HeaderColumn{ "7d_change": {
Slug: "7d_change", Slug: "7d_change",
Label: "[7]D%", Label: "[7]D%",
PlainLabel: "7D%", PlainLabel: "7D%",
}, },
"30d_change": &HeaderColumn{ "30d_change": {
Slug: "30d_change", Slug: "30d_change",
Label: "[3]0D%", Label: "[3]0D%",
PlainLabel: "30D%", PlainLabel: "30D%",
}, },
"1y_change": &HeaderColumn{ "1y_change": {
Slug: "1y_change", Slug: "1y_change",
Label: "1[y]%", Label: "1[y]%",
PlainLabel: "1Y%", PlainLabel: "1Y%",
}, },
"total_supply": &HeaderColumn{ "total_supply": {
Slug: "total_supply", Slug: "total_supply",
Label: "[t]otal supply", Label: "[t]otal supply",
ShortLabel: "[t]ot",
PlainLabel: "total supply", PlainLabel: "total supply",
}, },
"available_supply": &HeaderColumn{ "available_supply": {
Slug: "available_supply", Slug: "available_supply",
Label: "[a]vailable supply", Label: "[a]vailable supply",
ShortLabel: "[a]vl",
PlainLabel: "available supply", PlainLabel: "available supply",
}, },
"percent_holdings": &HeaderColumn{ "percent_holdings": {
Slug: "percent_holdings", Slug: "percent_holdings",
Label: "[%]holdings", Label: "[%]holdings",
PlainLabel: "%holdings", PlainLabel: "%holdings",
}, },
"last_updated": &HeaderColumn{ "last_updated": {
Slug: "last_updated", Slug: "last_updated",
Label: "last [u]pdated", Label: "last [u]pdated",
PlainLabel: "last updated", 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 // TableHeaderView is structure for table header view
@ -128,8 +162,7 @@ type TableHeaderView = ui.View
// NewTableHeaderView returns a new table header view // NewTableHeaderView returns a new table header view
func NewTableHeaderView() *TableHeaderView { func NewTableHeaderView() *TableHeaderView {
var view *TableHeaderView = ui.NewView("table_header") return ui.NewView("table_header")
return view
} }
// GetActiveTableHeaders returns the list of active table headers // GetActiveTableHeaders returns the list of active table headers
@ -146,6 +179,22 @@ func (ct *Cointop) GetActiveTableHeaders() []string {
return cols 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 // UpdateTableHeader renders the table header
func (ct *Cointop) UpdateTableHeader() error { func (ct *Cointop) UpdateTableHeader() error {
log.Debug("UpdateTableHeader()") log.Debug("UpdateTableHeader()")
@ -155,6 +204,7 @@ func (ct *Cointop) UpdateTableHeader() error {
cols := ct.GetActiveTableHeaders() cols := ct.GetActiveTableHeaders()
var headers []string var headers []string
var columnLookup []string // list of column-names or ""
for i, col := range cols { for i, col := range cols {
hc, ok := HeaderColumns[col] hc, ok := HeaderColumns[col]
if !ok { if !ok {
@ -167,28 +217,23 @@ func (ct *Cointop) UpdateTableHeader() error {
arrow := " " arrow := " "
colorfn := baseColor colorfn := baseColor
if !noSort { if !noSort {
if ct.State.sortBy == col { currentSortCons := ct.State.viewSorts[ct.State.selectedView]
if currentSortCons.sortBy == col {
colorfn = ct.colorscheme.TableHeaderColumnActiveSprintf() colorfn = ct.colorscheme.TableHeaderColumnActiveSprintf()
if ct.State.sortDesc { arrow = ArrowUp
if currentSortCons.sortDesc {
arrow = ArrowDown arrow = ArrowDown
} else {
arrow = ArrowUp
} }
} }
} }
label := hc.Label label := ct.GetLabel(hc)
if noSort { if noSort {
label = hc.PlainLabel label = hc.PlainLabel
} }
leftAlign := ct.GetTableColumnAlignLeft(col) leftAlign := ct.GetTableColumnAlignLeft(col)
switch col { switch col {
case "price", "balance": case "price", "balance", "pnl", "cost":
spacing := "" label = fmt.Sprintf("%s%s", ct.CurrencySymbol(), label)
// Add an extra space because "satoshi" UTF-8 chracter overlaps text on right
if ct.State.currencyConversion == "SATS" {
spacing = " "
}
label = fmt.Sprintf("%s%s%s", ct.CurrencySymbol(), spacing, label)
} }
if leftAlign { if leftAlign {
label = label + arrow label = label + arrow
@ -203,15 +248,27 @@ func (ct *Cointop) UpdateTableHeader() error {
if leftAlign { if leftAlign {
padfn = pad.Right padfn = pad.Right
} }
padded := padfn(label, width+(1-padLeft), " ")
colStr := fmt.Sprintf( colStr := fmt.Sprintf(
"%s%s%s", "%s%s%s",
strings.Repeat(" ", padLeft), strings.Repeat(" ", padLeft),
colorfn(padfn(label, width+(1-padLeft), " ")), colorfn(padded),
strings.Repeat(" ", 1), strings.Repeat(" ", 1),
) )
headers = append(headers, colStr) 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 { ct.UpdateUI(func() error {
return ct.Views.TableHeader.Update(strings.Join(headers, "")) return ct.Views.TableHeader.Update(strings.Join(headers, ""))
}) })
@ -219,6 +276,21 @@ func (ct *Cointop) UpdateTableHeader() error {
return nil 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 // SetTableColumnAlignLeft sets the column alignment direction for header
func (ct *Cointop) SetTableColumnAlignLeft(header string, alignLeft bool) { func (ct *Cointop) SetTableColumnAlignLeft(header string, alignLeft bool) {
ct.State.tableColumnAlignLeft.Store(header, alignLeft) ct.State.tableColumnAlignLeft.Store(header, alignLeft)
@ -241,7 +313,10 @@ func (ct *Cointop) SetTableColumnWidth(header string, width int) {
prev = prevIfc.(int) prev = prevIfc.(int)
} else { } else {
hc := HeaderColumns[header] hc := HeaderColumns[header]
prev = utf8.RuneCountInString(hc.Label) + 1 if hc == nil {
log.Warnf("SetTableColumnWidth(%s) not found", header)
}
prev = utf8.RuneCountInString(ct.GetLabel(hc)) + 1
switch header { switch header {
case "price", "balance": case "price", "balance":
prev++ prev++

@ -1,7 +1,7 @@
package cointop package cointop
import ( import (
"github.com/miguelmota/gocui" "github.com/cointop-sh/cointop/pkg/gocui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

@ -8,8 +8,9 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/miguelmota/cointop/pkg/open" "github.com/cointop-sh/cointop/pkg/open"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/blake2b"
) )
// OpenLink opens the url in a browser // OpenLink opens the url in a browser
@ -19,6 +20,13 @@ func (ct *Cointop) OpenLink() error {
return nil 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 // GetBytes returns the interface in bytes form
func GetBytes(key interface{}) ([]byte, error) { func GetBytes(key interface{}) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
@ -46,7 +54,7 @@ func TruncateString(value string, maxLen int) string {
} }
// ClearSyncMap clears a sync.Map // 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.Range(func(key interface{}, value interface{}) bool {
syncMap.Delete(key) syncMap.Delete(key)
return true return true
@ -66,3 +74,12 @@ func normalizeFloatString(input string, allowNegative bool) string {
return "" return ""
} }
func getStructHash(x interface{}) (string, error) {
b, err := GetBytes(x)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", blake2b.Sum256(b)), nil
}

@ -0,0 +1,110 @@
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,7 +3,7 @@ title: "Intro"
date: 2020-01-01T00:00:00-00:00 date: 2020-01-01T00:00:00-00:00
draft: false draft: false
--- ---
[`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. [`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.
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)). 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)).
@ -11,24 +11,19 @@ The interface is inspired by [`htop`](https://en.wikipedia.org/wiki/Htop) and sh
## Features ## Features
- Quick sort shortcuts - **Shortcut keys**: Vim-inspired shortcut keys, custom key bindings configuration
- Custom key bindings configuration - **Colorschemes**: Custom colorscheme configuration, 256-color and 24-bit support
- Vim inspired shortcut keys - **Favorites**: Save and view favorite coins
- Fast pagination - **Portfolio**: Portfolio tracking of holdings, view profit & loss
- Charts for coins and global market graphs - **Charts**: Charts for coin price history and global market graphs
- Quick chart date range change - **Search**: Fuzzy searching for finding coins
- Fuzzy searching for finding coins - **Conversion**: Currency conversion
- Currency conversion - **Price Alerts**: Price alerts with desktop notifications
- Save and view favorite coins - **Multiple APIs**: Supports multiple coin data APIs; CoinGecko and CoinMarketCap
- Portfolio tracking of holdings - **Mouse**: Mouse support
- 256-color support - **Offline**: Offline cache
- Custom colorschemes - **Fast**: Fast sort shortcuts, pagination, chart date range change, auto-refresh
- Help menu - **Lightweight**: It's very lightweight; can be left running indefinitely
- 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 ## In action

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

@ -16,6 +16,8 @@ $ cd ~/.config/cointop
$ git clone git@github.com:cointop-sh/colors.git $ 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: Then edit your config `~/.config/cointop/config.toml` and set the colorscheme you want to use:
```toml ```toml

@ -46,7 +46,7 @@ refresh_rate = 60
[shortcuts] [shortcuts]
"$" = "last_page" "$" = "last_page"
0 = "first_page" 0 = "move_to_first_page_first_row"
1 = "sort_column_1h_change" 1 = "sort_column_1h_change"
2 = "sort_column_24h_change" 2 = "sort_column_24h_change"
7 = "sort_column_7d_change" 7 = "sort_column_7d_change"
@ -133,6 +133,7 @@ Action|Description
----|------| ----|------|
`first_chart_range`|Select first chart date range (e.g. 24H) `first_chart_range`|Select first chart date range (e.g. 24H)
`first_page`|Go to first page `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 `enlarge_chart`|Increase chart height
`help`|Show help `help`|Show help
`hide_currency_convert_menu`|Hide currency convert menu `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. For contributions please create a new branch and submit a pull request for review.
Huge thanks to all the [contributors](https://github.com/miguelmota/cointop/graphs/contributors) that have made cointop better. Huge thanks to all the [contributors](https://github.com/cointop-sh/cointop/graphs/contributors) that have made cointop better.
## Documentation ## 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/miguelmota/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/cointop-sh/cointop/tree/master/docs/content)
Run the documentation locally with: Run the documentation locally with:

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

@ -15,7 +15,8 @@ draft: false
## What coins does this support? ## What coins does this support?
This supports any coin supported by the API being used to fetch coin information. 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.
## How do I set the API to use? ## How do I set the API to use?
@ -41,13 +42,18 @@ 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. 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.
## How do I make the background color transparent? 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.
Change the background color options in the colorscheme file to `default` to use the system default color, eg. `base_bg = "default"` You can also define values in the colorscheme file, and reference them from throughout the file, using the following syntax:
## Why don't colorschemes support RGB or hex colors? ```toml
define_base03 = "#002b36"
menu_header_fg = "$base03"
```
Some of the cointop underlying rendering libraries don't support true colors. See [issue](https://github.com/nsf/termbox/issues/37). ## 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? ## Where is the config file located?
@ -89,7 +95,7 @@ draft: false
## I'm no longer seeing any data! ## 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/miguelmota/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/cointop-sh/cointop/issues/new).
## How do I get a CoinMarketCap Pro API key? ## How do I get a CoinMarketCap Pro API key?
@ -118,7 +124,7 @@ draft: false
## I can I add my own API to cointop? ## I can I add my own API to cointop?
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. 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.
## I installed cointop without errors but the command is not found. ## I installed cointop without errors but the command is not found.
@ -132,6 +138,9 @@ draft: false
## How do I search? ## 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>. 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? ## How do I exit search?
@ -183,6 +192,29 @@ 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. 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)? ## 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. 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.
@ -199,11 +231,11 @@ draft: false
LANG=en_US.utf8 TERM=xterm-256color cointop 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/miguelmota/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/cointop-sh/cointop/wiki/Windows-Command-Prompt-and-WSL-Font-Support) for font support instructions.
## How do I install Go on Ubuntu? ## How do I install Go on Ubuntu?
There's instructions on installing Go on Ubuntu in the [wiki](https://github.com/miguelmota/cointop/wiki/Installing-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).
## I'm getting errors installing the snap in Windows WSL. ## I'm getting errors installing the snap in Windows WSL.
@ -228,8 +260,8 @@ draft: false
Here's how to build the executable and run it: Here's how to build the executable and run it:
```powershell ```powershell
> md C:\Users\Josem\go\src\github.com\miguelmota -ea 0 > md C:\Users\Josem\go\src\github.com\cointop-sh -ea 0
> git clone https://github.com/miguelmota/cointop.git > git clone https://github.com/cointop-sh/cointop.git
> go build -o cointop.exe main.go > go build -o cointop.exe main.go
> cointop.exe > cointop.exe
``` ```
@ -357,6 +389,12 @@ draft: false
Supported columns relating to price change are `1h_change`, `24h_change`, `7d_change`, `30d_change`, `1y_change` 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? ## 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. Run cointop with the `--config` flag, eg `cointop --config="/path/to/config.toml"`, to use the specified file as the config.
@ -373,7 +411,7 @@ draft: false
## I can only view the first page, why isn't the pagination is working? ## 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/miguelmota/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/cointop-sh/cointop/issues/new).
## How can run cointop with just the table? ## How can run cointop with just the table?
@ -479,10 +517,39 @@ draft: false
cointop server -k ~/.ssh/id_rsa [...] 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`? ## 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: 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 ```bash
GO111MODULE=on go get github.com/miguelmota/cointop 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
``` ```
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. There are multiple ways you can install cointop depending on the platform you're on.
## From source (always latest and recommeded) ## From source (always latest and recommended)
Make sure to have [go](https://golang.org/) (1.12+) installed, then do: Make sure to have [go](https://golang.org/) (1.17+) installed, then do:
```bash ```bash
go get github.com/miguelmota/cointop go install github.com/cointop-sh/cointop@latest
``` ```
The cointop executable will be under `~/go/bin/cointop` so make sure `$GOPATH/bin` is added to the `$PATH` variable if not already. The cointop executable will be under your GOPATH so make sure `$GOPATH/bin` is added to the `$PATH` variable if not already.
Now you can run cointop: Now you can run cointop:
@ -25,14 +25,14 @@ cointop
## Binary (all platforms) ## Binary (all platforms)
You can download the binary from the [releases](https://github.com/miguelmota/cointop/releases) page. You can download the binary from the [releases](https://github.com/cointop-sh/cointop/releases) page.
```bash ```bash
curl -o- https://raw.githubusercontent.com/miguelmota/cointop/master/install.sh | bash curl -o- https://raw.githubusercontent.com/cointop-sh/cointop/master/install.sh | bash
``` ```
```bash ```bash
wget -qO- https://raw.githubusercontent.com/miguelmota/cointop/master/install.sh | bash wget -qO- https://raw.githubusercontent.com/cointop-sh/cointop/master/install.sh | bash
``` ```
## Homebrew (macOS) ## 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. cointop is available as a [copr](https://copr.fedorainfracloud.org/coprs/miguelmota/cointop/) package.
First, enable the respository First, enable the repository
```bash ```bash
sudo dnf copr enable miguelmota/cointop -y sudo dnf copr enable miguelmota/cointop -y
@ -143,11 +143,11 @@ nix-env -iA nixpkgs.cointop
## AppImage (Linux) ## AppImage (Linux)
You can download the AppImage from the [releases](https://github.com/miguelmota/cointop/releases) page. You can download the AppImage from the [releases](https://github.com/cointop-sh/cointop/releases) page.
```bash ```bash
VERSION=$(curl --silent "https://api.github.com/repos/miguelmota/cointop/releases/latest" | grep -Po --color=never '"tag_name": ".\K.*?(?=")') VERSION=$(curl --silent "https://api.github.com/repos/cointop-sh/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" URL="https://github.com/cointop-sh/cointop/releases/download/v$VERSION/cointop-v$VERSION.glibc2.32-x86_64.AppImage"
wget $URL 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: Install [Go](https://golang.org/doc/install) and [git](https://git-scm.com/download/win), then:
```powershell ```powershell
go get -u github.com/miguelmota/cointop go get -u github.com/cointop-sh/cointop
``` ```
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. 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.
## Docker ## Docker
@ -197,4 +197,4 @@ docker run -v ~/.cache/cointop:/root/.config/cointop -it cointop/cointop
## Binaries ## Binaries
You can find pre-built binaries on the [releases](https://github.com/miguelmota/cointop/releases) page. You can find pre-built binaries on the [releases](https://github.com/cointop-sh/cointop/releases) page.

@ -10,7 +10,7 @@ draft: false
To update make sure to use the `-u` flag if installed via Go. To update make sure to use the `-u` flag if installed via Go.
```bash ```bash
go get -u github.com/miguelmota/cointop go get -u github.com/cointop-sh/cointop
``` ```
## Homebrew (macOS) ## Homebrew (macOS)

@ -0,0 +1,44 @@
{
"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
}

@ -0,0 +1,38 @@
{
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,4 +1,4 @@
module github.com/miguelmota/cointop module github.com/cointop-sh/cointop
go 1.17 go 1.17
@ -6,39 +6,41 @@ require (
github.com/BurntSushi/toml v0.4.1 github.com/BurntSushi/toml v0.4.1
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/antonmedv/expr v1.9.0 github.com/antonmedv/expr v1.9.0
github.com/creack/pty v1.1.15 github.com/creack/pty v1.1.17
github.com/fatih/color v1.12.0 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/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1
github.com/gliderlabs/ssh v0.3.3 github.com/gliderlabs/ssh v0.3.3
github.com/maruel/panicparse v1.6.1 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/mattn/go-runewidth v0.0.13
github.com/miguelmota/go-coinmarketcap v0.1.8 github.com/miguelmota/go-coinmarketcap v0.1.8
github.com/miguelmota/gocui v0.4.2
github.com/miguelmota/termbox-go v0.0.0-20191229070316-58d4fcbce2a7
github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/go-wordwrap v1.0.1
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
) )
require ( require (
github.com/anaskhan96/soup v1.0.1 // indirect github.com/anaskhan96/soup v1.2.4 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // 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/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/gopherjs/gopherjs v0.0.0-20211111143520-d0d5ecc1a356 // indirect
github.com/gopherjs/gopherwasm v1.1.0 // indirect github.com/gopherjs/gopherwasm v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // 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/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
) )

104
go.sum

@ -37,15 +37,19 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 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= 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= 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/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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/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/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/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= 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/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/anaskhan96/soup v1.0.1 h1:3p9zOr7o2weHqDakRA1uR0SZNr6VhH5qPkm6p3gvS6o= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/anaskhan96/soup v1.0.1/go.mod h1:pT5vs4HXDwA5y4KQCsKvnkpQd3D+joP7IqpiGskfWW0= 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/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 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/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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -67,8 +71,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 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/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -81,25 +85,35 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/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/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.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 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/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/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 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 h1:Xh9mvwEmhbdXlRSsgn+N0zj/NqnKvpeqL08oKDHln2s=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8= github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 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 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= 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 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-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-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/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= 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/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/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/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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 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-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-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -129,6 +143,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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 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/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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -141,8 +157,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/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.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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/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/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 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.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -163,8 +179,9 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 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/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/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 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= 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/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@ -192,6 +209,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/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 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/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.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/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@ -204,31 +225,27 @@ 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/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.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.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/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/maruel/panicparse v1.6.1 h1:803MjBzGcUgE1vYgg3UMNq3G1oyYeKkMu3t6hBS97x0=
github.com/maruel/panicparse v1.6.1/go.mod h1:uoxI4w9gJL6XahaYPMq/z9uadrdr1SyHuQwV2q80Mm0=
github.com/maruel/panicparse/v2 v2.1.1/go.mod h1:AeTWdCE4lcq8OKsLb6cHSj1RWHVSnV9HBCk7sKLF4Jg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 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/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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.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.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 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 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 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 h1:rZhB7xs1j7qxxd1zftjADhAv6ECJQVhBom1dh3zURKY=
github.com/miguelmota/go-coinmarketcap v0.1.8/go.mod h1:hBjej1IiB5+pfj+0cZhnxRkAc2bgky8qWLhCJTQ3zjw= github.com/miguelmota/go-coinmarketcap v0.1.8/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/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 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-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-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@ -242,6 +259,11 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/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/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 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/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 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@ -250,7 +272,9 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 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/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.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
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.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/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 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -267,19 +291,26 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 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/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/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/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 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 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/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/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
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/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/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 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= 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/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 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/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/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@ -292,8 +323,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 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 h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -320,8 +349,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/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-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-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/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-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-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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -334,6 +364,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 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-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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -393,8 +424,10 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/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-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-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 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-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-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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -448,7 +481,7 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/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-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-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/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-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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -462,12 +495,19 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/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-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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 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-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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -475,6 +515,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -493,6 +534,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 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-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-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-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-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-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -514,6 +556,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/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-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-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-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-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-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -531,6 +574,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/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.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -639,11 +683,13 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/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.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.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-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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=

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

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

@ -1,8 +1,8 @@
package api package api
import ( import (
cg "github.com/miguelmota/cointop/pkg/api/impl/coingecko" cg "github.com/cointop-sh/cointop/pkg/api/impl/coingecko"
cmc "github.com/miguelmota/cointop/pkg/api/impl/coinmarketcap" cmc "github.com/cointop-sh/cointop/pkg/api/impl/coinmarketcap"
) )
// NewCMC new CoinMarketCap API // NewCMC new CoinMarketCap API

@ -9,10 +9,11 @@ import (
"sync" "sync"
"time" "time"
apitypes "github.com/miguelmota/cointop/pkg/api/types" apitypes "github.com/cointop-sh/cointop/pkg/api/types"
util "github.com/miguelmota/cointop/pkg/api/util" "github.com/cointop-sh/cointop/pkg/api/util"
gecko "github.com/miguelmota/cointop/pkg/api/vendors/coingecko/v3" gecko "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3"
geckoTypes "github.com/miguelmota/cointop/pkg/api/vendors/coingecko/v3/types" "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types"
geckoTypes "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types"
) )
// ErrPingFailed is the error for when pinging the API fails // ErrPingFailed is the error for when pinging the API fails
@ -33,14 +34,15 @@ type Service struct {
maxResultsPerPage uint maxResultsPerPage uint
maxPages uint maxPages uint
cacheMap sync.Map cacheMap sync.Map
cachedRates *types.ExchangeRatesItem
} }
// NewCoinGecko new service // NewCoinGecko new service
func NewCoinGecko(config *Config) *Service { func NewCoinGecko(config *Config) *Service {
var maxResultsPerPage uint = 250 // absolute max maxResultsPerPage := 250 // absolute max
var maxResults uint = 0 maxResults := uint(0)
var maxPages uint = 10 maxPages := uint(10)
var perPage uint = 100 perPage := uint(100)
if config.PerPage > 0 { if config.PerPage > 0 {
perPage = config.PerPage perPage = config.PerPage
} }
@ -146,6 +148,45 @@ func (s *Service) GetCoinGraphData(convert, symbol, name string, start, end int6
return ret, nil 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 // GetGlobalMarketGraphData gets global market graph data
func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int64) (apitypes.MarketGraph, error) { func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int64) (apitypes.MarketGraph, error) {
days := strconv.Itoa(util.CalcDays(start, end)) days := strconv.Itoa(util.CalcDays(start, end))
@ -154,7 +195,14 @@ func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int6
if convertTo == "" { if convertTo == "" {
convertTo = "usd" convertTo = "usd"
} }
graphData, err := s.client.GlobalCharts(convertTo, days) 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)
if err != nil { if err != nil {
return ret, err return ret, err
} }
@ -165,7 +213,7 @@ func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int6
for _, item := range *graphData.Stats { for _, item := range *graphData.Stats {
marketCapUSD = append(marketCapUSD, []float64{ marketCapUSD = append(marketCapUSD, []float64{
float64(item[0]), float64(item[0]),
float64(item[1]), float64(item[1]) * rate,
}) })
} }
} }
@ -219,15 +267,13 @@ func (s *Service) Price(name string, convert string) (float64, error) {
return 0, ErrNotFound return 0, ErrNotFound
} }
// CoinLink returns the URL link for the coin func (s *Service) CoinLink(slug string) string {
func (s *Service) CoinLink(name string) string { // slug is API ID of coin
ID := s.coinNameToID(name) return fmt.Sprintf("https://www.coingecko.com/en/coins/%s", slug)
return fmt.Sprintf("https://www.coingecko.com/en/coins/%s", ID)
} }
// SupportedCurrencies returns a list of supported currencies // SupportedCurrencies returns a list of supported currencies
func (s *Service) SupportedCurrencies() []string { func (s *Service) SupportedCurrencies() []string {
// keep these in alphabetical order // keep these in alphabetical order
return []string{ return []string{
"AED", "AED",
@ -293,7 +339,7 @@ func (s *Service) cacheCoinsIDList() error {
if list == nil { if list == nil {
return nil return nil
} }
firstWords := [][]string{} var firstWords [][]string
for _, item := range *list { for _, item := range *list {
keys := []string{ keys := []string{
strings.ToLower(item.Name), strings.ToLower(item.Name),
@ -414,6 +460,7 @@ func (s *Service) getPaginatedCoinData(convert string, offset int, names []strin
PercentChange1Y: util.FormatPercentChange(percentChange1Y), PercentChange1Y: util.FormatPercentChange(percentChange1Y),
Volume24H: util.FormatVolume(item.TotalVolume), Volume24H: util.FormatVolume(item.TotalVolume),
LastUpdated: util.FormatLastUpdated(item.LastUpdated), LastUpdated: util.FormatLastUpdated(item.LastUpdated),
Slug: util.FormatSlug(item.ID),
}) })
} }
} }

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

@ -17,6 +17,8 @@ type Coin struct {
PercentChange30D float64 `json:"percentChange30D"` PercentChange30D float64 `json:"percentChange30D"`
PercentChange1Y float64 `json:"percentChange1Y"` PercentChange1Y float64 `json:"percentChange1Y"`
LastUpdated string `json:"lastUpdated"` LastUpdated string `json:"lastUpdated"`
// Slug uses to access the coin's info web page
Slug string `json:"slug"`
} }
// GlobalMarketData struct // GlobalMarketData struct

@ -29,6 +29,10 @@ func FormatName(name string) string {
return name return name
} }
func FormatSlug(slug string) string {
return slug
}
// FormatRank formats the rank value // FormatRank formats the rank value
func FormatRank(rank interface{}) int { func FormatRank(rank interface{}) int {
switch v := rank.(type) { switch v := rank.(type) {

@ -9,8 +9,11 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/miguelmota/cointop/pkg/api/vendors/coingecko/format" "os"
"github.com/miguelmota/cointop/pkg/api/vendors/coingecko/v3/types"
"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"
) )
var baseURL = "https://api.coingecko.com/api/v3" var baseURL = "https://api.coingecko.com/api/v3"
@ -31,6 +34,11 @@ func NewClient(httpClient *http.Client) *Client {
// helper // helper
// doReq HTTP client // doReq HTTP client
func doReq(req *http.Request, client *http.Client) ([]byte, error) { 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) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -41,6 +49,10 @@ func doReq(req *http.Request, client *http.Client) ([]byte, error) {
return nil, err return nil, err
} }
if 200 != resp.StatusCode { if 200 != resp.StatusCode {
if debugHttp {
log.Warnf("doReq Got Status '%s' from %s %s", resp.Status, req.Method, req.URL)
log.Debugf("doReq Got Body: %s", body)
}
return nil, fmt.Errorf("%s", body) return nil, fmt.Errorf("%s", body)
} }
return body, nil return body, nil
@ -198,7 +210,7 @@ func (c *Client) CoinsID(id string, localization bool, tickers bool, marketData
return nil, fmt.Errorf("id is required") return nil, fmt.Errorf("id is required")
} }
params := url.Values{} params := url.Values{}
params.Add("localization", format.Bool2String(sparkline)) params.Add("localization", format.Bool2String(localization))
params.Add("tickers", format.Bool2String(tickers)) params.Add("tickers", format.Bool2String(tickers))
params.Add("market_data", format.Bool2String(marketData)) params.Add("market_data", format.Bool2String(marketData))
params.Add("community_data", format.Bool2String(communityData)) params.Add("community_data", format.Bool2String(communityData))

@ -1,7 +1,7 @@
package chartplot package chartplot
import ( import (
"github.com/miguelmota/cointop/pkg/termui" "github.com/cointop-sh/cointop/pkg/termui"
) )
// ChartPlot ... // ChartPlot ...
@ -55,6 +55,11 @@ func (c *ChartPlot) SetData(data []float64) {
c.t.Data = data c.t.Data = data
} }
// SetDataLabels ...
func (c *ChartPlot) SetDataLabels(labels []string) {
c.t.DataLabels = labels
}
// GetChartDataSize ... // GetChartDataSize ...
func (c *ChartPlot) GetChartDataSize(width int) int { func (c *ChartPlot) GetChartDataSize(width int) int {
axisYWidth := 30 axisYWidth := 30

@ -1,39 +0,0 @@
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()
)

@ -19,7 +19,7 @@ func (p *patcher) Exit(node *ast.Node) {
} }
} }
// EvaluateExpression evaulates a simple math expression string to a float64 // EvaluateExpressionToFloat64 evaulates a simple math expression string to a float64
func EvaluateExpressionToFloat64(input string, env interface{}) (float64, error) { func EvaluateExpressionToFloat64(input string, env interface{}) (float64, error) {
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
if input == "" { if input == "" {
@ -43,3 +43,23 @@ func EvaluateExpressionToFloat64(input string, env interface{}) (float64, error)
} }
return f64, nil 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,7 +16,7 @@ import (
) )
// DefaultCacheDir ... // DefaultCacheDir ...
var DefaultCacheDir = "/tmp" var DefaultCacheDir = ":PREFERRED_CACHE_HOME:/cointop"
// FileCache ... // FileCache ...
type FileCache struct { type FileCache struct {

@ -4,22 +4,26 @@
package gocui package gocui
import "errors" import (
"errors"
"github.com/gdamore/tcell/v2"
)
const maxInt = int(^uint(0) >> 1) const maxInt = int(^uint(0) >> 1)
// Editor interface must be satisfied by gocui editors. // Editor interface must be satisfied by gocui editors.
type Editor interface { type Editor interface {
Edit(v *View, key Key, ch rune, mod Modifier) Edit(v *View, key tcell.Key, ch rune, mod tcell.ModMask)
} }
// The EditorFunc type is an adapter to allow the use of ordinary functions as // The EditorFunc type is an adapter to allow the use of ordinary functions as
// Editors. If f is a function with the appropriate signature, EditorFunc(f) // Editors. If f is a function with the appropriate signature, EditorFunc(f)
// is an Editor object that calls f. // is an Editor object that calls f.
type EditorFunc func(v *View, key Key, ch rune, mod Modifier) type EditorFunc func(v *View, key tcell.Key, ch rune, mod tcell.ModMask)
// Edit calls f(v, key, ch, mod) // Edit calls f(v, key, ch, mod)
func (f EditorFunc) Edit(v *View, key Key, ch rune, mod Modifier) { func (f EditorFunc) Edit(v *View, key tcell.Key, ch rune, mod tcell.ModMask) {
f(v, key, ch, mod) f(v, key, ch, mod)
} }
@ -27,27 +31,27 @@ func (f EditorFunc) Edit(v *View, key Key, ch rune, mod Modifier) {
var DefaultEditor Editor = EditorFunc(simpleEditor) var DefaultEditor Editor = EditorFunc(simpleEditor)
// simpleEditor is used as the default gocui editor. // simpleEditor is used as the default gocui editor.
func simpleEditor(v *View, key Key, ch rune, mod Modifier) { func simpleEditor(v *View, key tcell.Key, ch rune, mod tcell.ModMask) {
switch { switch {
case ch != 0 && mod == 0: case key == tcell.KeyRune && ch != 0 && (mod == tcell.ModShift || mod == tcell.ModNone):
v.EditWrite(ch) v.EditWrite(ch)
case key == KeySpace: case key == ' ':
v.EditWrite(' ') v.EditWrite(' ')
case key == KeyBackspace || key == KeyBackspace2: case key == tcell.KeyBackspace || key == tcell.KeyBackspace2:
v.EditDelete(true) v.EditDelete(true)
case key == KeyDelete: case key == tcell.KeyDelete:
v.EditDelete(false) v.EditDelete(false)
case key == KeyInsert: case key == tcell.KeyInsert:
v.Overwrite = !v.Overwrite v.Overwrite = !v.Overwrite
case key == KeyEnter: case key == tcell.KeyEnter:
v.EditNewLine() v.EditNewLine()
case key == KeyArrowDown: case key == tcell.KeyDown:
v.MoveCursor(0, 1, false) v.MoveCursor(0, 1, false)
case key == KeyArrowUp: case key == tcell.KeyUp:
v.MoveCursor(0, -1, false) v.MoveCursor(0, -1, false)
case key == KeyArrowLeft: case key == tcell.KeyLeft:
v.MoveCursor(-1, 0, false) v.MoveCursor(-1, 0, false)
case key == KeyArrowRight: case key == tcell.KeyRight:
v.MoveCursor(1, 0, false) v.MoveCursor(1, 0, false)
} }
} }
@ -265,9 +269,8 @@ func (v *View) writeRune(x, y int, ch rune) error {
copy(v.lines[y][x+1:], v.lines[y][x:]) copy(v.lines[y][x+1:], v.lines[y][x:])
} }
v.lines[y][x] = cell{ v.lines[y][x] = cell{
fgColor: v.FgColor, style: v.Style,
bgColor: v.BgColor, chr: ch,
chr: ch,
} }
return nil return nil

@ -7,14 +7,16 @@ package gocui
import ( import (
"errors" "errors"
"strconv" "strconv"
"github.com/gdamore/tcell/v2"
) )
type escapeInterpreter struct { type escapeInterpreter struct {
state escapeState state escapeState
curch rune curch rune
csiParam []string csiParam []string
curFgColor, curBgColor Attribute curStyle tcell.Style
mode OutputMode // mode OutputMode
} }
type escapeState int type escapeState int
@ -54,12 +56,11 @@ func (ei *escapeInterpreter) runes() []rune {
// newEscapeInterpreter returns an escapeInterpreter that will be able to parse // newEscapeInterpreter returns an escapeInterpreter that will be able to parse
// terminal escape sequences. // terminal escape sequences.
func newEscapeInterpreter(mode OutputMode) *escapeInterpreter { func newEscapeInterpreter() *escapeInterpreter {
ei := &escapeInterpreter{ ei := &escapeInterpreter{
state: stateNone, state: stateNone,
curFgColor: ColorDefault, curStyle: tcell.StyleDefault,
curBgColor: ColorDefault, // mode: mode,
mode: mode,
} }
return ei return ei
} }
@ -67,8 +68,7 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
// reset sets the escapeInterpreter in initial state. // reset sets the escapeInterpreter in initial state.
func (ei *escapeInterpreter) reset() { func (ei *escapeInterpreter) reset() {
ei.state = stateNone ei.state = stateNone
ei.curFgColor = ColorDefault ei.curStyle = tcell.StyleDefault
ei.curBgColor = ColorDefault
ei.csiParam = nil ei.csiParam = nil
} }
@ -120,12 +120,13 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
return true, nil return true, nil
case ch == 'm': case ch == 'm':
var err error var err error
switch ei.mode { err = ei.parseEscapeParams()
case OutputNormal: // switch ei.mode {
err = ei.outputNormal() // case OutputNormal:
case Output256: // err = ei.outputNormal()
err = ei.output256() // case Output256:
} // err = ei.output256()
// }
if err != nil { if err != nil {
return false, errCSIParseError return false, errCSIParseError
} }
@ -140,90 +141,72 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
return false, nil return false, nil
} }
// outputNormal provides 8 different colors: // parseEscapeParams interprets an escape sequence as a style modifier
// black, red, green, yellow, blue, magenta, cyan, white // allows you to leverage the 256-colors terminal mode:
func (ei *escapeInterpreter) outputNormal() error { // 0x01 - 0x08: the 8 colors as in OutputNormal (black, red, green, yellow, blue, magenta, cyan, white)
for _, param := range ei.csiParam { // 0x09 - 0x10: Color* | AttrBold
p, err := strconv.Atoi(param) // 0x11 - 0xe8: 216 different colors
if err != nil { // 0xe9 - 0x1ff: 24 different shades of grey
// see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
// see https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
// 256-colors: ESC[ 38;5;${ID}m # foreground
// 256-colors: ESC[ 48;5;${ID}m # background
// 24-bit ESC[ 38;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB foreground color
// 24-bit ESC[ 48;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB background color
func (ei *escapeInterpreter) parseEscapeParams() error {
// TODO: cache escape -> Style
// convert params to int
params := make([]int, len(ei.csiParam))
for i, param := range ei.csiParam {
if p, err := strconv.Atoi(param); err == nil {
params[i] = p
} else {
return errCSIParseError return errCSIParseError
} }
}
// consume elements of params until done
pos := 0
for ok := true; ok; ok = pos < len(params) {
p := params[pos]
switch { switch {
case p >= 30 && p <= 37: case p >= 30 && p <= 37:
ei.curFgColor = Attribute(p - 30 + 1) ei.curStyle = ei.curStyle.Foreground(tcell.PaletteColor(p - 30))
case p == 39: case p == 39:
ei.curFgColor = ColorDefault ei.curStyle = ei.curStyle.Foreground(tcell.ColorDefault)
case p >= 40 && p <= 47: case p >= 40 && p <= 47:
ei.curBgColor = Attribute(p - 40 + 1) ei.curStyle = ei.curStyle.Background(tcell.PaletteColor(p - 40))
case p == 49: case p == 49:
ei.curBgColor = ColorDefault ei.curStyle = ei.curStyle.Background(tcell.ColorDefault)
case p == 1: case p == 1:
ei.curFgColor |= AttrBold ei.curStyle = ei.curStyle.Bold(true)
case p == 4: case p == 4:
ei.curFgColor |= AttrUnderline ei.curStyle = ei.curStyle.Underline(true)
case p == 7: case p == 7:
ei.curFgColor |= AttrReverse ei.curStyle = ei.curStyle.Reverse(true)
case p == 0: case p == 0:
ei.curFgColor = ColorDefault ei.curStyle = tcell.StyleDefault
ei.curBgColor = ColorDefault case p == 38 || p == 48: // 256-color or 24-bit
} // parse mode and additional params to generate a color
} mode := params[pos+1] // second param - 2 or 5
var x tcell.Color
return nil if mode == 5 { // 256 color
} x = tcell.PaletteColor(params[pos+2] + 1)
pos += 2 // two additional (5+index)
// output256 allows you to leverage the 256-colors terminal mode: } else if mode == 2 { // 24-bit
// 0x01 - 0x08: the 8 colors as in OutputNormal x = tcell.NewRGBColor(int32(params[pos+2]), int32(params[pos+3]), int32(params[pos+4]))
// 0x09 - 0x10: Color* | AttrBold pos += 4 // four additional (2+r/g/b)
// 0x11 - 0xe8: 216 different colors } else {
// 0xe9 - 0x1ff: 24 different shades of grey return errCSIParseError // invalid mode
func (ei *escapeInterpreter) output256() error {
if len(ei.csiParam) < 3 {
return ei.outputNormal()
}
mode, err := strconv.Atoi(ei.csiParam[1])
if err != nil {
return errCSIParseError
}
if mode != 5 {
return ei.outputNormal()
}
fgbg, err := strconv.Atoi(ei.csiParam[0])
if err != nil {
return errCSIParseError
}
color, err := strconv.Atoi(ei.csiParam[2])
if err != nil {
return errCSIParseError
}
switch fgbg {
case 38:
ei.curFgColor = Attribute(color + 1)
for _, param := range ei.csiParam[3:] {
p, err := strconv.Atoi(param)
if err != nil {
return errCSIParseError
} }
if p == 38 {
switch { ei.curStyle = ei.curStyle.Foreground(x)
case p == 1: } else {
ei.curFgColor |= AttrBold ei.curStyle = ei.curStyle.Background(x)
case p == 4:
ei.curFgColor |= AttrUnderline
case p == 7:
ei.curFgColor |= AttrReverse
} }
} }
case 48:
ei.curBgColor = Attribute(color + 1)
default:
return errCSIParseError
}
pos += 1 // move along 1 by default
}
return nil return nil
} }

@ -0,0 +1,64 @@
// 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
}

@ -7,7 +7,7 @@ package gocui
import ( import (
"errors" "errors"
"github.com/miguelmota/termbox-go" "github.com/gdamore/tcell/v2"
) )
var ( var (
@ -19,35 +19,36 @@ var (
) )
// OutputMode represents the terminal's output mode (8 or 256 colors). // OutputMode represents the terminal's output mode (8 or 256 colors).
type OutputMode termbox.OutputMode // type OutputMode termbox.OutputMode // TODO: die
const ( // const ( // TODO: die
// OutputNormal provides 8-colors terminal mode. // // OutputNormal provides 8-colors terminal mode.
OutputNormal = OutputMode(termbox.OutputNormal) // OutputNormal = OutputMode(termbox.OutputNormal)
// Output256 provides 256-colors terminal mode. // // Output256 provides 256-colors terminal mode.
Output256 = OutputMode(termbox.Output256) // Output256 = OutputMode(termbox.Output256)
) // )
// Gui represents the whole User Interface, including the views, layouts // Gui represents the whole User Interface, including the views, layouts
// and keybindings. // and eventBindings.
type Gui struct { type Gui struct {
tbEvents chan termbox.Event tbEvents chan tcell.Event
userEvents chan userEvent userEvents chan userEvent
views []*View views []*View
currentView *View currentView *View
managers []Manager managers []Manager
keybindings []*keybinding eventBindings []*eventBinding
maxX, maxY int maxX, maxY int
outputMode OutputMode // outputMode OutputMode // TODO: die
screen tcell.Screen
// BgColor and FgColor allow to configure the background and foreground // BgColor and FgColor allow to configure the background and foreground
// colors of the GUI. // colors of the GUI.
BgColor, FgColor Attribute Style tcell.Style
// SelBgColor and SelFgColor allow to configure the background and // SelBgColor and SelFgColor allow to configure the background and
// foreground colors of the frame of the current view. // foreground colors of the frame of the current view.
SelBgColor, SelFgColor Attribute SelStyle tcell.Style
// If Highlight is true, Sel{Bg,Fg}Colors will be used to draw the // If Highlight is true, Sel{Bg,Fg}Colors will be used to draw the
// frame of the current view. // frame of the current view.
@ -66,26 +67,36 @@ type Gui struct {
// If ASCII is true then use ASCII instead of unicode to draw the // If ASCII is true then use ASCII instead of unicode to draw the
// interface. Using ASCII is more portable. // interface. Using ASCII is more portable.
ASCII bool ASCII bool
// The current event while in the handlers.
CurrentEvent tcell.Event
} }
// NewGui returns a new Gui object with a given output mode. // NewGui returns a new Gui object with a given output mode.
func NewGui(mode OutputMode) (*Gui, error) { // func NewGui(mode OutputMode) (*Gui, error) {
if err := termbox.Init(); err != nil { func NewGui() (*Gui, error) {
return nil, err
}
g := &Gui{} g := &Gui{}
g.outputMode = mode // outMode = OutputNormal
termbox.SetOutputMode(termbox.OutputMode(mode)) if s, e := tcell.NewScreen(); e != nil {
return nil, e
} else if e = s.Init(); e != nil {
return nil, e
} else {
g.screen = s
}
// g.outputMode = mode
// termbox.SetScreen(g.Screen) // ugly global
// termbox.SetOutputMode(termbox.OutputMode(mode))
g.tbEvents = make(chan termbox.Event, 20) g.tbEvents = make(chan tcell.Event, 20)
g.userEvents = make(chan userEvent, 20) g.userEvents = make(chan userEvent, 20)
g.maxX, g.maxY = termbox.Size() g.maxX, g.maxY = g.screen.Size()
g.BgColor, g.FgColor = ColorDefault, ColorDefault g.Style = tcell.StyleDefault
g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault g.SelStyle = tcell.StyleDefault
return g, nil return g, nil
} }
@ -93,7 +104,7 @@ func NewGui(mode OutputMode) (*Gui, error) {
// Close finalizes the library. It should be called after a successful // Close finalizes the library. It should be called after a successful
// initialization and when gocui is not needed anymore. // initialization and when gocui is not needed anymore.
func (g *Gui) Close() { func (g *Gui) Close() {
termbox.Close() g.screen.Fini()
} }
// Size returns the terminal's size. // Size returns the terminal's size.
@ -101,26 +112,48 @@ func (g *Gui) Size() (x, y int) {
return g.maxX, g.maxY return g.maxX, g.maxY
} }
// temporary kludge for the pretty
func (g *Gui) prettyColor(x, y int, style tcell.Style) tcell.Style {
if true {
w, h := g.screen.Size()
// dark blue gradient background
red := int32(0)
grn := int32(0)
blu := int32(50 * float64(y) / float64(h))
style = style.Background(tcell.NewRGBColor(red, grn, blu))
// two-axis green-blue gradient
red = int32(200)
grn = int32(255 * float64(y) / float64(h))
blu = int32(255 * float64(x) / float64(w))
style = style.Foreground(tcell.NewRGBColor(red, grn, blu))
}
return style
}
// SetRune writes a rune at the given point, relative to the top-left // SetRune writes a rune at the given point, relative to the top-left
// corner of the terminal. It checks if the position is valid and applies // corner of the terminal. It checks if the position is valid and applies
// the given colors. // the given colors.
func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error { func (g *Gui) SetRune(x, y int, ch rune, style tcell.Style) error {
if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY { if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
return errors.New("invalid point") return errors.New("invalid point")
} }
termbox.SetCell(x, y, ch, termbox.Attribute(fgColor), termbox.Attribute(bgColor)) // temporary kludge for the pretty
// st = g.prettyColor(x, y, st)
g.screen.SetContent(x, y, ch, nil, style)
return nil return nil
} }
// Rune returns the rune contained in the cell at the given position. // Rune returns the rune contained in the cell at the given position.
// It checks if the position is valid. // It checks if the position is valid.
func (g *Gui) Rune(x, y int) (rune, error) { // func (g *Gui) Rune(x, y int) (rune, error) {
if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY { // if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
return ' ', errors.New("invalid point") // return ' ', errors.New("invalid point")
} // }
c := termbox.CellBuffer()[y*g.maxX+x] // c := termbox.CellBuffer()[y*g.maxX+x]
return c.Ch, nil // return c.Ch, nil
} // }
// SetView creates a new view with its top-left corner at (x0, y0) // SetView creates a new view with its top-left corner at (x0, y0)
// and the bottom-right one at (x1, y1). If a view with the same name // and the bottom-right one at (x1, y1). If a view with the same name
@ -144,9 +177,9 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int) (*View, error) {
return v, nil return v, nil
} }
v := newView(name, x0, y0, x1, y1, g.outputMode) v := newView(name, x0, y0, x1, y1, g)
v.BgColor, v.FgColor = g.BgColor, g.FgColor v.Style = g.Style
v.SelBgColor, v.SelFgColor = g.SelBgColor, g.SelFgColor v.SelStyle = g.SelStyle
g.views = append(g.views, v) g.views = append(g.views, v)
return v, ErrUnknownView return v, ErrUnknownView
} }
@ -243,60 +276,84 @@ func (g *Gui) CurrentView() *View {
return g.currentView return g.currentView
} }
// SetKeybinding creates a new keybinding. If viewname equals to "" // SetKeybinding creates a new eventBinding. If viewname equals to ""
// (empty string) then the keybinding will apply to all views. key must // (empty string) then the eventBinding will apply to all views. key must
// be a rune or a Key. // be a rune or a Key.
func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error { // TODO: split into key/mouse bindings?
var kb *keybinding func (g *Gui) SetKeybinding(viewname string, key tcell.Key, ch rune, mod tcell.ModMask, handler func(*Gui, *View) error) error {
// var kb *eventBinding
// k, ch, err := getKey(key)
// if err != nil {
// return err
// }
// TODO: get rid of this ugly mess
//switch key {
//case termbox.MouseLeft:
// kb = newMouseBinding(viewname, tcell.Button1, mod, handler)
//case termbox.MouseMiddle:
// kb = newMouseBinding(viewname, tcell.Button3, mod, handler)
//case termbox.MouseRight:
// kb = newMouseBinding(viewname, tcell.Button2, mod, handler)
//case termbox.MouseWheelUp:
// kb = newMouseBinding(viewname, tcell.WheelUp, mod, handler)
//case termbox.MouseWheelDown:
// kb = newMouseBinding(viewname, tcell.WheelDown, mod, handler)
//default:
// kb = newKeybinding(viewname, key, ch, mod, handler)
//}
kb := newKeybinding(viewname, key, ch, mod, handler)
g.eventBindings = append(g.eventBindings, kb)
return nil
}
k, ch, err := getKey(key) func (g *Gui) SetMousebinding(viewname string, btn tcell.ButtonMask, mod tcell.ModMask, handler func(*Gui, *View) error) error {
if err != nil { kb := newMouseBinding(viewname, btn, mod, handler)
return err g.eventBindings = append(g.eventBindings, kb)
}
kb = newKeybinding(viewname, k, ch, mod, handler)
g.keybindings = append(g.keybindings, kb)
return nil return nil
} }
// DeleteKeybinding deletes a keybinding. // DeleteKeybinding deletes a eventBinding.
func (g *Gui) DeleteKeybinding(viewname string, key interface{}, mod Modifier) error { func (g *Gui) DeleteKeybinding(viewname string, key tcell.Key, ch rune, mod tcell.ModMask) error {
k, ch, err := getKey(key) // k, ch, err := getKey(key)
if err != nil { // if err != nil {
return err // return err
} // }
for i, kb := range g.keybindings { for i, kb := range g.eventBindings {
if kb.viewName == viewname && kb.ch == ch && kb.key == k && kb.mod == mod { if kbe, ok := kb.ev.(*tcell.EventKey); ok {
g.keybindings = append(g.keybindings[:i], g.keybindings[i+1:]...) if kb.viewName == viewname && kbe.Rune() == ch && kbe.Key() == key && kbe.Modifiers() == mod {
return nil g.eventBindings = append(g.eventBindings[:i], g.eventBindings[i+1:]...)
return nil
}
} }
} }
return errors.New("keybinding not found") return errors.New("eventBinding not found")
} }
// DeleteKeybindings deletes all keybindings of view. // DeleteKeybindings deletes all eventBindings of view.
func (g *Gui) DeleteKeybindings(viewname string) { func (g *Gui) DeleteKeybindings(viewname string) {
var s []*keybinding var s []*eventBinding
for _, kb := range g.keybindings { for _, kb := range g.eventBindings {
if kb.viewName != viewname { if kb.viewName != viewname {
s = append(s, kb) s = append(s, kb)
} }
} }
g.keybindings = s g.eventBindings = s
} }
// getKey takes an empty interface with a key and returns the corresponding // getKey takes an empty interface with a key and returns the corresponding
// typed Key or rune. // typed Key or rune.
func getKey(key interface{}) (Key, rune, error) { // func getKey(key interface{}) (tcell.Key, rune, error) {
switch t := key.(type) { // switch t := key.(type) {
case Key: // case Key:
return t, 0, nil // return t, 0, nil
case rune: // case rune:
return 0, t, nil // return 0, t, nil
default: // default:
return 0, 0, errors.New("unknown type") // return 0, 0, errors.New("unknown type")
} // }
} // }
// userEvent represents an event triggered by the user. // userEvent represents an event triggered by the user.
type userEvent struct { type userEvent struct {
@ -330,18 +387,18 @@ func (f ManagerFunc) Layout(g *Gui) error {
} }
// SetManager sets the given GUI managers. It deletes all views and // SetManager sets the given GUI managers. It deletes all views and
// keybindings. // eventBindings.
func (g *Gui) SetManager(managers ...Manager) { func (g *Gui) SetManager(managers ...Manager) {
g.managers = managers g.managers = managers
g.currentView = nil g.currentView = nil
g.views = nil g.views = nil
g.keybindings = nil g.eventBindings = nil
go func() { g.tbEvents <- termbox.Event{Type: termbox.EventResize} }() go func() { g.tbEvents <- tcell.NewEventResize(0, 0) }()
} }
// SetManagerFunc sets the given manager function. It deletes all views and // SetManagerFunc sets the given manager function. It deletes all views and
// keybindings. // eventBindings.
func (g *Gui) SetManagerFunc(manager func(*Gui) error) { func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
g.SetManager(ManagerFunc(manager)) g.SetManager(ManagerFunc(manager))
} }
@ -351,18 +408,14 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
func (g *Gui) MainLoop() error { func (g *Gui) MainLoop() error {
go func() { go func() {
for { for {
g.tbEvents <- termbox.PollEvent() g.tbEvents <- g.screen.PollEvent()
} }
}() }()
inputMode := termbox.InputAlt
if g.InputEsc {
inputMode = termbox.InputEsc
}
if g.Mouse { if g.Mouse {
inputMode |= termbox.InputMouse g.screen.EnableMouse()
} }
termbox.SetInputMode(inputMode) // s.EnablePaste()
if err := g.flush(); err != nil { if err := g.flush(); err != nil {
return err return err
@ -370,7 +423,7 @@ func (g *Gui) MainLoop() error {
for { for {
select { select {
case ev := <-g.tbEvents: case ev := <-g.tbEvents:
if err := g.handleEvent(&ev); err != nil { if err := g.handleEvent(ev); err != nil {
return err return err
} }
case ev := <-g.userEvents: case ev := <-g.userEvents:
@ -392,7 +445,7 @@ func (g *Gui) consumeevents() error {
for { for {
select { select {
case ev := <-g.tbEvents: case ev := <-g.tbEvents:
if err := g.handleEvent(&ev); err != nil { if err := g.handleEvent(ev); err != nil {
return err return err
} }
case ev := <-g.userEvents: case ev := <-g.userEvents:
@ -407,12 +460,12 @@ func (g *Gui) consumeevents() error {
// handleEvent handles an event, based on its type (key-press, error, // handleEvent handles an event, based on its type (key-press, error,
// etc.) // etc.)
func (g *Gui) handleEvent(ev *termbox.Event) error { func (g *Gui) handleEvent(ev tcell.Event) error {
switch ev.Type { switch tev := ev.(type) {
case termbox.EventKey, termbox.EventMouse: case *tcell.EventMouse, *tcell.EventKey:
return g.onKey(ev) return g.onEvent(tev)
case termbox.EventError: case *tcell.EventError:
return ev.Err return errors.New(tev.Error())
default: default:
return nil return nil
} }
@ -420,9 +473,15 @@ func (g *Gui) handleEvent(ev *termbox.Event) error {
// flush updates the gui, re-drawing frames and buffers. // flush updates the gui, re-drawing frames and buffers.
func (g *Gui) flush() error { func (g *Gui) flush() error {
termbox.Clear(termbox.Attribute(g.FgColor), termbox.Attribute(g.BgColor)) // termbox.Clear(termbox.Attribute(g.FgColor), termbox.Attribute(g.BgColor))
w, h := g.screen.Size() // TODO: merge with maxX, maxY below
for row := 0; row < h; row++ {
for col := 0; col < w; col++ {
g.screen.SetContent(col, row, ' ', nil, g.Style)
}
}
maxX, maxY := termbox.Size() maxX, maxY := g.screen.Size()
// if GUI's size has changed, we need to redraw all views // if GUI's size has changed, we need to redraw all views
if maxX != g.maxX || maxY != g.maxY { if maxX != g.maxX || maxY != g.maxY {
for _, v := range g.views { for _, v := range g.views {
@ -438,23 +497,20 @@ func (g *Gui) flush() error {
} }
for _, v := range g.views { for _, v := range g.views {
if v.Frame { if v.Frame {
var fgColor, bgColor Attribute // var fgColor, bgColor Attribute
st := g.Style
if g.Highlight && v == g.currentView { if g.Highlight && v == g.currentView {
fgColor = g.SelFgColor st = g.SelStyle
bgColor = g.SelBgColor
} else {
fgColor = g.FgColor
bgColor = g.BgColor
} }
if err := g.drawFrameEdges(v, fgColor, bgColor); err != nil { if err := g.drawFrameEdges(v, st); err != nil {
return err return err
} }
if err := g.drawFrameCorners(v, fgColor, bgColor); err != nil { if err := g.drawFrameCorners(v, st); err != nil {
return err return err
} }
if v.Title != "" { if v.Title != "" {
if err := g.drawTitle(v, fgColor, bgColor); err != nil { if err := g.drawTitle(v, st); err != nil {
return err return err
} }
} }
@ -463,12 +519,12 @@ func (g *Gui) flush() error {
return err return err
} }
} }
termbox.Flush() g.screen.Show()
return nil return nil
} }
// drawFrameEdges draws the horizontal and vertical edges of a view. // drawFrameEdges draws the horizontal and vertical edges of a view.
func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error { func (g *Gui) drawFrameEdges(v *View, style tcell.Style) error {
runeH, runeV := '─', '│' runeH, runeV := '─', '│'
if g.ASCII { if g.ASCII {
runeH, runeV = '-', '|' runeH, runeV = '-', '|'
@ -479,12 +535,12 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
continue continue
} }
if v.y0 > -1 && v.y0 < g.maxY { if v.y0 > -1 && v.y0 < g.maxY {
if err := g.SetRune(x, v.y0, runeH, fgColor, bgColor); err != nil { if err := g.SetRune(x, v.y0, runeH, style); err != nil {
return err return err
} }
} }
if v.y1 > -1 && v.y1 < g.maxY { if v.y1 > -1 && v.y1 < g.maxY {
if err := g.SetRune(x, v.y1, runeH, fgColor, bgColor); err != nil { if err := g.SetRune(x, v.y1, runeH, style); err != nil {
return err return err
} }
} }
@ -494,12 +550,12 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
continue continue
} }
if v.x0 > -1 && v.x0 < g.maxX { if v.x0 > -1 && v.x0 < g.maxX {
if err := g.SetRune(v.x0, y, runeV, fgColor, bgColor); err != nil { if err := g.SetRune(v.x0, y, runeV, style); err != nil {
return err return err
} }
} }
if v.x1 > -1 && v.x1 < g.maxX { if v.x1 > -1 && v.x1 < g.maxX {
if err := g.SetRune(v.x1, y, runeV, fgColor, bgColor); err != nil { if err := g.SetRune(v.x1, y, runeV, style); err != nil {
return err return err
} }
} }
@ -508,7 +564,7 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
} }
// drawFrameCorners draws the corners of the view. // drawFrameCorners draws the corners of the view.
func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error { func (g *Gui) drawFrameCorners(v *View, style tcell.Style) error {
runeTL, runeTR, runeBL, runeBR := '┌', '┐', '└', '┘' runeTL, runeTR, runeBL, runeBR := '┌', '┐', '└', '┘'
if g.ASCII { if g.ASCII {
runeTL, runeTR, runeBL, runeBR = '+', '+', '+', '+' runeTL, runeTR, runeBL, runeBR = '+', '+', '+', '+'
@ -521,7 +577,7 @@ func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
for _, c := range corners { for _, c := range corners {
if c.x >= 0 && c.y >= 0 && c.x < g.maxX && c.y < g.maxY { if c.x >= 0 && c.y >= 0 && c.x < g.maxX && c.y < g.maxY {
if err := g.SetRune(c.x, c.y, c.ch, fgColor, bgColor); err != nil { if err := g.SetRune(c.x, c.y, c.ch, style); err != nil {
return err return err
} }
} }
@ -530,7 +586,7 @@ func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
} }
// drawTitle draws the title of the view. // drawTitle draws the title of the view.
func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error { func (g *Gui) drawTitle(v *View, style tcell.Style) error {
if v.y0 < 0 || v.y0 >= g.maxY { if v.y0 < 0 || v.y0 >= g.maxY {
return nil return nil
} }
@ -542,7 +598,7 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
} else if x > v.x1-2 || x >= g.maxX { } else if x > v.x1-2 || x >= g.maxX {
break break
} }
if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil { if err := g.SetRune(x, v.y0, ch, style); err != nil {
return err return err
} }
} }
@ -568,13 +624,13 @@ func (g *Gui) draw(v *View) error {
gMaxX, gMaxY := g.Size() gMaxX, gMaxY := g.Size()
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1 cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY { if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
termbox.SetCursor(cx, cy) g.screen.ShowCursor(cx, cy)
} else { } else {
termbox.HideCursor() g.screen.ShowCursor(-1, -1) // HideCursor
} }
} }
} else { } else {
termbox.HideCursor() g.screen.ShowCursor(-1, -1) // HideCursor
} }
v.clearRunes() v.clearRunes()
@ -584,13 +640,13 @@ func (g *Gui) draw(v *View) error {
return nil return nil
} }
// onKey manages key-press events. A keybinding handler is called when // onEvent manages key/mouse events. A eventBinding handler is called when
// a key-press or mouse event satisfies a configured keybinding. Furthermore, // a key-press or mouse event satisfies a configured eventBinding. Furthermore,
// currentView's internal buffer is modified if currentView.Editable is true. // currentView's internal buffer is modified if currentView.Editable is true.
func (g *Gui) onKey(ev *termbox.Event) error { func (g *Gui) onEvent(ev tcell.Event) error {
switch ev.Type { switch tev := ev.(type) {
case termbox.EventKey: case *tcell.EventKey:
matched, err := g.execKeybindings(g.currentView, ev) matched, err := g.execEventBindings(g.currentView, ev)
if err != nil { if err != nil {
return err return err
} }
@ -598,34 +654,58 @@ func (g *Gui) onKey(ev *termbox.Event) error {
break break
} }
if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil { if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil {
g.currentView.Editor.Edit(g.currentView, Key(ev.Key), ev.Ch, Modifier(ev.Mod)) g.currentView.Editor.Edit(g.currentView, tev.Key(), tev.Rune(), tev.Modifiers())
} }
case termbox.EventMouse: case *tcell.EventMouse:
mx, my := ev.MouseX, ev.MouseY v, _, _, err := g.GetViewRelativeMousePosition(tev)
v, err := g.ViewByPosition(mx, my)
if err != nil { if err != nil {
break break
} }
if err := v.SetCursor(mx-v.x0-1, my-v.y0-1); err != nil { // If the key-binding wants to move the cursor, it should call SetCursorFromCurrentMouseEvent()
// Not all mouse events will want to do this (eg: scroll wheel)
g.CurrentEvent = ev
if _, err := g.execEventBindings(v, g.CurrentEvent); err != nil {
return err return err
} }
if _, err := g.execKeybindings(v, ev); err != nil { }
return err return nil
}
// GetViewRelativeMousePosition returns the View and relative x/y for the provided mouse event.
func (g *Gui) GetViewRelativeMousePosition(ev tcell.Event) (*View, int, int, error) {
if kbe, ok := ev.(*tcell.EventMouse); ok {
mx, my := kbe.Position()
v, err := g.ViewByPosition(mx, my)
if err != nil {
return nil, 0, 0, err
} }
return v, mx - v.x0 - 1, my - v.y0 - 1, nil
} }
return nil, 0, 0, errors.New("Cannot GetViewRelativeMousePosition on non-mouse event")
}
// SetCursorFromCurrentMouseEvent updates the cursor position based on the mouse coordinates.
func (g *Gui) SetCursorFromCurrentMouseEvent() error {
v, x, y, err := g.GetViewRelativeMousePosition(g.CurrentEvent)
if err != nil {
return err
}
if err := v.SetCursor(x, y); err != nil {
return err
}
return nil return nil
} }
// execKeybindings executes the keybinding handlers that match the passed view // execEventBindings executes the handlers that match the passed view
// and event. The value of matched is true if there is a match and no errors. // and event. The value of matched is true if there is a match and no errors.
func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err error) { // TODO: rename to more generic - it's not just keys (incl mouse)
func (g *Gui) execEventBindings(v *View, xev tcell.Event) (matched bool, err error) {
matched = false matched = false
for _, kb := range g.keybindings { for _, kb := range g.eventBindings {
if kb.handler == nil { if kb.handler == nil {
continue continue
} }
if kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) && kb.matchView(v) { if kb.matchEvent(xev) && kb.matchView(v) {
if err := kb.handler(g, v); err != nil { if err := kb.handler(g, v); err != nil {
return false, err return false, err
} }

@ -10,7 +10,7 @@ import (
"io" "io"
"strings" "strings"
"github.com/miguelmota/termbox-go" "github.com/gdamore/tcell/v2"
) )
// A View is a window. It maintains its own internal buffer and cursor // A View is a window. It maintains its own internal buffer and cursor
@ -31,18 +31,18 @@ type View struct {
// BgColor and FgColor allow to configure the background and foreground // BgColor and FgColor allow to configure the background and foreground
// colors of the View. // colors of the View.
BgColor, FgColor Attribute Style tcell.Style
// SelBgColor and SelFgColor are used to configure the background and // SelBgColor and SelFgColor are used to configure the background and
// foreground colors of the selected line, when it is highlighted. // foreground colors of the selected line, when it is highlighted.
SelBgColor, SelFgColor Attribute SelStyle tcell.Style
// If Editable is true, keystrokes will be added to the view's internal // If Editable is true, keystrokes will be added to the view's internal
// buffer at the cursor position. // buffer at the cursor position.
Editable bool Editable bool
// Editor allows to define the editor that manages the edition mode, // Editor allows to define the editor that manages the edition mode,
// including keybindings or cursor behaviour. DefaultEditor is used by // including eventBindings or cursor behaviour. DefaultEditor is used by
// default. // default.
Editor Editor Editor Editor
@ -71,6 +71,9 @@ type View struct {
// If Mask is true, the View will display the mask instead of the real // If Mask is true, the View will display the mask instead of the real
// content // content
Mask rune Mask rune
// The gui that owns this view
g *Gui
} }
type viewLine struct { type viewLine struct {
@ -79,8 +82,9 @@ type viewLine struct {
} }
type cell struct { type cell struct {
chr rune chr rune
bgColor, fgColor Attribute // bgColor, fgColor Attribute
style tcell.Style
} }
type lineType []cell type lineType []cell
@ -95,7 +99,7 @@ func (l lineType) String() string {
} }
// newView returns a new View object. // newView returns a new View object.
func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { func newView(name string, x0, y0, x1, y1 int, g *Gui) *View {
v := &View{ v := &View{
name: name, name: name,
x0: x0, x0: x0,
@ -105,7 +109,8 @@ func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
Frame: true, Frame: true,
Editor: DefaultEditor, Editor: DefaultEditor,
tainted: true, tainted: true,
ei: newEscapeInterpreter(mode), ei: newEscapeInterpreter(),
g: g,
} }
return v return v
} }
@ -123,7 +128,7 @@ func (v *View) Name() string {
// setRune sets a rune at the given point relative to the view. It applies the // setRune sets a rune at the given point relative to the view. It applies the
// specified colors, taking into account if the cell must be highlighted. Also, // specified colors, taking into account if the cell must be highlighted. Also,
// it checks if the position is valid. // it checks if the position is valid.
func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error { func (v *View) setRune(x, y int, ch rune, style tcell.Style) error {
maxX, maxY := v.Size() maxX, maxY := v.Size()
if x < 0 || x >= maxX || y < 0 || y >= maxY { if x < 0 || x >= maxX || y < 0 || y >= maxY {
return errors.New("invalid point") return errors.New("invalid point")
@ -145,16 +150,13 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
} }
if v.Mask != 0 { if v.Mask != 0 {
fgColor = v.FgColor style = v.Style
bgColor = v.BgColor
ch = v.Mask ch = v.Mask
} else if v.Highlight && ry == rcy { } else if v.Highlight && ry == rcy {
fgColor = v.SelFgColor style = v.SelStyle
bgColor = v.SelBgColor
} }
termbox.SetCell(v.x0+x+1, v.y0+y+1, ch, v.g.SetRune(v.x0+x+1, v.y0+y+1, ch, style)
termbox.Attribute(fgColor), termbox.Attribute(bgColor))
return nil return nil
} }
@ -240,9 +242,8 @@ func (v *View) parseInput(ch rune) []cell {
if err != nil { if err != nil {
for _, r := range v.ei.runes() { for _, r := range v.ei.runes() {
c := cell{ c := cell{
fgColor: v.FgColor, style: v.Style,
bgColor: v.BgColor, chr: r,
chr: r,
} }
cells = append(cells, c) cells = append(cells, c)
} }
@ -252,9 +253,8 @@ func (v *View) parseInput(ch rune) []cell {
return nil return nil
} }
c := cell{ c := cell{
fgColor: v.ei.curFgColor, style: v.ei.curStyle,
bgColor: v.ei.curBgColor, chr: ch,
chr: ch,
} }
cells = append(cells, c) cells = append(cells, c)
} }
@ -341,16 +341,16 @@ func (v *View) draw() error {
break break
} }
fgColor := c.fgColor st := c.style
if fgColor == ColorDefault { fgColor, bgColor, _ := c.style.Decompose()
fgColor = v.FgColor vfgColor, vbgColor, _ := v.Style.Decompose()
if fgColor == tcell.ColorDefault {
st = st.Foreground(vfgColor)
} }
bgColor := c.bgColor if bgColor == tcell.ColorDefault {
if bgColor == ColorDefault { st = st.Background(vbgColor)
bgColor = v.BgColor
} }
if err := v.setRune(x, y, c.chr, st); err != nil {
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
return err return err
} }
x++ x++
@ -402,8 +402,7 @@ func (v *View) clearRunes() {
maxX, maxY := v.Size() maxX, maxY := v.Size()
for x := 0; x < maxX; x++ { for x := 0; x < maxX; x++ {
for y := 0; y < maxY; y++ { for y := 0; y < maxY; y++ {
termbox.SetCell(v.x0+x+1, v.y0+y+1, ' ', v.g.SetRune(v.x0+x+1, v.y0+y+1, ' ', v.Style)
termbox.Attribute(v.FgColor), termbox.Attribute(v.BgColor))
} }
} }
} }
@ -493,7 +492,7 @@ func (v *View) Word(x, y int) (string, error) {
} else { } else {
nr = nr + x nr = nr + x
} }
return string(str[nl:nr]), nil return str[nl:nr], nil
} }
// indexFunc allows to split lines by words taking into account spaces // indexFunc allows to split lines by words taking into account spaces

@ -2,17 +2,23 @@ package humanize
import ( import (
"fmt" "fmt"
"math"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/goodsign/monday"
"github.com/jeandeaual/go-locale"
"golang.org/x/text/language" "golang.org/x/text/language"
"golang.org/x/text/message" "golang.org/x/text/message"
) )
var cachedSystemLocale = ""
// Numericf produces a string from of the given number with give fixed precision // 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 // in base 10 with thousands separators after every three orders of magnitude
// using a thousands and decimal spearator according to LC_NUMERIC; defaulting "en". // using thousands and decimal separator according to LC_NUMERIC; defaulting "en".
// //
// e.g. Numericf(834142.32, 2) -> "834,142.32" // e.g. Numericf(834142.32, 2) -> "834,142.32"
func Numericf(value float64, precision int) string { func Numericf(value float64, precision int) string {
@ -21,16 +27,76 @@ func Numericf(value float64, precision int) string {
// Monetaryf produces a string from of the given number give minimum precision // Monetaryf produces a string from of the given number give minimum precision
// in base 10 with thousands separators after every three orders of magnitude // in base 10 with thousands separators after every three orders of magnitude
// using thousands and decimal spearator according to LC_MONETARY; defaulting "en". // using thousands and decimal separator according to LC_MONETARY; defaulting "en".
// //
// e.g. Monetaryf(834142.3256, 2) -> "834,142.3256" // e.g. Monetaryf(834142.3256, 2) -> "834,142.3256"
func Monetaryf(value float64, precision int) string { func Monetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", false) return f(value, precision, "LC_MONETARY", false)
} }
// f formats given value v, with d decimal places using thousands and decimal // 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
// separator according to language found in given locale environment variable e. // separator according to language found in given locale environment variable e.
// If r is true the decimal places are fixed to the given d otherwise d is the // If fixed is true the decimal places are fixed to the given precision otherwise d is the
// minimum of decimal places until the first 0. // minimum of decimal places until the first 0.
func f(value float64, precision int, envvar string, fixed bool) string { func f(value float64, precision int, envvar string, fixed bool) string {
parts := strings.Split(strconv.FormatFloat(value, 'f', -1, 64), ".") parts := strings.Split(strconv.FormatFloat(value, 'f', -1, 64), ".")
@ -51,3 +117,47 @@ func f(value float64, precision int, envvar string, fixed bool) string {
format := fmt.Sprintf("%%.%df", precision) format := fmt.Sprintf("%%.%df", precision)
return message.NewPrinter(lang).Sprintf(format, value) 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,7 +1,9 @@
package humanize package humanize
import ( import (
"fmt"
"testing" "testing"
"time"
) )
// TestMonetary tests monetary formatting // TestMonetary tests monetary formatting
@ -10,3 +12,83 @@ func TestMonetary(t *testing.T) {
t.FailNow() 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. // min returns the minimum number of passed int slices.
func min(is ...int) int { func min(is ...int) int {
min := int(math.MaxInt32) min := math.MaxInt32
for _, v := range is { for _, v := range is {
if min > v { if min > v {
min = v min = v

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

@ -1,4 +1,5 @@
//+build !windows //go:build !windows
// +build !windows
package ssh package ssh
@ -16,9 +17,9 @@ import (
"time" "time"
"unsafe" "unsafe"
"github.com/cointop-sh/cointop/pkg/pathutil"
"github.com/creack/pty" "github.com/creack/pty"
"github.com/gliderlabs/ssh" "github.com/gliderlabs/ssh"
"github.com/miguelmota/cointop/pkg/pathutil"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
@ -196,6 +197,9 @@ func (s *Server) ListenAndServe() error {
cmd := exec.CommandContext(cmdCtx, s.executableBinary, flags...) cmd := exec.CommandContext(cmdCtx, s.executableBinary, flags...)
cmd.Env = append(sshSession.Environ(), fmt.Sprintf("TERM=%s", ptyReq.Term)) 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) f, err := pty.Start(cmd)
if err != nil { if err != nil {
@ -238,7 +242,7 @@ func (s *Server) ListenAndServe() error {
err := s.sshServer.SetOption(ssh.HostKeyFile(s.hostKeyFile)) err := s.sshServer.SetOption(ssh.HostKeyFile(s.hostKeyFile))
if err != nil { if err != nil {
return err return fmt.Errorf("error setting HostKeyFile: %s: %v", s.hostKeyFile, err)
} }
return s.sshServer.ListenAndServe() return s.sshServer.ListenAndServe()

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

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

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

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

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

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

@ -27,15 +27,15 @@ import (
*/ */
type MBarChart struct { type MBarChart struct {
Block Block
BarColor [NumberofColors]Attribute BarColor [NumberOfColors]Attribute
TextColor Attribute TextColor Attribute
NumColor [NumberofColors]Attribute NumColor [NumberOfColors]Attribute
Data [NumberofColors][]int Data [NumberOfColors][]int
DataLabels []string DataLabels []string
BarWidth int BarWidth int
BarGap int BarGap int
labels [][]rune labels [][]rune
dataNum [NumberofColors][][]rune dataNum [NumberOfColors][][]rune
numBar int numBar int
scale float64 scale float64
max int max int
@ -102,11 +102,11 @@ func (bc *MBarChart) layout() {
bc.BarColor[i] = ColorBlack bc.BarColor[i] = ColorBlack
} else { } else {
bc.BarColor[i] = bc.BarColor[i-1] + 1 bc.BarColor[i] = bc.BarColor[i-1] + 1
if bc.BarColor[i] > NumberofColors { if bc.BarColor[i] > NumberOfColors {
bc.BarColor[i] = ColorBlack bc.BarColor[i] = ColorBlack
} }
} }
bc.NumColor[i] = (NumberofColors + 1) - bc.BarColor[i] //Make NumColor opposite of barColor for visibility bc.NumColor[i] = (NumberOfColors + 1) - bc.BarColor[i] //Make NumColor opposite of barColor for visibility
} }
} }

@ -4,29 +4,12 @@
package termui package termui
import (
"image"
"io"
"sync"
"time"
"fmt"
"os"
"runtime/debug"
"bytes"
"github.com/maruel/panicparse/stack"
tm "github.com/miguelmota/termbox-go"
)
// Bufferer should be implemented by all renderable components. // Bufferer should be implemented by all renderable components.
type Bufferer interface { type Bufferer interface {
Buffer() Buffer Buffer() Buffer
} }
/*
// Init initializes termui library. This function should be called before any others. // Init initializes termui library. This function should be called before any others.
// After initialization, the library must be finalized by 'Close' function. // After initialization, the library must be finalized by 'Close' function.
func Init() error { func Init() error {
@ -188,3 +171,4 @@ func Render(bs ...Bufferer) {
//go func() { renderJobs <- bs }() //go func() { renderJobs <- bs }()
renderJobs <- bs renderJobs <- bs
} }
*/

@ -39,8 +39,8 @@ type Sparklines struct {
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
// Add appends a given Sparkline to s *Sparklines. // Add appends a given Sparkline to s *Sparklines.
func (s *Sparklines) Add(sl Sparkline) { func (sl *Sparklines) Add(line Sparkline) {
s.Lines = append(s.Lines, sl) sl.Lines = append(sl.Lines, line)
} }
// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines. // NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines.

@ -111,7 +111,7 @@ func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
ns := strings.Split(name, ".") ns := strings.Split(name, ".")
for i := range ns { for i := range ns {
nn := strings.Join(ns[i:len(ns)], ".") nn := strings.Join(ns[i:], ".")
a, ok = ColorMap[nn] a, ok = ColorMap[nn]
if ok { if ok {
break break
@ -121,7 +121,7 @@ func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
return a return a
} }
// 0<=r,g,b <= 5 // ColorRGB return an Attribute for the given RGB (value 0-5)
func ColorRGB(r, g, b int) Attribute { func ColorRGB(r, g, b int) Attribute {
within := func(n int) int { within := func(n int) int {
if n < 0 { if n < 0 {

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

Loading…
Cancel
Save