Merge branch 'master' into master

pull/318/head
владимир иванович архипов 11 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:
branches: [ master ]
pull_request:
branches: [ master ]
branches: [ mas....
jobs:
build:

6
.gitignore vendored

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

@ -6,4 +6,4 @@ builds:
goarch:
- amd64
ldflags:
- -X github.com/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
License: Apache-2.0
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: golang >= 1.14
@ -20,11 +20,11 @@ cointop is a fast and lightweight interactive terminal based UI application for
%setup -q -n %{name}-%{version}
%build
mkdir -p ./_build/src/github.com/miguelmota
ln -s $(pwd) ./_build/src/github.com/miguelmota/%{name}
mkdir -p ./_build/src/github.com/cointop-sh
ln -s $(pwd) ./_build/src/github.com/cointop-sh/%{name}
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 -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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.6.10] - 2021-11-06
### Added
- Search by symbol or name
- Purchase price option for portfolio entries
- Mouse support for column sorting and menu options
### Changed
- `0` keybinding to go to first row of first page
### Fixed
- Coin sorting
- Editable shortcuts
- Duplicate portfolio entries
## [1.6.9] - 2021-10-12
### Added
- Chart x-axis date labels
- Configurable favorite character
- Configurable chart width
- Save chart height
### Changed
- Renamed organization `miguelmota``cointop-sh`
### Fixed
- Global chart currency
- Chart resampling and interpolation
- Chart time periods
- Use preferred cache directory
- Currency symbol width
## [1.6.8] - 2021-09-13
### Fixed
- Hide holdings amount when using command hide flag
@ -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
## [1.6.1] - 2021-02-12
### Added
- Multiple coin support in price command
### Fixed
- Chart data interpolation
- CoinMarketCap graph data endpoint
## [1.6.0] - 2021-02-12
### Added
- Multiple coin support in price command
- Configurable table columns
- Basic price alerts
## [1.6.0] - 2021-02-12
### Fixed
- Coin chart lookup
- Dynamic column widths
## [1.5.5] - 2020-11-15
### Added
- Configurable table columns
- Basic price alerts
- Currency convesion option to holdings command
- Sort by percent holdings shortcut
## [1.5.5] - 2020-11-15
### Fixed
- Termux cache directory
- Open command on Windows
## [1.5.4] - 2020-08-24
### Added
- Currency convesion option to holdings command
- Sort by percent holdings shortcut
- Colorschemes directory flag
## [1.5.4] - 2020-08-24
### Fixed
- Rank order for low market cap coins
### Added
- Colorschemes directory flag
## [1.5.3] - 2020-08-14
### Fixed
- Build error
## [1.5.2] - 2020-08-13
### Fixed
- `XDG_CONFIG_HOME` config path
### Added
- Holdings command with sorting and filter options
- Bitcoin dominance command
### Fixed
- `XDG_CONFIG_HOME` config path
## [1.5.1] - 2020-08-05
### Fixed
- Version typo
@ -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
## [1.4.5] - 2020-02-18
### Fixed
- Convert to chosen currency for market data
### Added
- VND currency conversion
### Fixed
- Convert to chosen currency for market data
## [1.4.4] - 2019-12-31
### Fixed
- Flathub app release version
## [1.4.3] - 2019-12-29
### Added
- Tab keybinding
### Fixed
- Chart update bug fixes
- Marketbar currency bug fixes
### Added
- Tab keybinding
## [1.4.2] - 2019-12-29
### Fixed
- Fix keybinding issue on FreeBSD
@ -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
## [1.1.4] - 2019-04-21
### Changed
- CoinMarketCap legacy V2 API to Pro V1 API
### Added
- Config option to use CoinMarketCap Pro V1 API KEY
### Changed
- CoinMarketCap legacy V2 API to Pro V1 API
## [1.1.3] - 2019-02-25
### Fixed
- Vendor dependencies
## [1.1.2] - 2018-12-30
### Fixed
- Paginate CoinMarketCap V1 API responses due to their backward-incompatible update
### Added
- `-clean` flag to clean cache
- `-reset` flag to clean cache and delete config
- `-config` flag to use a different specified config file
### Fixed
- Paginate CoinMarketCap V1 API responses due to their backward-incompatible update
## [1.1.1] - 2018-12-26
### Changed
- Use go modules instead of dep

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

@ -1,4 +1,5 @@
VERSION = $$(git describe --abbrev=0 --tags)
COMMIT_TAG = $$(git tag --points-at HEAD)
VERSION_DATE = $$(git log -1 --pretty='%ad' --date=format:'%Y-%m-%d' $(VERSION))
COMMIT_REV = $$(git rev-list -n 1 $(VERSION))
MAINTAINER = "Miguel Mota"
@ -30,18 +31,18 @@ debug:
.PHONY: 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
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
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
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop64 && upx bin/cointop64 && \
env GOARCH=386 go build -ldflags "-s -w -X github.com/miguelmota/cointop/cointop.version=$(VERSION)" -o bin/cointop32 && upx bin/cointop32
env GOARCH=amd64 go build -ldflags "-s -w -X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/cointop64 && upx bin/cointop64 && \
env GOARCH=386 go build -ldflags "-s -w -X github.com/cointop-sh/cointop/cointop.version=$(VERSION)" -o bin/cointop32 && upx bin/cointop32
install: build
sudo mv bin/cointop /usr/local/bin
@ -96,7 +97,7 @@ snap-clean:
snap-stage:
# https://github.com/elopio/go/issues/2
mv go.mod go.mod~ ;GO111MODULE=off GOFLAGS="-ldflags=-s -ldflags=-w -ldflags=-X=github.com/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:
sudo apt install snapd
@ -176,7 +177,7 @@ rpm-dirs:
chmod -R a+rwx ~/rpmbuild
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:
sudo dnf install -y copr-cli
@ -210,7 +211,7 @@ brew-test:
brew test cointop.rb
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 cointop/cointop
@ -228,12 +229,23 @@ release:
rm -rf dist
VERSION=$(VERSION) goreleaser
docker-login:
docker login
docker-login-ci:
docker login -u $(DOCKER_USER) -p $(DOCKER_PASS)
docker-build:
docker build --build-arg VERSION=$(VERSION) --build-arg MAINTAINER=$(MAINTAINER) -t cointop/cointop .
docker-tag:
docker tag cointop/cointop:latest cointop/cointop:$(VERSION)
docker-tag-ci:
docker tag cointop/cointop:latest cointop/cointop:$(CIRCLE_SHA1)
docker tag cointop/cointop:latest cointop/cointop:$(CIRCLE_BRANCH)
test $(COMMIT_TAG) && docker tag cointop/cointop:latest cointop/cointop:$(COMMIT_TAG); true
docker-run:
docker run -it cointop/cointop
@ -241,13 +253,19 @@ docker-push:
docker push cointop/cointop:$(VERSION)
docker push cointop/cointop:latest
docker-push-ci:
docker push cointop/cointop:$(CIRCLE_SHA1)
docker push cointop/cointop:$(CIRCLE_BRANCH)
test $(COMMIT_TAG) && docker push cointop/cointop:$(COMMIT_TAG); true
test $(CIRCLE_BRANCH) == "master" && docker push cointop/cointop:latest; true
docker-build-and-push: docker-build docker-tag docker-push
docker-run-ssh:
docker run -p 2222:22 -v ~/.ssh/demo:/keys -v ~/.cache/cointop:/tmp/cointop_config --entrypoint cointop -it cointop/cointop server -k /keys/id_rsa
ssh-server:
go run cmd/cointop/cointop.go server -p 2222
go run cmd/cointop/cointop.go server -p 2222 -k ~/.ssh/demo/id_rsa
ssh-client:
ssh localhost -p 2222

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,8 @@
package cointop
import log "github.com/sirupsen/logrus"
import (
log "github.com/sirupsen/logrus"
)
// Coin is the row structure
type Coin struct {
@ -23,8 +25,10 @@ type Coin struct {
// for favorites
Favorite bool
// for portfolio
Holdings float64
Balance float64
Holdings float64
Balance float64
BuyPrice float64
BuyCurrency string
}
// AllCoins returns a slice of all the coins
@ -90,3 +94,36 @@ func (ct *Cointop) CoinByID(id string) *Coin {
}
return nil
}
// UpdateCoin updates coin info after fetching from API
func (ct *Cointop) UpdateCoin(coin *Coin) error {
log.Debug("UpdateCoin()")
v, err := ct.api.GetCoinData(coin.Name, ct.State.currencyConversion)
if err != nil {
log.Debugf("UpdateCoin() could not fetch coin data %s", coin.Name)
return err
}
coin = &Coin{
ID: v.ID,
Name: v.Name,
Symbol: v.Symbol,
Rank: v.Rank,
Price: v.Price,
Volume24H: v.Volume24H,
MarketCap: v.MarketCap,
AvailableSupply: v.AvailableSupply,
TotalSupply: v.TotalSupply,
PercentChange1H: v.PercentChange1H,
PercentChange24H: v.PercentChange24H,
PercentChange7D: v.PercentChange7D,
PercentChange30D: v.PercentChange30D,
PercentChange1Y: v.PercentChange1Y,
LastUpdated: v.LastUpdated,
Slug: v.Slug,
}
ct.State.allCoinsSlugMap.Store(coin.Name, coin)
return nil
}

@ -5,8 +5,8 @@ import (
"strconv"
"time"
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/table"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/cointop-sh/cointop/pkg/table"
)
// SupportedCoinTableHeaders are all the supported coin table header columns
@ -67,8 +67,8 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
if ct.IsFavoritesVisible() {
headers = ct.GetFavoritesTableHeaders()
}
ct.ClearSyncMap(ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft)
ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
for _, coin := range ct.State.coins {
if coin == nil {
continue
@ -82,7 +82,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
star := " "
rankcolor := ct.colorscheme.TableRow
if coin.Favorite {
star = "*"
star = ct.State.favoriteChar
rankcolor = ct.colorscheme.TableRowFavorite
}
rank := fmt.Sprintf("%s%6v ", star, coin.Rank)
@ -136,6 +136,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "24h_volume":
text := humanize.Monetaryf(coin.Volume24H, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.Volume24H, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -243,6 +246,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "market_cap":
text := humanize.Monetaryf(coin.MarketCap, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.MarketCap, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -255,6 +261,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "total_supply":
text := humanize.Numericf(coin.TotalSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.TotalSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -267,6 +276,9 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "available_supply":
text := humanize.Numericf(coin.AvailableSupply, 0)
if ct.IsActiveTableCompactNotation() {
text = humanize.ScaleNumericf(coin.AvailableSupply, 3)
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -279,7 +291,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table {
})
case "last_updated":
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.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,

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

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

@ -3,15 +3,18 @@ package cointop
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
color "github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/pad"
fcolor "github.com/fatih/color"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/mattn/go-runewidth"
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.
var FiatCurrencyNames = map[string]string{
"AUD": "Australian Dollar",
@ -175,9 +178,10 @@ func (ct *Cointop) UpdateConvertMenu() error {
}
shortcut := string(alphanumericcharacters[i])
if key == ct.State.currencyConversion {
shortcut = ct.colorscheme.MenuLabelActive(color.Bold("*"))
key = ct.colorscheme.Menu(color.Bold(key))
currency = ct.colorscheme.MenuLabelActive(color.Bold(currency))
Bold := fcolor.New(fcolor.Bold).SprintFunc()
shortcut = ct.colorscheme.MenuLabelActive(Bold("*"))
key = ct.colorscheme.Menu(Bold(key))
currency = ct.colorscheme.MenuLabelActive(Bold(currency))
} else {
key = ct.colorscheme.Menu(key)
currency = ct.colorscheme.MenuLabel(currency)
@ -231,6 +235,10 @@ func (ct *Cointop) SetCurrencyConverstion(convert string) error {
func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
log.Debug("SetCurrencyConverstionFn()")
return func() error {
if !ct.State.convertMenuVisible {
return nil
}
ct.HideConvertMenu()
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 {
return err
}
go ct.UpdateCurrentPageCoins()
go ct.RefreshAll()
return nil
}
@ -249,7 +257,14 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
// CurrencySymbol returns the symbol for the currency conversion
func (ct *Cointop) CurrencySymbol() string {
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
@ -293,3 +308,40 @@ func CurrencySymbol(currency string) string {
return "?"
}
// ConversionMouseLeftClick is called on mouse left click event
func (ct *Cointop) ConversionMouseLeftClick() error {
v, x, y, err := ct.g.GetViewRelativeMousePosition(ct.g.CurrentEvent)
if err != nil {
return err
}
// Find the menu entry that includes the mouse position
line := v.BufferLines()[y]
matches := regexp.MustCompile(`\[ . \] \w+ [^\[]+`).FindAllStringIndex(line, -1)
for _, match := range matches {
if x >= match[0] && x <= match[1] {
s := line[match[0]:match[1]]
convert := strings.Split(s, " ")[3]
return ct.SetCurrencyConverstionFn(convert)()
}
}
return nil
}
// Convert converts an amount to another currency type
func (ct *Cointop) Convert(convertFrom, convertTo string, amount float64) (float64, error) {
convertFrom = strings.ToLower(convertFrom)
convertTo = strings.ToLower(convertTo)
if convertFrom == convertTo {
return amount, nil
}
rate, err := ct.api.GetExchangeRate(convertFrom, convertTo, true)
if err != nil {
return 0, err
}
return rate * amount, nil
}

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

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

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

@ -56,7 +56,7 @@ func (ct *Cointop) ToggleShowFavorites() error {
// GetFavoritesSlice returns coin favorites as slice
func (ct *Cointop) GetFavoritesSlice() []*Coin {
log.Debug("GetFavoritesSlice()")
sliced := []*Coin{}
var sliced []*Coin
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
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
})

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

@ -2,168 +2,103 @@ package cointop
import (
"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
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{}
mod := gocui.ModNone
split := strings.Split(s, "+")
if len(split) > 1 {
m := strings.ToLower(strings.TrimSpace(split[0]))
k := strings.ToLower(strings.TrimSpace(split[1]))
if m == "alt" {
mod = gocui.ModAlt
s = k
} else if m == "ctrl" {
switch k {
case "0":
key = '0'
case "1":
key = '1'
case "2":
key = gocui.KeyCtrl2
case "3":
key = gocui.KeyCtrl3
case "4":
key = gocui.KeyCtrl4
case "5":
key = gocui.KeyCtrl5
case "6":
key = gocui.KeyCtrl6
case "7":
key = gocui.KeyCtrl7
case "8":
key = gocui.KeyCtrl8
case "9":
key = '9'
case "a":
key = gocui.KeyCtrlA
case "b":
key = gocui.KeyCtrlB
case "c":
key = gocui.KeyCtrlC
case "d":
key = gocui.KeyCtrlD
case "e":
key = gocui.KeyCtrlE
case "f":
key = gocui.KeyCtrlF
case "g":
key = gocui.KeyCtrlG
case "h":
key = gocui.KeyCtrlH
case "i":
key = gocui.KeyCtrlI
case "j":
key = gocui.KeyCtrlJ
case "k":
key = gocui.KeyCtrlK
case "l":
key = gocui.KeyCtrlL
case "m":
key = gocui.KeyCtrlL
case "n":
key = gocui.KeyCtrlN
case "o":
key = gocui.KeyCtrlO
case "p":
key = gocui.KeyCtrlP
case "q":
key = gocui.KeyCtrlQ
case "r":
key = gocui.KeyCtrlR
case "s":
key = gocui.KeyCtrlS
case "t":
key = gocui.KeyCtrlT
case "u":
key = gocui.KeyCtrlU
case "v":
key = gocui.KeyCtrlV
case "w":
key = gocui.KeyCtrlW
case "x":
key = gocui.KeyCtrlX
case "y":
key = gocui.KeyCtrlY
case "z":
key = gocui.KeyCtrlZ
case "~":
key = gocui.KeyCtrlTilde
case "[", "lsqrbracket", "leftsqrbracket", "leftsquarebracket":
key = gocui.KeyCtrlLsqBracket
case "]", "rsqrbracket", "rightsqrbracket", "rightsquarebracket":
key = gocui.KeyCtrlRsqBracket
case "space":
key = gocui.KeyCtrlSpace
case "backslash":
key = gocui.KeyCtrlBackslash
case "underscore":
key = gocui.KeyCtrlUnderscore
case "\\\\":
key = '\\'
mod := tcell.ModNone
// translate legacy and special names for keys
keyName := keyMap(strings.TrimSpace(s))
if len(keyName) > 1 {
keyName = strings.Replace(keyName, "+", "-", -1)
split := strings.Split(keyName, "-")
if len(split) > 1 {
m := strings.ToLower(strings.TrimSpace(split[0]))
k := strings.TrimSpace(split[1])
k = keyMap(k)
if k == " " {
k = "Space" // fix mod+space
}
if m == "alt" {
mod = tcell.ModAlt
keyName = k
} else if m == "ctrl" {
// let the lookup handle it
keyName = m + "-" + k
} else {
keyName = m + "-" + k
}
// TODO: other mods?
}
}
// First try looking up keyname directly
lcKeyName := strings.ToLower(keyName)
for key, name := range tcell.KeyNames {
if strings.ToLower(name) == lcKeyName {
if strings.HasPrefix(name, "Ctrl-") {
mod = tcell.ModCtrl
}
return key, mod
}
}
if len(s) == 1 {
r := []rune(s)
// Then try one-rune variants
if len(keyName) == 1 {
r := []rune(keyName)
key = r[0]
return key, mod
}
s = strings.ToLower(s)
switch s {
case "arrowup", "uparrow", "up":
key = gocui.KeyArrowUp
case "arrowdown", "downarrow", "down":
key = gocui.KeyArrowDown
case "arrowleft", "leftarrow", "left":
key = gocui.KeyArrowLeft
case "arrowright", "rightarrow", "right":
key = gocui.KeyArrowRight
case "enter", "return":
key = gocui.KeyEnter
case "space", "spacebar":
key = gocui.KeySpace
case "esc", "escape":
key = gocui.KeyEsc
case "f1":
key = gocui.KeyF1
case "f2":
key = gocui.KeyF2
case "f3":
key = gocui.KeyF3
case "f4":
key = gocui.KeyF4
case "f5":
key = gocui.KeyF5
case "f6":
key = gocui.KeyF6
case "f7":
key = gocui.KeyF7
case "f8":
key = gocui.KeyF8
case "f9":
key = gocui.KeyF9
case "tab":
key = gocui.KeyTab
case "pageup", "pgup":
key = gocui.KeyPgup
case "pagedown", "pgdown", "pgdn":
key = gocui.KeyPgdn
case "home":
key = gocui.KeyHome
case "end":
key = gocui.KeyEnd
case "\\\\":
key = '\\'
if key == nil {
log.Debugf("Could not map key '%s' to key", s)
}
return key, mod
}
@ -197,6 +132,8 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
fn = ct.Keyfn(ct.NavigateLastLine)
case "open_link":
fn = ct.Keyfn(ct.OpenLink)
case "open_alt_link":
fn = ct.Keyfn(ct.OpenAltLink)
case "refresh":
fn = ct.Keyfn(ct.Refresh)
case "sort_column_asc":
@ -218,6 +155,8 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
view = "help"
case "first_page":
fn = ct.Keyfn(ct.FirstPage)
case "move_to_first_page_first_row":
fn = ct.Keyfn(ct.NavigateToFirstPageFirstRow)
case "sort_column_1h_change":
fn = ct.Sortfn("1h_change", true)
case "sort_column_24h_change":
@ -323,11 +262,18 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error
fn = ct.Keyfn(ct.CursorDownOrNextPage)
case "move_up_or_previous_page":
fn = ct.Keyfn(ct.CursorUpOrPreviousPage)
case "sort_column_cost":
fn = ct.Sortfn("cost", true)
case "sort_column_pnl":
fn = ct.Sortfn("pnl", true)
case "sort_column_pnl_percent":
fn = ct.Sortfn("pnl_percent", true)
default:
fn = ct.Keyfn(ct.Noop)
}
ct.SetKeybindingMod(key, mod, fn, view)
return nil
}
@ -340,61 +286,91 @@ func (ct *Cointop) SetKeybindings() error {
}
// keys to force quit
ct.SetKeybindingMod(gocui.KeyCtrlC, gocui.ModNone, ct.Keyfn(ct.Quit), "")
ct.SetKeybindingMod(gocui.KeyCtrlZ, gocui.ModNone, ct.Keyfn(ct.Quit), "")
ct.SetKeybindingMod(tcell.KeyCtrlC, tcell.ModNone, ct.Keyfn(ct.Quit), "")
ct.SetKeybindingMod(tcell.KeyCtrlZ, tcell.ModNone, ct.Keyfn(ct.Quit), "")
// searchfield keys
ct.SetKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.Keyfn(ct.DoSearch), ct.Views.SearchField.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.CancelSearch), ct.Views.SearchField.Name())
ct.SetKeybindingMod(tcell.KeyEnter, tcell.ModNone, ct.Keyfn(ct.DoSearch), ct.Views.SearchField.Name())
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.CancelSearch), ct.Views.SearchField.Name())
// keys to quit help when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
// keys to quit portfolio update menu when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
// keys to quit convert menu when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod(tcell.KeyEsc, tcell.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', tcell.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
// 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("/")
ct.DeleteKeybindingMod(key, mod, "")
// mouse events
ct.SetKeybindingMod(gocui.MouseRelease, gocui.ModNone, ct.Keyfn(ct.MouseRelease), "")
ct.SetKeybindingMod(gocui.MouseLeft, gocui.ModNone, ct.Keyfn(ct.MouseLeftClick), "")
ct.SetKeybindingMod(gocui.MouseMiddle, gocui.ModNone, ct.Keyfn(ct.MouseMiddleClick), "")
ct.SetKeybindingMod(gocui.MouseRight, gocui.ModNone, ct.Keyfn(ct.MouseRightClick), "")
ct.SetKeybindingMod(gocui.MouseWheelUp, gocui.ModNone, ct.Keyfn(ct.MouseWheelUp), "")
ct.SetKeybindingMod(gocui.MouseWheelDown, gocui.ModNone, ct.Keyfn(ct.MouseWheelDown), "")
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.MouseLeftClick), ct.Views.Table.Name()) // click to focus
// clicking table headers sorts table
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.TableHeaderMouseLeftClick), ct.Views.TableHeader.Name())
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.StatusbarMouseLeftClick), ct.Views.Statusbar.Name())
// debug mouse clicks
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.MouseDebug), "")
ct.SetMousebindingMod(tcell.WheelUp, tcell.ModNone, ct.Keyfn(ct.CursorUpOrPreviousPage), ct.Views.Table.Name())
ct.SetMousebindingMod(tcell.WheelDown, tcell.ModNone, ct.Keyfn(ct.CursorDownOrNextPage), ct.Views.Table.Name())
// character key press to select option
// TODO: use scrolling table
keys := ct.SortedSupportedCurrencyConversions()
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
}
// 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
switch t := key.(type) {
case gocui.Key:
err = ct.g.SetKeybinding(view, t, mod, callback)
case tcell.Key:
err = ct.g.SetKeybinding(view, t, 0, mod, callback)
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
}
// SetMousebindingMod adds a binding for a mouse eventdef
func (ct *Cointop) SetMousebindingMod(btn tcell.ButtonMask, mod tcell.ModMask, callback func(g *gocui.Gui, v *gocui.View) error, view string) error {
return ct.g.SetMousebinding(view, btn, mod, callback)
}
// DeleteKeybinding ...
func (ct *Cointop) DeleteKeybinding(shortcutKey string) error {
key, mod := ct.ParseKeys(shortcutKey)
@ -402,13 +378,14 @@ func (ct *Cointop) DeleteKeybinding(shortcutKey string) error {
}
// 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
switch t := key.(type) {
case gocui.Key:
err = ct.g.DeleteKeybinding(view, t, mod)
case tcell.Key:
err = ct.g.DeleteKeybinding(view, t, 0, mod)
case rune:
err = ct.g.DeleteKeybinding(view, t, mod)
err = ct.g.DeleteKeybinding(view, tcell.KeyRune, t, mod)
}
return err
}

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

@ -4,12 +4,14 @@ import (
"sync"
"time"
types "github.com/miguelmota/cointop/pkg/api/types"
"github.com/cointop-sh/cointop/pkg/api/types"
log "github.com/sirupsen/logrus"
)
var coinslock sync.Mutex
var updatecoinsmux sync.Mutex
var (
coinslock sync.Mutex
updatecoinsmux sync.Mutex
)
// UpdateCoins updates coins view
func (ct *Cointop) UpdateCoins() error {
@ -27,9 +29,12 @@ func (ct *Cointop) UpdateCoins() error {
log.Debug("UpdateCoins() soft cache hit")
}
// cache miss
if allCoinsSlugMap == nil {
log.Debug("UpdateCoins() cache miss")
// cache miss or coin struct has been changed from the last time
isCacheMissed := allCoinsSlugMap == nil
currentCoinHash, _ := getStructHash(Coin{})
isCoinStructHashChanged := currentCoinHash != ct.config.CoinStructHash
if isCacheMissed || isCoinStructHashChanged {
log.Debug("UpdateCoins() cache miss or coin struct has changed")
ch := make(chan []types.Coin)
err = ct.api.GetAllCoinData(ct.State.currencyConversion, ch)
if err != nil {
@ -46,6 +51,22 @@ func (ct *Cointop) UpdateCoins() error {
return nil
}
// UpdateCurrentPageCoins updates all the coins in the current page
func (ct *Cointop) UpdateCurrentPageCoins() error {
log.Debugf("UpdateCurrentPageCoins(%d)", len(ct.State.coins))
currentPageCoins := make([]string, len(ct.State.coins))
for i, entry := range ct.State.coins {
currentPageCoins[i] = entry.Name
}
coins, err := ct.api.GetCoinDataBatch(currentPageCoins, ct.State.currencyConversion)
if err != nil {
return err
}
go ct.processCoins(coins)
return nil
}
// ProcessCoinsMap processes coins map
func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) {
log.Debug("ProcessCoinsMap()")
@ -69,7 +90,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
for _, v := range coins {
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
// or coins with low market cap data so we need to put them
// at the end of the list
@ -94,6 +115,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
PercentChange30D: v.PercentChange30D,
PercentChange1Y: v.PercentChange1Y,
LastUpdated: v.LastUpdated,
Slug: v.Slug,
})
if ilast != nil {
last, _ := ilast.(*Coin)
@ -114,7 +136,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
})
if len(ct.State.allCoins) < size {
list := []*Coin{}
var list []*Coin
for _, v := range coins {
k := v.Name
icoin, _ := ct.State.allCoinsSlugMap.Load(k)
@ -146,6 +168,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
c.PercentChange1Y = cm.PercentChange1Y
c.LastUpdated = cm.LastUpdated
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() {
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()
})
}

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

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

@ -309,6 +309,13 @@ func (ct *Cointop) PrevPageTop() error {
return nil
}
// NavigateToFirstPageFirstRow navigates to the first row on the first page
func (ct *Cointop) NavigateToFirstPageFirstRow() error {
log.Debug("TopCoin()")
ct.GoToGlobalIndex(0)
return nil
}
// FirstPage navigates to the first page
func (ct *Cointop) FirstPage() error {
log.Debug("FirstPage()")
@ -409,13 +416,18 @@ func (ct *Cointop) GoToPageRowIndex(idx int) error {
// GoToGlobalIndex navigates to the selected row index of all page rows
func (ct *Cointop) GoToGlobalIndex(idx int) error {
log.Debug("GoToGlobalIndex()")
log.Debugf("GoToGlobalIndex(%d)", idx)
target := ct.State.allCoins[idx]
l := ct.TableRowsLen()
atpage := idx / l
ct.SetPage(atpage)
rowIndex := (idx % l)
ct.HighlightRow(rowIndex)
ct.UpdateTable()
// Look for the coin in the current page
for i, coin := range ct.State.coins {
if coin == target {
ct.HighlightRow(i)
}
}
return nil
}
@ -537,7 +549,7 @@ func (ct *Cointop) TableScrollLeft() error {
return nil
}
// TableScrollRight scrolls the the table to the right
// TableScrollRight scrolls the table to the right
func (ct *Cointop) TableScrollRight() error {
ct.State.tableOffsetX--
maxX := int(math.Min(float64(1-(ct.maxTableWidth-ct.Width())), 0))
@ -548,34 +560,9 @@ func (ct *Cointop) TableScrollRight() error {
return nil
}
// MouseRelease is called on mouse releae event
func (ct *Cointop) MouseRelease() error {
return nil
}
// MouseLeftClick is called on mouse left click event
func (ct *Cointop) MouseLeftClick() error {
return nil
}
// MouseMiddleClick is called on mouse middle click event
func (ct *Cointop) MouseMiddleClick() error {
return nil
}
// MouseRightClick is called on mouse right click event
func (ct *Cointop) MouseRightClick() error {
return ct.OpenLink()
}
// MouseWheelUp is called on mouse wheel up event
func (ct *Cointop) MouseWheelUp() error {
return nil
}
// MouseWheelDown is called on mouse wheel down event
func (ct *Cointop) MouseWheelDown() error {
return nil
return ct.g.SetCursorFromCurrentMouseEvent()
}
// TableRowsLen returns the number of table row entries

@ -12,11 +12,11 @@ import (
"time"
"unicode/utf8"
"github.com/miguelmota/cointop/pkg/asciitable"
"github.com/miguelmota/cointop/pkg/eval"
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table"
"github.com/cointop-sh/cointop/pkg/asciitable"
"github.com/cointop-sh/cointop/pkg/eval"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/table"
log "github.com/sirupsen/logrus"
)
@ -35,6 +35,10 @@ var SupportedPortfolioTableHeaders = []string{
"1y_change",
"percent_holdings",
"last_updated",
"cost_price",
"cost",
"pnl",
"pnl_percent",
}
// DefaultPortfolioTableHeaders are the default portfolio table header columns
@ -49,12 +53,23 @@ var DefaultPortfolioTableHeaders = []string{
"24h_change",
"7d_change",
"percent_holdings",
"cost_price",
"cost",
"pnl",
"pnl_percent",
"last_updated",
}
// HiddenBalanceChars are the characters to show when hidding balances
var HiddenBalanceChars = "********"
var costColumns = map[string]bool{
"cost_price": true,
"cost": true,
"pnl": true,
"pnl_percent": true,
}
// ValidPortfolioTableHeader returns the portfolio table headers
func (ct *Cointop) ValidPortfolioTableHeader(name string) bool {
for _, v := range SupportedPortfolioTableHeaders {
@ -78,8 +93,27 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
t := table.NewTable().SetWidth(maxX)
var rows [][]*table.RowCell
headers := ct.GetPortfolioTableHeaders()
ct.ClearSyncMap(ct.State.tableColumnWidths)
ct.ClearSyncMap(ct.State.tableColumnAlignLeft)
ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
displayCostColumns := false
for _, coin := range ct.State.coins {
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
displayCostColumns = true
break
}
}
if !displayCostColumns {
filtered := make([]string, 0)
for _, header := range headers {
if _, ok := costColumns[header]; !ok {
filtered = append(filtered, header)
}
}
headers = filtered
}
for _, coin := range ct.State.coins {
leftMargin := 1
rightMargin := 1
@ -89,7 +123,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
case "rank":
star := ct.colorscheme.TableRow(" ")
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)))
ct.SetTableColumnWidth(header, 8)
@ -290,7 +324,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
})
case "last_updated":
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.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
@ -301,6 +335,117 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
Color: ct.colorscheme.TableRow,
Text: lastUpdated,
})
case "cost_price":
text := fmt.Sprintf("%s %s", coin.BuyCurrency, ct.FormatPrice(coin.BuyPrice))
if coin.BuyPrice == 0.0 || coin.BuyCurrency == "" {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: ct.colorscheme.TableRow,
Text: text,
})
case "cost":
cost := 0.0
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
cost = costPrice * coin.Holdings
}
}
text := humanize.FixedMonetaryf(cost, 2)
if coin.BuyPrice == 0.0 {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: ct.colorscheme.TableColumnPrice,
Text: text,
})
case "pnl":
text := ""
colorProfit := ct.colorscheme.TableColumnChange
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
profit := (coin.Price - costPrice) * coin.Holdings
text = humanize.FixedMonetaryf(profit, 2)
if profit > 0 {
colorProfit = ct.colorscheme.TableColumnChangeUp
} else if profit < 0 {
colorProfit = ct.colorscheme.TableColumnChangeDown
}
} else {
text = "?"
}
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
colorProfit = ct.colorscheme.TableColumnChange
}
symbolPadding := 1
ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: colorProfit,
Text: text,
})
case "pnl_percent":
profitPercent := 0.0
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
costPrice, err := ct.Convert(coin.BuyCurrency, ct.State.currencyConversion, coin.BuyPrice)
if err == nil {
profitPercent = 100 * (coin.Price/costPrice - 1)
}
}
colorProfit := ct.colorscheme.TableColumnChange
if profitPercent > 0 {
colorProfit = ct.colorscheme.TableColumnChangeUp
} else if profitPercent < 0 {
colorProfit = ct.colorscheme.TableColumnChangeDown
}
text := fmt.Sprintf("%.2f%%", profitPercent)
if coin.BuyPrice == 0.0 {
text = ""
}
if ct.State.hidePortfolioBalances {
text = HiddenBalanceChars
colorProfit = ct.colorscheme.TableColumnChange
}
ct.SetTableColumnWidthFromString(header, text)
ct.SetTableColumnAlignLeft(header, false)
rowCells = append(rowCells,
&table.RowCell{
LeftMargin: leftMargin,
RightMargin: rightMargin,
LeftAlign: false,
Color: colorProfit,
Text: text,
})
}
}
@ -456,8 +601,12 @@ func (ct *Cointop) SetPortfolioHoldings() error {
}
shouldDelete := holdings == 0
// TODO: add fields to form, parse here
buyPrice := 0.0
buyCurrency := ""
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
}
@ -482,7 +631,7 @@ func (ct *Cointop) SetPortfolioHoldings() error {
// PortfolioEntry returns a portfolio entry
func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
//log.Debug("PortfolioEntry()") // too many
// log.Debug("PortfolioEntry()") // too many
if c == nil {
return &PortfolioEntry{}, true
}
@ -492,22 +641,18 @@ func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
var ok bool
key := strings.ToLower(c.Name)
if p, ok = ct.State.portfolio.Entries[key]; !ok {
// NOTE: if not found then try the symbol
key := strings.ToLower(c.Symbol)
if p, ok = ct.State.portfolio.Entries[key]; !ok {
p = &PortfolioEntry{
Coin: c.Name,
Holdings: 0,
}
isNew = true
p = &PortfolioEntry{
Coin: c.Name,
Holdings: 0,
}
isNew = true
}
return p, isNew
}
// 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()")
ic, _ := ct.State.allCoinsSlugMap.Load(strings.ToLower(coin))
c, _ := ic.(*Coin)
@ -515,8 +660,10 @@ func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
if isNew {
key := strings.ToLower(coin)
ct.State.portfolio.Entries[key] = &PortfolioEntry{
Coin: coin,
Holdings: holdings,
Coin: coin,
Holdings: holdings,
BuyPrice: buyPrice,
BuyCurrency: buyCurrency,
}
} else {
p.Holdings = holdings
@ -555,31 +702,21 @@ func (ct *Cointop) PortfolioEntriesCount() int {
// GetPortfolioSlice returns portfolio entries as a slice
func (ct *Cointop) GetPortfolioSlice() []*Coin {
log.Debug("GetPortfolioSlice()")
sliced := []*Coin{}
var sliced []*Coin
if ct.PortfolioEntriesCount() == 0 {
return sliced
}
OUTER:
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
p, isNew := ct.PortfolioEntry(coin)
if isNew {
for _, p := range ct.State.portfolio.Entries {
coinIfc, _ := ct.State.allCoinsSlugMap.Load(p.Coin)
coin, ok := coinIfc.(*Coin)
if !ok {
log.Errorf("Could not find coin %s", p.Coin)
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.BuyPrice = p.BuyPrice
coin.BuyCurrency = p.BuyCurrency
balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance)
if ct.State.currencyConversion == "ETH" || ct.State.currencyConversion == "BTC" {
@ -587,15 +724,10 @@ OUTER:
}
balance, _ = strconv.ParseFloat(balancestr, 64)
coin.Balance = balance
if updateSlice == -1 {
sliced = append(sliced, coin)
} else {
sliced[updateSlice] = coin
}
sliced = append(sliced, coin)
}
sort.Slice(sliced, func(i, j int) bool {
sort.SliceStable(sliced, func(i, j int) bool {
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)
}
ct.Sort(sortBy, sortDesc, holdings, true)
ct.Sort(&sortConstraint{sortBy: sortBy, sortDesc: sortDesc}, holdings, true)
}
if _, ok := outputFormats[format]; !ok {
@ -709,7 +841,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
records := make([][]string, len(holdings))
symbol := ct.CurrencySymbol()
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings"}
headers := []string{"name", "symbol", "price", "holdings", "balance", "24h%", "%holdings", "cost_price", "cost", "pnl", "pnl_percent"}
if len(filterCols) > 0 {
for _, col := range filterCols {
valid := false
@ -806,6 +938,70 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
if hideBalances {
item[i] = HiddenBalanceChars
}
case "cost_price":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
if humanReadable {
item[i] = fmt.Sprintf("%s %s", entry.BuyCurrency, ct.FormatPrice(entry.BuyPrice))
} else {
item[i] = fmt.Sprintf("%s %s", entry.BuyCurrency, strconv.FormatFloat(entry.BuyPrice, 'f', -1, 64))
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "cost":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
cost := costPrice * entry.Holdings
if humanReadable {
item[i] = fmt.Sprintf("%s%s", symbol, humanize.FixedMonetaryf(cost, 2))
} else {
item[i] = strconv.FormatFloat(cost, 'f', -1, 64)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "pnl":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
profit := (entry.Price - costPrice) * entry.Holdings
if humanReadable {
// TODO: if <0 "£-3.71" should be "-£3.71"?
item[i] = fmt.Sprintf("%s%s", symbol, humanize.FixedMonetaryf(profit, 2))
} else {
item[i] = strconv.FormatFloat(profit, 'f', -1, 64)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
case "pnl_percent":
if entry.BuyPrice > 0 && entry.BuyCurrency != "" {
costPrice, err := ct.Convert(entry.BuyCurrency, ct.State.currencyConversion, entry.BuyPrice)
if err == nil {
profitPercent := 100 * (entry.Price/costPrice - 1)
if humanReadable {
item[i] = fmt.Sprintf("%s%%", humanize.Numericf(profitPercent, 2))
} else {
item[i] = fmt.Sprintf("%.2f", profitPercent)
}
} else {
item[i] = "?" // error
}
}
if hideBalances {
item[i] = HiddenBalanceChars
}
}
}
records[i] = item
@ -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) {
continue
}

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

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

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

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

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

@ -2,11 +2,13 @@ package cointop
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
"github.com/miguelmota/cointop/pkg/open"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
"github.com/cointop-sh/cointop/pkg/open"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
@ -15,8 +17,7 @@ type StatusbarView = ui.View
// NewStatusbarView returns a new statusbar view
func NewStatusbarView() *StatusbarView {
var view *StatusbarView = ui.NewView("statusbar")
return view
return ui.NewView("statusbar")
}
// UpdateStatusbar updates the statusbar view
@ -84,3 +85,51 @@ func (ct *Cointop) RefreshRowLink() error {
return nil
}
// StatusbarMouseLeftClick is called on mouse left click event
func (ct *Cointop) StatusbarMouseLeftClick() error {
_, x, _, err := ct.g.GetViewRelativeMousePosition(ct.g.CurrentEvent)
if err != nil {
return err
}
// Parse the statusbar text to identify hotspots and actions
b := make([]byte, 1000)
ct.Views.Statusbar.Rewind()
if n, err := ct.Views.Statusbar.Read(b); err != nil {
return err
} else {
// Find all the "[X]word" substrings, then look for the one that was clicked
matches := regexp.MustCompile(`\[.*?\]\w+`).FindAllIndex(b[:n], -1)
for _, match := range matches {
if x >= match[0] && x <= match[1] {
s := string(b[match[0]:match[1]])
word := strings.Split(s, "]")[1] // matches the \w+ from regex
// Quit/Return Help Chart Range Search Convert Favorites Portfolio Edit(portfolio) Unfavorite
switch word {
case "Help":
ct.ToggleHelp()
case "Range":
// left hand edge of "Range" is Prev, the rest is Next
if x-match[0] < 3 {
ct.PrevChartRange()
} else {
ct.NextChartRange()
}
case "Search":
ct.OpenSearch()
case "Convert":
ct.ToggleConvertMenu()
case "Favorites":
ct.ToggleSelectedView(FavoritesView)
case "Portfolio":
ct.ToggleSelectedView(PortfolioView)
}
}
}
}
return nil
}

@ -3,9 +3,10 @@ package cointop
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/miguelmota/cointop/pkg/ui"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
@ -14,8 +15,7 @@ type TableView = ui.View
// NewTableView returns a new table view
func NewTableView() *TableView {
var view *TableView = ui.NewView("table")
return view
return ui.NewView("table")
}
const dots = "..."
@ -81,16 +81,10 @@ func (ct *Cointop) UpdateTable() error {
} else if ct.IsPortfolioVisible() {
ct.State.coins = ct.GetPortfolioSlice()
} else {
// TODO: maintain state of previous sorting
if ct.State.sortBy == "holdings" {
ct.State.sortBy = "rank"
ct.State.sortDesc = false
}
ct.State.coins = ct.GetTableCoinsSlice()
}
ct.Sort(ct.State.sortBy, ct.State.sortDesc, ct.State.coins, true)
ct.Sort(ct.State.viewSorts[ct.State.selectedView], ct.State.coins, true)
go ct.RefreshTable()
return nil
}
@ -98,7 +92,7 @@ func (ct *Cointop) UpdateTable() error {
// GetTableCoinsSlice returns a slice of the table rows
func (ct *Cointop) GetTableCoinsSlice() []*Coin {
log.Debug("GetTableCoinsSlice()")
sliced := []*Coin{}
var sliced []*Coin
start := ct.State.page * ct.State.perPage
end := start + ct.State.perPage
allCoins := ct.AllCoins()
@ -198,7 +192,27 @@ func (ct *Cointop) RowLink() string {
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
@ -225,6 +239,20 @@ func (ct *Cointop) RowLinkShort() string {
return ""
}
func (ct *Cointop) GetAltCoinLink(coin *Coin) string {
if ct.State.altCoinLink == "" {
return ct.api.CoinLink(coin.Slug)
}
url := ct.State.altCoinLink
url = strings.Replace(url, "{{ID}}", coin.ID, -1)
url = strings.Replace(url, "{{NAME}}", coin.Name, -1)
url = strings.Replace(url, "{{RANK}}", strconv.Itoa(coin.Rank), -1)
url = strings.Replace(url, "{{SLUG}}", coin.Slug, -1)
url = strings.Replace(url, "{{SYMBOL}}", coin.Symbol, -1)
return url
}
// ToggleTableFullscreen toggles the table fullscreen mode
func (ct *Cointop) ToggleTableFullscreen() error {
log.Debug("ToggleTableFullscreen()")
@ -266,6 +294,11 @@ func (ct *Cointop) ToggleTableFullscreen() error {
func (ct *Cointop) SetSelectedView(viewName string) {
ct.State.lastSelectedView = ct.State.selectedView
ct.State.selectedView = viewName
// init sort constraint for the view if it hasn't been seen before
if _, found := ct.State.viewSorts[viewName]; !found {
ct.State.viewSorts[viewName] = &sortConstraint{DefaultSortBy, false}
}
}
// ToggleSelectedView toggles between current table view and last selected table view

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

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

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

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

@ -5,6 +5,6 @@ draft: false
---
# 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
```
Note: depending on your system, this may not be the correct location. The "colors" directory needs to go in the same place as your config.toml file.
Then edit your config `~/.config/cointop/config.toml` and set the colorscheme you want to use:
```toml

@ -46,7 +46,7 @@ refresh_rate = 60
[shortcuts]
"$" = "last_page"
0 = "first_page"
0 = "move_to_first_page_first_row"
1 = "sort_column_1h_change"
2 = "sort_column_24h_change"
7 = "sort_column_7d_change"
@ -133,6 +133,7 @@ Action|Description
----|------|
`first_chart_range`|Select first chart date range (e.g. 24H)
`first_page`|Go to first page
`move_to_first_page_first_row`|Go to first row on the first page
`enlarge_chart`|Increase chart height
`help`|Show help
`hide_currency_convert_menu`|Hide currency convert menu

@ -9,11 +9,11 @@ Pull requests are welcome!
For contributions please create a new branch and submit a pull request for review.
Huge thanks to all the [contributors](https://github.com/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
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:

@ -24,7 +24,7 @@ make deps
Installing from source
```bash
make brew/build
make brew-build
```
## Flatpak
@ -44,7 +44,7 @@ sudo flatpak install flathub org.freedesktop.Sdk.Extension.golang
Building flatpak package
```bash
make flatpak/build
make flatpak-build
```
## Copr
@ -52,18 +52,18 @@ make flatpak/build
Install dependencies
```bash
make copr/install/cli
make rpm/install/deps
make rpm/dirs
make copr-install-cli
make rpm-install-deps
make rpm-dirs
```
Build package
```bash
make rpm/cp/specs
make rpm/download
make rpm/build
make copr/build
make rpm-cp-specs
make rpm-download
make rpm-build
make copr-build
```
## Snap
@ -71,5 +71,13 @@ make copr/build
Building snap
```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?
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?
@ -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.
## 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?
@ -89,7 +95,7 @@ draft: false
## I'm no longer seeing any data!
Run cointop with the `--clean` flag to delete the cache. If you're still not seeing any data, then please [submit an issue](https://github.com/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?
@ -118,7 +124,7 @@ draft: false
## 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.
@ -132,6 +138,9 @@ draft: false
## How do I search?
The default key to open search is <kbd>/</kbd>. Type the search query after the `/` in the field and hit <kbd>Enter</kbd>.
Each search starts from the current cursor position. To search for the same term again, hit <kbd>/</kbd> then <kbd>Enter</kbd>.
The default behaviour will start to search by symbol first, then it will continues searching by name if there is no result. To search by only symbol, type the search query after `/s:`. To search by only name, type the search query after `/n:`.
## How do I exit search?
@ -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.
## How do I include buy/cost price in my portfolio?
Currently there is no UI for this. If you want to include the cost of your coins in the Portfolio screen, you will need to edit your config.toml
Each coin consists of four values: coin name, coin amount, cost-price, cost-currency.
For example, the following configuration includes 100 ALGO at USD1.95 each; and 0.1 BTC at AUD50100.83 each.
```toml
holdings = [["Algorand", "100", "1.95", "USD"], ["Bitcoin", "0.1", "50100.83", "AUD"]]
```
With this configuration, four new columns are useful:
- `cost_price` the price and currency that the coins were purchased at
- `cost` the cost (in the current currency) of the coins
- `pnl` the PNL of the coins (current value vs original cost)
- `pnl_percent` the PNL of the coins as a fraction of the original cost
With the holdings above, and the currency set to GBP (British Pounds) cointop will look something like this:
![portfolio profit and loss](https://user-images.githubusercontent.com/122371/138361142-8e1f32b5-ca24-471d-a628-06968f07c65f.png)
## How do I hide my portfolio balances (private mode)?
You can run cointop with the `--hide-portfolio-balances` flag to hide portfolio balances or use the keyboard shortcut <kbd>Ctrl</kbd>+<kbd>space</kbd> on the portfolio page to toggle hide/show.
@ -199,11 +231,11 @@ draft: false
LANG=en_US.utf8 TERM=xterm-256color cointop
```
If you're on Windows (PowerShell, Command Prompt, or WSL), please see the [wiki](https://github.com/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?
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.
@ -228,8 +260,8 @@ draft: false
Here's how to build the executable and run it:
```powershell
> md C:\Users\Josem\go\src\github.com\miguelmota -ea 0
> git clone https://github.com/miguelmota/cointop.git
> md C:\Users\Josem\go\src\github.com\cointop-sh -ea 0
> git clone https://github.com/cointop-sh/cointop.git
> go build -o cointop.exe main.go
> 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`
## How can I use K (thousand), M (million), B (billion), T (trillion) suffixes for shorter numbers?
There is a setting at the top-level of the configuration file called `compact_notation=true` which changes the marketbar values `market cap`, `volume` and `portfolio total value`.
The same setting can be applied at in the `[table]` section to impact the `24h_volume`, `market_cap`, `total_supply`, `available_supply` columns in the main coin view; and in the `[favorites]` section to change the same columns. The setting also changes the column names to be shorter.
## How can use a different config file other than the default?
Run cointop with the `--config` flag, eg `cointop --config="/path/to/config.toml"`, to use the specified file as the config.
@ -373,7 +411,7 @@ draft: false
## I can only view the first page, why isn't the pagination is working?
Sometimes the coin APIs will make updates and break things. If you see this problem please [submit an issue](https://github.com/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?
@ -479,10 +517,39 @@ draft: false
cointop server -k ~/.ssh/id_rsa [...]
```
## How do I fix the error `no matching host key type found. Their offer: ssh-rsa` when trying to SSH?
Use the following flag when connecting to the SSH server:
```bash
ssh -oHostKeyAlgorithms=+ssh-rsa cointop.sh
```
You can also add this config to the `~/.ssh/config` file so you don't have to use the flag every time:
```
Host cointop.sh
HostName cointop.sh
HostKeyAlgorithms=+ssh-rsa
```
## Why doesn't the version number work when I install with `go get`?
The version number is read from the git tag during the build process but this requires the `GO111MODULE` environment variable to be set in order for Go to read the build information:
```bash
GO111MODULE=on go get github.com/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.
## 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
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:
@ -25,14 +25,14 @@ cointop
## 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
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
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)
@ -69,7 +69,7 @@ Note: snaps don't work in Windows WSL. See this [issue thread](https://forum.sna
cointop is available as a [copr](https://copr.fedorainfracloud.org/coprs/miguelmota/cointop/) package.
First, enable the respository
First, enable the repository
```bash
sudo dnf copr enable miguelmota/cointop -y
@ -143,11 +143,11 @@ nix-env -iA nixpkgs.cointop
## AppImage (Linux)
You can download the AppImage from the [releases](https://github.com/miguelmota/cointop/releases) page.
You can download the AppImage from the [releases](https://github.com/cointop-sh/cointop/releases) page.
```bash
VERSION=$(curl --silent "https://api.github.com/repos/miguelmota/cointop/releases/latest" | grep -Po --color=never '"tag_name": ".\K.*?(?=")')
URL="https://github.com/miguelmota/cointop/releases/download/v$VERSION/cointop-v$VERSION.glibc2.32-x86_64.AppImage"
VERSION=$(curl --silent "https://api.github.com/repos/cointop-sh/cointop/releases/latest" | grep -Po --color=never '"tag_name": ".\K.*?(?=")')
URL="https://github.com/cointop-sh/cointop/releases/download/v$VERSION/cointop-v$VERSION.glibc2.32-x86_64.AppImage"
wget $URL
```
@ -176,10 +176,10 @@ sudo pkg install cointop
Install [Go](https://golang.org/doc/install) and [git](https://git-scm.com/download/win), then:
```powershell
go get -u github.com/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
@ -197,4 +197,4 @@ docker run -v ~/.cache/cointop:/root/.config/cointop -it cointop/cointop
## Binaries
You can find pre-built binaries on the [releases](https://github.com/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.
```bash
go get -u github.com/miguelmota/cointop
go get -u github.com/cointop-sh/cointop
```
## 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
@ -6,39 +6,41 @@ require (
github.com/BurntSushi/toml v0.4.1
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/antonmedv/expr v1.9.0
github.com/creack/pty v1.1.15
github.com/fatih/color v1.12.0
github.com/creack/pty v1.1.17
github.com/fatih/color v1.13.0
github.com/gdamore/tcell/v2 v2.4.0
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1
github.com/gliderlabs/ssh v0.3.3
github.com/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/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/olekukonko/tablewriter v0.0.5
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
golang.org/x/text v0.3.7
)
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/gdamore/encoding v1.0.0 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/gopherjs/gopherjs v0.0.0-20211111143520-d0d5ecc1a356 // indirect
github.com/gopherjs/gopherwasm v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
)

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.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
fyne.io/fyne v1.4.2/go.mod h1:xL4c3WmpE/Tvz5CEm5vqsaizU/EeOCm9DYlL2GtTSiM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/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/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.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/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
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-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/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -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/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.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fyne-io/mobile v0.1.2-0.20201127155338-06aeb98410cc/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 h1:Xh9mvwEmhbdXlRSsgn+N0zj/NqnKvpeqL08oKDHln2s=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.0.3/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.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -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.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-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-20211111143520-d0d5ecc1a356 h1:d3wWSjdOuGrMHa8+Tvw3z9EGPzATpzVq1BmGK3+IyeU=
github.com/gopherjs/gopherjs v0.0.0-20211111143520-d0d5ecc1a356/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI=
github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@ -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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/jeandeaual/go-locale v0.0.0-20211014152413-b809787f45c8 h1:t3zg0eJ2qUP6yqqcwicCBqqaQVKs3ul4n27CAcyh0aw=
github.com/jeandeaual/go-locale v0.0.0-20211014152413-b809787f45c8/go.mod h1:3/uOR/xyUPi69BwdDezaGEixFZOspXUmKujIOg2r8JM=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@ -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/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/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.1.7/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.8/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.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/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/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/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/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=
@ -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/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@ -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/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
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.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -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/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/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/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/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.27/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-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-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -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/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -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-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-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-20200905004654-be1d3432aa8f/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
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-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.3.0/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -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-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -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-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=

@ -1,6 +1,6 @@
#!/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"
if [[ $(uname) == 'Darwin' ]]; then
@ -9,7 +9,7 @@ fi
(
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
sudo mv cointop /usr/local/bin/cointop

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

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

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

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

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

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

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

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

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

@ -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) {
input = strings.TrimSpace(input)
if input == "" {
@ -43,3 +43,23 @@ func EvaluateExpressionToFloat64(input string, env interface{}) (float64, error)
}
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 ...
var DefaultCacheDir = "/tmp"
var DefaultCacheDir = ":PREFERRED_CACHE_HOME:/cointop"
// FileCache ...
type FileCache struct {

@ -4,22 +4,26 @@
package gocui
import "errors"
import (
"errors"
"github.com/gdamore/tcell/v2"
)
const maxInt = int(^uint(0) >> 1)
// Editor interface must be satisfied by gocui editors.
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
// Editors. If f is a function with the appropriate signature, EditorFunc(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)
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)
}
@ -27,27 +31,27 @@ func (f EditorFunc) Edit(v *View, key Key, ch rune, mod Modifier) {
var DefaultEditor Editor = EditorFunc(simpleEditor)
// 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 {
case ch != 0 && mod == 0:
case key == tcell.KeyRune && ch != 0 && (mod == tcell.ModShift || mod == tcell.ModNone):
v.EditWrite(ch)
case key == KeySpace:
case key == ' ':
v.EditWrite(' ')
case key == KeyBackspace || key == KeyBackspace2:
case key == tcell.KeyBackspace || key == tcell.KeyBackspace2:
v.EditDelete(true)
case key == KeyDelete:
case key == tcell.KeyDelete:
v.EditDelete(false)
case key == KeyInsert:
case key == tcell.KeyInsert:
v.Overwrite = !v.Overwrite
case key == KeyEnter:
case key == tcell.KeyEnter:
v.EditNewLine()
case key == KeyArrowDown:
case key == tcell.KeyDown:
v.MoveCursor(0, 1, false)
case key == KeyArrowUp:
case key == tcell.KeyUp:
v.MoveCursor(0, -1, false)
case key == KeyArrowLeft:
case key == tcell.KeyLeft:
v.MoveCursor(-1, 0, false)
case key == KeyArrowRight:
case key == tcell.KeyRight:
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:])
}
v.lines[y][x] = cell{
fgColor: v.FgColor,
bgColor: v.BgColor,
chr: ch,
style: v.Style,
chr: ch,
}
return nil

@ -7,14 +7,16 @@ package gocui
import (
"errors"
"strconv"
"github.com/gdamore/tcell/v2"
)
type escapeInterpreter struct {
state escapeState
curch rune
csiParam []string
curFgColor, curBgColor Attribute
mode OutputMode
state escapeState
curch rune
csiParam []string
curStyle tcell.Style
// mode OutputMode
}
type escapeState int
@ -54,12 +56,11 @@ func (ei *escapeInterpreter) runes() []rune {
// newEscapeInterpreter returns an escapeInterpreter that will be able to parse
// terminal escape sequences.
func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
func newEscapeInterpreter() *escapeInterpreter {
ei := &escapeInterpreter{
state: stateNone,
curFgColor: ColorDefault,
curBgColor: ColorDefault,
mode: mode,
state: stateNone,
curStyle: tcell.StyleDefault,
// mode: mode,
}
return ei
}
@ -67,8 +68,7 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
// reset sets the escapeInterpreter in initial state.
func (ei *escapeInterpreter) reset() {
ei.state = stateNone
ei.curFgColor = ColorDefault
ei.curBgColor = ColorDefault
ei.curStyle = tcell.StyleDefault
ei.csiParam = nil
}
@ -120,12 +120,13 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
return true, nil
case ch == 'm':
var err error
switch ei.mode {
case OutputNormal:
err = ei.outputNormal()
case Output256:
err = ei.output256()
}
err = ei.parseEscapeParams()
// switch ei.mode {
// case OutputNormal:
// err = ei.outputNormal()
// case Output256:
// err = ei.output256()
// }
if err != nil {
return false, errCSIParseError
}
@ -140,90 +141,72 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
return false, nil
}
// outputNormal provides 8 different colors:
// black, red, green, yellow, blue, magenta, cyan, white
func (ei *escapeInterpreter) outputNormal() error {
for _, param := range ei.csiParam {
p, err := strconv.Atoi(param)
if err != nil {
// parseEscapeParams interprets an escape sequence as a style modifier
// allows you to leverage the 256-colors terminal mode:
// 0x01 - 0x08: the 8 colors as in OutputNormal (black, red, green, yellow, blue, magenta, cyan, white)
// 0x09 - 0x10: Color* | AttrBold
// 0x11 - 0xe8: 216 different colors
// 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
}
}
// consume elements of params until done
pos := 0
for ok := true; ok; ok = pos < len(params) {
p := params[pos]
switch {
case p >= 30 && p <= 37:
ei.curFgColor = Attribute(p - 30 + 1)
ei.curStyle = ei.curStyle.Foreground(tcell.PaletteColor(p - 30))
case p == 39:
ei.curFgColor = ColorDefault
ei.curStyle = ei.curStyle.Foreground(tcell.ColorDefault)
case p >= 40 && p <= 47:
ei.curBgColor = Attribute(p - 40 + 1)
ei.curStyle = ei.curStyle.Background(tcell.PaletteColor(p - 40))
case p == 49:
ei.curBgColor = ColorDefault
ei.curStyle = ei.curStyle.Background(tcell.ColorDefault)
case p == 1:
ei.curFgColor |= AttrBold
ei.curStyle = ei.curStyle.Bold(true)
case p == 4:
ei.curFgColor |= AttrUnderline
ei.curStyle = ei.curStyle.Underline(true)
case p == 7:
ei.curFgColor |= AttrReverse
ei.curStyle = ei.curStyle.Reverse(true)
case p == 0:
ei.curFgColor = ColorDefault
ei.curBgColor = ColorDefault
}
}
return nil
}
// output256 allows you to leverage the 256-colors terminal mode:
// 0x01 - 0x08: the 8 colors as in OutputNormal
// 0x09 - 0x10: Color* | AttrBold
// 0x11 - 0xe8: 216 different colors
// 0xe9 - 0x1ff: 24 different shades of grey
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
ei.curStyle = tcell.StyleDefault
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
if mode == 5 { // 256 color
x = tcell.PaletteColor(params[pos+2] + 1)
pos += 2 // two additional (5+index)
} else if mode == 2 { // 24-bit
x = tcell.NewRGBColor(int32(params[pos+2]), int32(params[pos+3]), int32(params[pos+4]))
pos += 4 // four additional (2+r/g/b)
} else {
return errCSIParseError // invalid mode
}
switch {
case p == 1:
ei.curFgColor |= AttrBold
case p == 4:
ei.curFgColor |= AttrUnderline
case p == 7:
ei.curFgColor |= AttrReverse
if p == 38 {
ei.curStyle = ei.curStyle.Foreground(x)
} else {
ei.curStyle = ei.curStyle.Background(x)
}
}
case 48:
ei.curBgColor = Attribute(color + 1)
default:
return errCSIParseError
}
pos += 1 // move along 1 by default
}
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 (
"errors"
"github.com/miguelmota/termbox-go"
"github.com/gdamore/tcell/v2"
)
var (
@ -19,35 +19,36 @@ var (
)
// OutputMode represents the terminal's output mode (8 or 256 colors).
type OutputMode termbox.OutputMode
// type OutputMode termbox.OutputMode // TODO: die
const (
// OutputNormal provides 8-colors terminal mode.
OutputNormal = OutputMode(termbox.OutputNormal)
// const ( // TODO: die
// // OutputNormal provides 8-colors terminal mode.
// OutputNormal = OutputMode(termbox.OutputNormal)
// Output256 provides 256-colors terminal mode.
Output256 = OutputMode(termbox.Output256)
)
// // Output256 provides 256-colors terminal mode.
// Output256 = OutputMode(termbox.Output256)
// )
// Gui represents the whole User Interface, including the views, layouts
// and keybindings.
// and eventBindings.
type Gui struct {
tbEvents chan termbox.Event
userEvents chan userEvent
views []*View
currentView *View
managers []Manager
keybindings []*keybinding
maxX, maxY int
outputMode OutputMode
tbEvents chan tcell.Event
userEvents chan userEvent
views []*View
currentView *View
managers []Manager
eventBindings []*eventBinding
maxX, maxY int
// outputMode OutputMode // TODO: die
screen tcell.Screen
// BgColor and FgColor allow to configure the background and foreground
// colors of the GUI.
BgColor, FgColor Attribute
Style tcell.Style
// SelBgColor and SelFgColor allow to configure the background and
// 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
// 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
// interface. Using ASCII is more portable.
ASCII bool
// The current event while in the handlers.
CurrentEvent tcell.Event
}
// NewGui returns a new Gui object with a given output mode.
func NewGui(mode OutputMode) (*Gui, error) {
if err := termbox.Init(); err != nil {
return nil, err
}
// func NewGui(mode OutputMode) (*Gui, error) {
func NewGui() (*Gui, error) {
g := &Gui{}
g.outputMode = mode
termbox.SetOutputMode(termbox.OutputMode(mode))
// outMode = OutputNormal
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.maxX, g.maxY = termbox.Size()
g.maxX, g.maxY = g.screen.Size()
g.BgColor, g.FgColor = ColorDefault, ColorDefault
g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault
g.Style = tcell.StyleDefault
g.SelStyle = tcell.StyleDefault
return g, nil
}
@ -93,7 +104,7 @@ func NewGui(mode OutputMode) (*Gui, error) {
// Close finalizes the library. It should be called after a successful
// initialization and when gocui is not needed anymore.
func (g *Gui) Close() {
termbox.Close()
g.screen.Fini()
}
// Size returns the terminal's size.
@ -101,26 +112,48 @@ func (g *Gui) Size() (x, y int) {
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
// corner of the terminal. It checks if the position is valid and applies
// 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 {
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
}
// Rune returns the rune contained in the cell at the given position.
// It checks if the position is valid.
func (g *Gui) Rune(x, y int) (rune, error) {
if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
return ' ', errors.New("invalid point")
}
c := termbox.CellBuffer()[y*g.maxX+x]
return c.Ch, nil
}
// func (g *Gui) Rune(x, y int) (rune, error) {
// if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
// return ' ', errors.New("invalid point")
// }
// c := termbox.CellBuffer()[y*g.maxX+x]
// return c.Ch, nil
// }
// 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
@ -144,9 +177,9 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int) (*View, error) {
return v, nil
}
v := newView(name, x0, y0, x1, y1, g.outputMode)
v.BgColor, v.FgColor = g.BgColor, g.FgColor
v.SelBgColor, v.SelFgColor = g.SelBgColor, g.SelFgColor
v := newView(name, x0, y0, x1, y1, g)
v.Style = g.Style
v.SelStyle = g.SelStyle
g.views = append(g.views, v)
return v, ErrUnknownView
}
@ -243,60 +276,84 @@ func (g *Gui) CurrentView() *View {
return g.currentView
}
// SetKeybinding creates a new keybinding. If viewname equals to ""
// (empty string) then the keybinding will apply to all views. key must
// SetKeybinding creates a new eventBinding. If viewname equals to ""
// (empty string) then the eventBinding will apply to all views. key must
// be a rune or a Key.
func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error {
var kb *keybinding
// TODO: split into key/mouse bindings?
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)
if err != nil {
return err
}
kb = newKeybinding(viewname, k, ch, mod, handler)
g.keybindings = append(g.keybindings, kb)
func (g *Gui) SetMousebinding(viewname string, btn tcell.ButtonMask, mod tcell.ModMask, handler func(*Gui, *View) error) error {
kb := newMouseBinding(viewname, btn, mod, handler)
g.eventBindings = append(g.eventBindings, kb)
return nil
}
// DeleteKeybinding deletes a keybinding.
func (g *Gui) DeleteKeybinding(viewname string, key interface{}, mod Modifier) error {
k, ch, err := getKey(key)
if err != nil {
return err
}
// DeleteKeybinding deletes a eventBinding.
func (g *Gui) DeleteKeybinding(viewname string, key tcell.Key, ch rune, mod tcell.ModMask) error {
// k, ch, err := getKey(key)
// if err != nil {
// return err
// }
for i, kb := range g.keybindings {
if kb.viewName == viewname && kb.ch == ch && kb.key == k && kb.mod == mod {
g.keybindings = append(g.keybindings[:i], g.keybindings[i+1:]...)
return nil
for i, kb := range g.eventBindings {
if kbe, ok := kb.ev.(*tcell.EventKey); ok {
if kb.viewName == viewname && kbe.Rune() == ch && kbe.Key() == key && kbe.Modifiers() == mod {
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) {
var s []*keybinding
for _, kb := range g.keybindings {
var s []*eventBinding
for _, kb := range g.eventBindings {
if kb.viewName != viewname {
s = append(s, kb)
}
}
g.keybindings = s
g.eventBindings = s
}
// getKey takes an empty interface with a key and returns the corresponding
// typed Key or rune.
func getKey(key interface{}) (Key, rune, error) {
switch t := key.(type) {
case Key:
return t, 0, nil
case rune:
return 0, t, nil
default:
return 0, 0, errors.New("unknown type")
}
}
// func getKey(key interface{}) (tcell.Key, rune, error) {
// switch t := key.(type) {
// case Key:
// return t, 0, nil
// case rune:
// return 0, t, nil
// default:
// return 0, 0, errors.New("unknown type")
// }
// }
// userEvent represents an event triggered by the user.
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
// keybindings.
// eventBindings.
func (g *Gui) SetManager(managers ...Manager) {
g.managers = managers
g.currentView = 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
// keybindings.
// eventBindings.
func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
g.SetManager(ManagerFunc(manager))
}
@ -351,18 +408,14 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
func (g *Gui) MainLoop() error {
go func() {
for {
g.tbEvents <- termbox.PollEvent()
g.tbEvents <- g.screen.PollEvent()
}
}()
inputMode := termbox.InputAlt
if g.InputEsc {
inputMode = termbox.InputEsc
}
if g.Mouse {
inputMode |= termbox.InputMouse
g.screen.EnableMouse()
}
termbox.SetInputMode(inputMode)
// s.EnablePaste()
if err := g.flush(); err != nil {
return err
@ -370,7 +423,7 @@ func (g *Gui) MainLoop() error {
for {
select {
case ev := <-g.tbEvents:
if err := g.handleEvent(&ev); err != nil {
if err := g.handleEvent(ev); err != nil {
return err
}
case ev := <-g.userEvents:
@ -392,7 +445,7 @@ func (g *Gui) consumeevents() error {
for {
select {
case ev := <-g.tbEvents:
if err := g.handleEvent(&ev); err != nil {
if err := g.handleEvent(ev); err != nil {
return err
}
case ev := <-g.userEvents:
@ -407,12 +460,12 @@ func (g *Gui) consumeevents() error {
// handleEvent handles an event, based on its type (key-press, error,
// etc.)
func (g *Gui) handleEvent(ev *termbox.Event) error {
switch ev.Type {
case termbox.EventKey, termbox.EventMouse:
return g.onKey(ev)
case termbox.EventError:
return ev.Err
func (g *Gui) handleEvent(ev tcell.Event) error {
switch tev := ev.(type) {
case *tcell.EventMouse, *tcell.EventKey:
return g.onEvent(tev)
case *tcell.EventError:
return errors.New(tev.Error())
default:
return nil
}
@ -420,9 +473,15 @@ func (g *Gui) handleEvent(ev *termbox.Event) error {
// flush updates the gui, re-drawing frames and buffers.
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 maxX != g.maxX || maxY != g.maxY {
for _, v := range g.views {
@ -438,23 +497,20 @@ func (g *Gui) flush() error {
}
for _, v := range g.views {
if v.Frame {
var fgColor, bgColor Attribute
// var fgColor, bgColor Attribute
st := g.Style
if g.Highlight && v == g.currentView {
fgColor = g.SelFgColor
bgColor = g.SelBgColor
} else {
fgColor = g.FgColor
bgColor = g.BgColor
st = g.SelStyle
}
if err := g.drawFrameEdges(v, fgColor, bgColor); err != nil {
if err := g.drawFrameEdges(v, st); err != nil {
return err
}
if err := g.drawFrameCorners(v, fgColor, bgColor); err != nil {
if err := g.drawFrameCorners(v, st); err != nil {
return err
}
if v.Title != "" {
if err := g.drawTitle(v, fgColor, bgColor); err != nil {
if err := g.drawTitle(v, st); err != nil {
return err
}
}
@ -463,12 +519,12 @@ func (g *Gui) flush() error {
return err
}
}
termbox.Flush()
g.screen.Show()
return nil
}
// 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 := '─', '│'
if g.ASCII {
runeH, runeV = '-', '|'
@ -479,12 +535,12 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
continue
}
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
}
}
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
}
}
@ -494,12 +550,12 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
continue
}
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
}
}
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
}
}
@ -508,7 +564,7 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
}
// 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 := '┌', '┐', '└', '┘'
if g.ASCII {
runeTL, runeTR, runeBL, runeBR = '+', '+', '+', '+'
@ -521,7 +577,7 @@ func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
for _, c := range corners {
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
}
}
@ -530,7 +586,7 @@ func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
}
// 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 {
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 {
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
}
}
@ -568,13 +624,13 @@ func (g *Gui) draw(v *View) error {
gMaxX, gMaxY := g.Size()
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
termbox.SetCursor(cx, cy)
g.screen.ShowCursor(cx, cy)
} else {
termbox.HideCursor()
g.screen.ShowCursor(-1, -1) // HideCursor
}
}
} else {
termbox.HideCursor()
g.screen.ShowCursor(-1, -1) // HideCursor
}
v.clearRunes()
@ -584,13 +640,13 @@ func (g *Gui) draw(v *View) error {
return nil
}
// onKey manages key-press events. A keybinding handler is called when
// a key-press or mouse event satisfies a configured keybinding. Furthermore,
// onEvent manages key/mouse events. A eventBinding handler is called when
// a key-press or mouse event satisfies a configured eventBinding. Furthermore,
// currentView's internal buffer is modified if currentView.Editable is true.
func (g *Gui) onKey(ev *termbox.Event) error {
switch ev.Type {
case termbox.EventKey:
matched, err := g.execKeybindings(g.currentView, ev)
func (g *Gui) onEvent(ev tcell.Event) error {
switch tev := ev.(type) {
case *tcell.EventKey:
matched, err := g.execEventBindings(g.currentView, ev)
if err != nil {
return err
}
@ -598,34 +654,58 @@ func (g *Gui) onKey(ev *termbox.Event) error {
break
}
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:
mx, my := ev.MouseX, ev.MouseY
v, err := g.ViewByPosition(mx, my)
case *tcell.EventMouse:
v, _, _, err := g.GetViewRelativeMousePosition(tev)
if err != nil {
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
}
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
}
// 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.
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
for _, kb := range g.keybindings {
for _, kb := range g.eventBindings {
if kb.handler == nil {
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 {
return false, err
}

@ -10,7 +10,7 @@ import (
"io"
"strings"
"github.com/miguelmota/termbox-go"
"github.com/gdamore/tcell/v2"
)
// 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
// colors of the View.
BgColor, FgColor Attribute
Style tcell.Style
// SelBgColor and SelFgColor are used to configure the background and
// 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
// buffer at the cursor position.
Editable bool
// 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.
Editor Editor
@ -71,6 +71,9 @@ type View struct {
// If Mask is true, the View will display the mask instead of the real
// content
Mask rune
// The gui that owns this view
g *Gui
}
type viewLine struct {
@ -79,8 +82,9 @@ type viewLine struct {
}
type cell struct {
chr rune
bgColor, fgColor Attribute
chr rune
// bgColor, fgColor Attribute
style tcell.Style
}
type lineType []cell
@ -95,7 +99,7 @@ func (l lineType) String() string {
}
// 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{
name: name,
x0: x0,
@ -105,7 +109,8 @@ func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
Frame: true,
Editor: DefaultEditor,
tainted: true,
ei: newEscapeInterpreter(mode),
ei: newEscapeInterpreter(),
g: g,
}
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
// specified colors, taking into account if the cell must be highlighted. Also,
// 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()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
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 {
fgColor = v.FgColor
bgColor = v.BgColor
style = v.Style
ch = v.Mask
} else if v.Highlight && ry == rcy {
fgColor = v.SelFgColor
bgColor = v.SelBgColor
style = v.SelStyle
}
termbox.SetCell(v.x0+x+1, v.y0+y+1, ch,
termbox.Attribute(fgColor), termbox.Attribute(bgColor))
v.g.SetRune(v.x0+x+1, v.y0+y+1, ch, style)
return nil
}
@ -240,9 +242,8 @@ func (v *View) parseInput(ch rune) []cell {
if err != nil {
for _, r := range v.ei.runes() {
c := cell{
fgColor: v.FgColor,
bgColor: v.BgColor,
chr: r,
style: v.Style,
chr: r,
}
cells = append(cells, c)
}
@ -252,9 +253,8 @@ func (v *View) parseInput(ch rune) []cell {
return nil
}
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ch,
style: v.ei.curStyle,
chr: ch,
}
cells = append(cells, c)
}
@ -341,16 +341,16 @@ func (v *View) draw() error {
break
}
fgColor := c.fgColor
if fgColor == ColorDefault {
fgColor = v.FgColor
st := c.style
fgColor, bgColor, _ := c.style.Decompose()
vfgColor, vbgColor, _ := v.Style.Decompose()
if fgColor == tcell.ColorDefault {
st = st.Foreground(vfgColor)
}
bgColor := c.bgColor
if bgColor == ColorDefault {
bgColor = v.BgColor
if bgColor == tcell.ColorDefault {
st = st.Background(vbgColor)
}
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
if err := v.setRune(x, y, c.chr, st); err != nil {
return err
}
x++
@ -402,8 +402,7 @@ func (v *View) clearRunes() {
maxX, maxY := v.Size()
for x := 0; x < maxX; x++ {
for y := 0; y < maxY; y++ {
termbox.SetCell(v.x0+x+1, v.y0+y+1, ' ',
termbox.Attribute(v.FgColor), termbox.Attribute(v.BgColor))
v.g.SetRune(v.x0+x+1, v.y0+y+1, ' ', v.Style)
}
}
}
@ -493,7 +492,7 @@ func (v *View) Word(x, y int) (string, error) {
} else {
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

@ -2,17 +2,23 @@ package humanize
import (
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
"github.com/goodsign/monday"
"github.com/jeandeaual/go-locale"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
var cachedSystemLocale = ""
// Numericf produces a string from of the given number with give fixed precision
// in base 10 with thousands separators after every three orders of magnitude
// using 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"
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
// 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"
func Monetaryf(value float64, precision int) string {
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.
// 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.
func f(value float64, precision int, envvar string, fixed bool) string {
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)
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
import (
"fmt"
"testing"
"time"
)
// TestMonetary tests monetary formatting
@ -10,3 +12,83 @@ func TestMonetary(t *testing.T) {
t.FailNow()
}
}
func TestScale(t *testing.T) {
scaleTests := map[float64]string{
5.54 * 1e12: "5.5T",
4.44 * 1e9: "4.4B",
3.34 * 1e6: "3.3M",
2.24 * 1e3: "2.2K",
1.1: "1.1",
0.06: "0.1",
0.04: "0.0",
-5.54 * 1e12: "-5.5T",
}
for value, expected := range scaleTests {
volScale, volSuffix := Scale(value)
result := fmt.Sprintf("%.1f%s", volScale, volSuffix)
if result != expected {
t.Fatalf("Expected %f to scale to '%s' but got '%s'\n", value, expected, result)
}
}
}
func TestScaleNumeric(t *testing.T) {
scaleTests := map[float64]string{
5.54 * 1e12: "5.5T",
4.44 * 1e9: "4.4B",
3.34 * 1e6: "3.3M",
2.24 * 1e3: "2.2K",
1.1: "1.1",
0.0611: "0.06",
-5.5432 * 1e12: "-5.5T",
}
for value, expected := range scaleTests {
result := ScaleNumericf(value, 2)
if result != expected {
t.Fatalf("Expected %f to scale to '%s' but got '%s'\n", value, expected, result)
}
}
}
func TestFormatTime(t *testing.T) {
testData := map[string]map[string]string{
"en_GB": {
"Monday 2 January 2006": "Wednesday 12 March 2014",
"Jan 2006": "Mar 2014",
"02 Jan 2006": "12 Mar 2014",
"02/01/2006": "12/03/2014",
},
"en_US": {
"Monday 2 January 2006": "Wednesday 12 March 2014",
"Jan 2006": "Mar 2014",
"02 Jan 2006": "12 Mar 2014",
"02/01/2006": "12/03/2014", // ??
},
"fr_FR": {
"Monday 2 January 2006": "mercredi 12 mars 2014",
"Jan 2006": "mars 2014",
"02 Jan 2006": "12 mars 2014",
"02/01/2006": "12/03/2014",
},
"de_DE": {
"Monday 2 January 2006": "Mittwoch 12 März 2014",
"Jan 2006": "Mär 2014",
"02 Jan 2006": "12 Mär 2014",
"02/01/2006": "12/03/2014",
},
}
testTime := time.Date(2014, 3, 12, 0, 0, 0, 0, time.Local)
for locale, tests := range testData {
for layout, result := range tests {
s := formatTimeExplicit(testTime, layout, locale)
if s != result {
t.Fatalf("Expected layout '%s' in locale %s to render '%s' but got '%s'", layout, locale, result, s)
}
}
}
}

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

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

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

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

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

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

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

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

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

@ -27,15 +27,15 @@ import (
*/
type MBarChart struct {
Block
BarColor [NumberofColors]Attribute
BarColor [NumberOfColors]Attribute
TextColor Attribute
NumColor [NumberofColors]Attribute
Data [NumberofColors][]int
NumColor [NumberOfColors]Attribute
Data [NumberOfColors][]int
DataLabels []string
BarWidth int
BarGap int
labels [][]rune
dataNum [NumberofColors][][]rune
dataNum [NumberOfColors][][]rune
numBar int
scale float64
max int
@ -102,11 +102,11 @@ func (bc *MBarChart) layout() {
bc.BarColor[i] = ColorBlack
} else {
bc.BarColor[i] = bc.BarColor[i-1] + 1
if bc.BarColor[i] > NumberofColors {
if bc.BarColor[i] > NumberOfColors {
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
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.
type Bufferer interface {
Buffer() Buffer
}
/*
// Init initializes termui library. This function should be called before any others.
// After initialization, the library must be finalized by 'Close' function.
func Init() error {
@ -188,3 +171,4 @@ func Render(bs ...Bufferer) {
//go func() { renderJobs <- bs }()
renderJobs <- bs
}
*/

@ -39,8 +39,8 @@ type Sparklines struct {
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
// Add appends a given Sparkline to s *Sparklines.
func (s *Sparklines) Add(sl Sparkline) {
s.Lines = append(s.Lines, sl)
func (sl *Sparklines) Add(line Sparkline) {
sl.Lines = append(sl.Lines, line)
}
// 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, ".")
for i := range ns {
nn := strings.Join(ns[i:len(ns)], ".")
nn := strings.Join(ns[i:], ".")
a, ok = ColorMap[nn]
if ok {
break
@ -121,7 +121,7 @@ func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
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 {
within := func(n int) int {
if n < 0 {

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

Loading…
Cancel
Save