Merge branch 'cointop-sh:master' into feature/better-resample

pull/210/head
Simon Roberts 3 years ago committed by GitHub
commit f37499a381
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: /.*/

1
.gitignore vendored

@ -57,6 +57,7 @@ wasm
.appimage_workspace
todo.txt
.vscode
docs/public
deploy_docs.sh

@ -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,7 +2,7 @@ FROM golang:alpine AS build
ARG VERSION
RUN wget \
--output-document "/cointop-$VERSION.tar.gz" \
"https://github.com/cointop-sh/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" \

@ -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"
@ -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,6 +253,12 @@ 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:

@ -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
```
@ -55,24 +57,19 @@ Documentation has been moved to [docs.cointop.sh](https://docs.cointop.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
## Contributing

@ -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,
}
}

@ -315,10 +315,15 @@ func (ct *Cointop) PortfolioChart() error {
// Scale Portfolio Balances to hide value
if ct.State.hidePortfolioBalances {
var lastPrice = data[len(data)-1]
if lastPrice > 0.0 {
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 / lastPrice
data[i] = 100 * price / scalePrice
}
}
}

@ -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
}

@ -13,10 +13,11 @@ import (
"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"
"github.com/miguelmota/gocui"
log "github.com/sirupsen/logrus"
)
@ -47,9 +48,7 @@ type State struct {
defaultView string
defaultChartRange string
maxChartWidth int
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions.
favoritesBySymbol map[string]bool
columnLookup []string
favorites map[string]bool
favoritesTableColumns []string
@ -72,6 +71,7 @@ type State struct {
refreshRate time.Duration
running bool
searchFieldVisible bool
lastSearchQuery string
selectedCoin *Coin
selectedChartRange string
selectedView string
@ -94,6 +94,7 @@ type State struct {
tableCompactNotation bool
favoritesCompactNotation bool
portfolioCompactNotation bool
enableMouse bool
}
// Cointop cointop
@ -127,8 +128,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
@ -189,6 +192,9 @@ var DefaultChartRange = "1Y"
// DefaultCompactNotation ...
var DefaultCompactNotation = false
// DefaultEnableMouse ...
var DefaultEnableMouse = true
// DefaultMaxChartWidth ...
var DefaultMaxChartWidth = 175
@ -202,7 +208,7 @@ var DefaultSortBy = "rank"
var DefaultPerPage = uint(100)
// DefaultMaxPages ...
var DefaultMaxPages = uint(35)
var DefaultMaxPages = uint(10)
// DefaultColorscheme ...
var DefaultColorscheme = "cointop"
@ -213,9 +219,6 @@ var DefaultConfigFilepath = pathutil.NormalizePath(":PREFERRED_CONFIG_HOME:/coin
// DefaultCacheDir ...
var DefaultCacheDir = filecache.DefaultCacheDir
// DefaultColorsDir ...
var DefaultColorsDir = fmt.Sprintf("%s/colors", DefaultConfigFilepath)
// DefaultFavoriteChar ...
var DefaultFavoriteChar = "*"
@ -259,14 +262,12 @@ 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,
maxChartWidth: DefaultMaxChartWidth,
// 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,
@ -300,6 +301,7 @@ func NewCointop(config *Config) (*Cointop, error) {
SoundEnabled: true,
},
compactNotation: DefaultCompactNotation,
enableMouse: DefaultEnableMouse,
tableCompactNotation: DefaultCompactNotation,
favoritesCompactNotation: DefaultCompactNotation,
portfolioCompactNotation: DefaultCompactNotation,
@ -454,21 +456,6 @@ func NewCointop(config *Config) (*Cointop, error) {
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 {
@ -500,14 +487,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 {

@ -2,12 +2,11 @@ package cointop
import (
"fmt"
"strconv"
"strings"
"sync"
fcolor "github.com/fatih/color"
"github.com/miguelmota/gocui"
"github.com/tomnomnom/xtermcolor"
"github.com/gdamore/tcell/v2"
)
// TODO: fix hex color support
@ -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,9 +370,10 @@ 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
}
@ -346,39 +387,3 @@ func (c *Colorscheme) ToBoldAttr(v bool) (fcolor.Attribute, bool) {
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

@ -49,6 +49,7 @@ type ConfigFileConfig struct {
RefreshRate interface{} `toml:"refresh_rate"`
CacheDir interface{} `toml:"cache_dir"`
CompactNotation interface{} `toml:"compact_notation"`
EnableMouse interface{} `toml:"enable_mouse"`
Table map[string]interface{} `toml:"table"`
Chart map[string]interface{} `toml:"chart"`
}
@ -72,6 +73,7 @@ func (ct *Cointop) SetupConfig() error {
ct.loadRefreshRateFromConfig,
ct.loadCacheDirFromConfig,
ct.loadCompactNotationFromConfig,
ct.loadEnableMouseFromConfig,
ct.loadPriceAlertsFromConfig,
ct.loadPortfolioFromConfig,
}
@ -214,10 +216,7 @@ 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,
"columns": ct.State.favoritesTableColumns,
"character": ct.State.favoriteChar,
@ -230,9 +229,12 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
if !ok || entry.Coin == "" {
continue
}
amount := strconv.FormatFloat(entry.Holdings, 'f', -1, 64)
coinName := entry.Coin
tuple := []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 {
@ -292,6 +294,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
Table: tableMapIfc,
Chart: chartMapIfc,
CompactNotation: ct.State.compactNotation,
EnableMouse: ct.State.enableMouse,
}
var b bytes.Buffer
@ -369,14 +372,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
}
@ -479,6 +512,16 @@ func (ct *Cointop) loadCompactNotationFromConfig() error {
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
}
// LoadAPIChoiceFromConfig loads API choices from config file to struct
func (ct *Cointop) loadAPIChoiceFromConfig() error {
log.Debug("loadAPIKeysFromConfig()")
@ -509,13 +552,6 @@ func (ct *Cointop) loadFavoritesFromConfig() error {
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 {
@ -564,33 +600,7 @@ 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 {
@ -600,12 +610,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
}

@ -3,16 +3,18 @@ package cointop
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
"github.com/cointop-sh/cointop/pkg/color"
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",
@ -176,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)
@ -232,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 {
@ -241,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
}
@ -301,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,13 +1,22 @@
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"
debugFile := os.Getenv("DEBUG_FILE")
if debugFile != "" {
filename = pathutil.NormalizePath(debugFile)
if filename != debugFile && os.Getenv("DEBUG") != "" {
fmt.Printf("Writing debug log to %s\n", filename)
}
}
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
panic(err)

@ -36,7 +36,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 +85,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",
}
}

@ -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
})

@ -2,168 +2,101 @@ 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, "+")
mod := tcell.ModNone
// translate legacy and special names for keys
keyName := strings.TrimSpace(strings.Replace(s, "+", "-", -1))
split := strings.Split(keyName, "-")
if len(split) > 1 {
m := strings.ToLower(strings.TrimSpace(split[0]))
k := strings.ToLower(strings.TrimSpace(split[1]))
k := strings.TrimSpace(split[1])
k = keyMap(k)
if k == " " {
k = "Space" // fix mod+space
}
if m == "alt" {
mod = gocui.ModAlt
s = k
mod = tcell.ModAlt
keyName = 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 = '\\'
// let the lookup handle it
keyName = m + "-" + k
} else {
keyName = m + "-" + k
}
// TODO: other mods?
} else {
keyName = keyMap(keyName)
}
// 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 descriptio '%s' to key", s)
}
return key, mod
}
@ -218,6 +151,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 +258,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 +282,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(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 +374,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

@ -8,8 +8,10 @@ import (
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 {
@ -46,6 +48,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()")
@ -94,6 +112,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)
@ -146,6 +165,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
c.PercentChange1Y = cm.PercentChange1Y
c.LastUpdated = cm.LastUpdated
c.Favorite = cm.Favorite
c.Slug = cm.Slug
}
}

@ -6,8 +6,9 @@ import (
"strings"
"time"
fcolor "github.com/fatih/color"
"github.com/cointop-sh/cointop/pkg/api/types"
"github.com/cointop-sh/cointop/pkg/color"
"github.com/cointop-sh/cointop/pkg/humanize"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/ui"
@ -28,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
@ -78,10 +81,9 @@ func (ct *Cointop) UpdateMarketbar() error {
chartInfo := ""
if !ct.State.hideChart {
chartInfo = fmt.Sprintf(
"[ Chart: %s %s %s ] ",
"[ Chart: %s %s ] ",
charttitle,
timeframe,
ct.State.currencyConversion,
)
}
@ -92,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,
)
@ -142,10 +145,9 @@ func (ct *Cointop) UpdateMarketbar() error {
chartInfo := ""
if !ct.State.hideChart {
chartInfo = fmt.Sprintf(
"[ Chart: %s %s %s] ",
"[ Chart: %s %s] ",
ct.colorscheme.MarketBarLabelActive(chartname),
timeframe,
ct.State.currencyConversion,
)
}
@ -166,8 +168,9 @@ func (ct *Cointop) UpdateMarketbar() error {
}
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,
ct.State.currencyConversion,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), marketCapStr),
separator1,
fmt.Sprintf("%s%s", ct.CurrencySymbol(), volumeStr),

@ -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
}
@ -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

@ -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 {
@ -80,6 +95,25 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
headers := ct.GetPortfolioTableHeaders()
ct.ClearSyncMap(&ct.State.tableColumnWidths)
ct.ClearSyncMap(&ct.State.tableColumnAlignLeft)
displayCostColumns := false
for _, coin := range ct.State.coins {
if coin.BuyPrice > 0 && coin.BuyCurrency != "" {
displayCostColumns = true
break
}
}
if !displayCostColumns {
filtered := make([]string, 0)
for _, header := range headers {
if _, ok := costColumns[header]; !ok {
filtered = append(filtered, header)
}
}
headers = filtered
}
for _, coin := range ct.State.coins {
leftMargin := 1
rightMargin := 1
@ -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
}
@ -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
@ -560,26 +707,16 @@ func (ct *Cointop) GetPortfolioSlice() []*Coin {
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
})
@ -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

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

@ -66,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)
@ -78,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 {
@ -112,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,7 +4,7 @@ import (
"sort"
"sync"
"github.com/miguelmota/gocui"
"github.com/cointop-sh/cointop/pkg/gocui"
log "github.com/sirupsen/logrus"
)
@ -23,7 +23,7 @@ func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bo
if len(list) < 2 {
return
}
sort.Slice(list[:], func(i, j int) bool {
sort.SliceStable(list[:], func(i, j int) bool {
if ct.State.sortDesc {
i, j = j, i
}
@ -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
}

@ -2,6 +2,8 @@ package cointop
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
"github.com/cointop-sh/cointop/pkg/open"
@ -83,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
}

@ -197,7 +197,16 @@ 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)
}
// RowLinkShort returns a shortened version of the row url link

@ -126,6 +126,26 @@ var HeaderColumns = map[string]*HeaderColumn{
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)
@ -159,7 +179,7 @@ func (ct *Cointop) GetActiveTableHeaders() []string {
return cols
}
// GetActiveTableHeaders returns the list of active table headers
// IsActiveTableCompactNotation returns whether the current view is using compact-notation
func (ct *Cointop) IsActiveTableCompactNotation() bool {
var compact bool
switch ct.State.selectedView {
@ -184,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 {
@ -211,7 +232,7 @@ func (ct *Cointop) UpdateTableHeader() error {
}
leftAlign := ct.GetTableColumnAlignLeft(col)
switch col {
case "price", "balance":
case "price", "balance", "pnl", "cost":
label = fmt.Sprintf("%s%s", ct.CurrencySymbol(), label)
}
if leftAlign {
@ -227,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, ""))
})
@ -243,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)
@ -265,6 +313,9 @@ func (ct *Cointop) SetTableColumnWidth(header string, width int) {
prev = prevIfc.(int)
} else {
hc := HeaderColumns[header]
if hc == nil {
log.Warnf("SetTableColumnWidth(%s) not found", header)
}
prev = utf8.RuneCountInString(ct.GetLabel(hc)) + 1
switch header {
case "price", "balance":

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

@ -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

@ -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

@ -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:
```toml
define_base03 = "#002b36"
menu_header_fg = "$base03"
```
## Why don't colorschemes support RGB or hex colors?
## How do I make the background color transparent?
Some of the cointop underlying rendering libraries don't support true colors. See [issue](https://github.com/nsf/termbox/issues/37).
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?
@ -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.
@ -492,3 +524,16 @@ draft: false
```bash
GO111MODULE=on go get github.com/cointop-sh/cointop
```
## How can I get more information when something is going wrong?
Cointop creates a logfile at `/tmp/cointop.log`. Normally nothing is written to this, but if you set the environment variable
`DEBUG=1` cointop will write a lot of output describing its operation. Furthermore, if you also set `DEBUG_HTTP=1` it will
emit lots about every HTTP request that cointop makes to coingecko (backend). Developers may ask for this information
to help diagnose any problems you may experience.
```bash
DEBUG=1 DEBUG_HTTP=1 cointop
```
If you set environment variable `DEBUG_FILE` you can explicitly provide a logfile location, rather than `/tmp/cointop.log`

@ -8,21 +8,18 @@ require (
github.com/antonmedv/expr v1.9.0
github.com/creack/pty v1.1.16
github.com/fatih/color v1.13.0
github.com/gdamore/tcell/v2 v2.4.0
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1
github.com/gliderlabs/ssh v0.3.3
github.com/goodsign/monday v1.0.0
github.com/jeandeaual/go-locale v0.0.0-20210323163322-5cf4ff553a8d
github.com/maruel/panicparse v1.6.1
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-20210921155107-089bfa567519
golang.org/x/text v0.3.7
)
@ -30,11 +27,13 @@ require (
require (
github.com/anaskhan96/soup v1.0.1 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/gopherjs/gopherwasm v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
@ -43,4 +42,5 @@ require (
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-20210630005230-0f9fa26af87c // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
)

@ -89,8 +89,12 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF
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=
@ -151,7 +155,6 @@ 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -217,14 +220,11 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
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 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/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.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -232,19 +232,14 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
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=
@ -317,8 +312,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=
@ -475,7 +468,6 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/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=
@ -495,8 +487,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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=

@ -12,6 +12,7 @@ import (
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"
)
@ -33,6 +34,7 @@ type Service struct {
maxResultsPerPage uint
maxPages uint
cacheMap sync.Map
cachedRates *types.ExchangeRatesItem
}
// NewCoinGecko new service
@ -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 excange 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))
@ -160,25 +201,10 @@ func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int6
}
// This API does not appear to support vs_currency and only returns USD, so use ExchangeRates to convert
rate := 1.0
if convertTo != "usd" {
rates, err := s.client.ExchangeRates()
if err != nil {
return ret, err
}
if rates == nil {
return ret, fmt.Errorf("expected rates, received nil")
}
// Combined rate is USD->BTC->other
btcRate, found := (*rates)[convertTo]
if !found {
return ret, fmt.Errorf("unsupported currency conversion: %s", convertTo)
}
usdRate, found := (*rates)["usd"]
if !found {
return ret, fmt.Errorf("unsupported currency conversion: usd")
}
rate = btcRate.Value / usdRate.Value
// 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
}
var marketCapUSD [][]float64
@ -241,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",
@ -436,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),
})
}
}

@ -77,6 +77,7 @@ func (s *Service) getPaginatedCoinData(convert string, offset int) ([]apitypes.C
}
ret = append(ret, apitypes.Coin{
// TODO: Fix ID
ID: util.FormatID(v.Name),
Name: util.FormatName(v.Name),
Symbol: util.FormatSymbol(v.Symbol),
@ -90,6 +91,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
@ -297,7 +299,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 +333,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 +430,11 @@ func getChartInterval(start, end int64) string {
}
return interval
}
// GetExchangeRate gets the current excange 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)
}

@ -13,7 +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)
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"
"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,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()
)

@ -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

@ -34,6 +34,11 @@ func Monetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", false)
}
// FixedMonetaryf produces a fixed-precision monetary-value string. See Monetaryf.
func FixedMonetaryf(value float64, precision int) string {
return f(value, precision, "LC_MONETARY", true)
}
// borrowed from go-locale/util.go
func splitLocale(locale string) (string, string) {
// Remove the encoding, if present

@ -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
@ -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()

@ -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)
@ -225,7 +229,7 @@ func findMatch(mux map[string]func(Event), path string) string {
// 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

@ -8,8 +8,6 @@ import (
"regexp"
"strings"
tm "github.com/miguelmota/termbox-go"
rw "github.com/mattn/go-runewidth"
)
@ -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)
}

@ -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
}
*/

@ -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

@ -45,7 +45,11 @@ func LinearInterpolateTimeSeriesData(data [][]float64, start float64, end float6
idx := sort.Search(l, func(i int) bool { return data[i][0] >= pos })
var val float64
if idx == 0 {
val = math.NaN() // off the left
if data[0][0] == pos {
val = data[0][1] // exactly left
} else {
val = math.NaN() // off the left
}
} else if idx == l {
val = math.NaN() // off the right
} else {

@ -1,7 +1,8 @@
package ui
import (
"github.com/miguelmota/gocui"
"github.com/cointop-sh/cointop/pkg/gocui"
"github.com/gdamore/tcell/v2"
)
// UI is the UI view struct
@ -11,7 +12,7 @@ type UI struct {
// NewUI returns a new UI instance
func NewUI() (*UI, error) {
g, err := gocui.NewGui(gocui.Output256)
g, err := gocui.NewGui()
if err != nil {
return nil, err
}
@ -26,24 +27,19 @@ func (ui *UI) GetGocui() *gocui.Gui {
return ui.g
}
// SetFgColor sets the foreground color
func (ui *UI) SetFgColor(fgColor gocui.Attribute) {
ui.g.FgColor = fgColor
}
// SetBgColor sets the background color
func (ui *UI) SetBgColor(bgColor gocui.Attribute) {
ui.g.BgColor = bgColor
// SetStyle sets the default style
func (ui *UI) SetStyle(style tcell.Style) {
ui.g.Style = style
}
// SetInputEsc enables the escape key
func (ui *UI) SetInputEsc(enabled bool) {
ui.g.InputEsc = true
ui.g.InputEsc = enabled
}
// SetMouse enables the mouse
func (ui *UI) SetMouse(enabled bool) {
ui.g.Mouse = true
ui.g.Mouse = enabled
}
// SetCursor enables the input field cursor
@ -53,7 +49,7 @@ func (ui *UI) SetCursor(enabled bool) {
// SetHighlight enables the highlight active state
func (ui *UI) SetHighlight(enabled bool) {
ui.g.Highlight = true
ui.g.Highlight = enabled
}
// SetManagerFunc sets the function to call for rendering UI

@ -3,7 +3,8 @@ package ui
import (
"fmt"
"github.com/miguelmota/gocui"
"github.com/cointop-sh/cointop/pkg/gocui"
"github.com/gdamore/tcell/v2"
)
// IView is the view interface
@ -213,31 +214,17 @@ func (view *View) SetWrap(enabled bool) error {
return nil
}
// SetFgColor sets the foreground color
func (view *View) SetFgColor(color gocui.Attribute) {
// SetStyle sets the text style for the view
func (view *View) SetStyle(style tcell.Style) {
if view.HasBacking() {
view.backing.FgColor = color
view.backing.Style = style
}
}
// SetBgColor sets the background color
func (view *View) SetBgColor(color gocui.Attribute) {
// SetSelStyle sets the selection text style for the view
func (view *View) SetSelStyle(style tcell.Style) {
if view.HasBacking() {
view.backing.BgColor = color
}
}
// SetSelFgColor sets the foreground color for selection
func (view *View) SetSelFgColor(color gocui.Attribute) {
if view.HasBacking() {
view.backing.SelFgColor = color
}
}
// SetSelBgColor sets the background color for selection
func (view *View) SetSelBgColor(color gocui.Attribute) {
if view.HasBacking() {
view.backing.SelBgColor = color
view.backing.SelStyle = style
}
}

@ -0,0 +1,13 @@
version: 1.0.{build}
clone_folder: c:\gopath\src\github.com\gdamore\encoding
environment:
GOPATH: c:\gopath
build_script:
- go version
- go env
- SET PATH=%LOCALAPPDATA%\atom\bin;%GOPATH%\bin;%PATH%
- go get -t ./...
- go build
- go install ./...
test_script:
- go test ./...

@ -0,0 +1,7 @@
language: go
go:
- 1.9.x
- 1.10.x
- 1.11.x
- tip

@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -178,7 +179,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@ -186,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Marc-Antoine Ruel
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

@ -0,0 +1,19 @@
## encoding
[![Linux Status](https://img.shields.io/travis/gdamore/encoding.svg?label=linux)](https://travis-ci.org/gdamore/encoding)
[![Windows Status](https://img.shields.io/appveyor/ci/gdamore/encoding.svg?label=windows)](https://ci.appveyor.com/project/gdamore/encoding)
[![Apache License](https://img.shields.io/badge/license-APACHE2-blue.svg)](https://github.com/gdamore/encoding/blob/master/LICENSE)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/gdamore/encoding)
[![Go Report Card](http://goreportcard.com/badge/gdamore/encoding)](http://goreportcard.com/report/gdamore/encoding)
Package encoding provides a number of encodings that are missing from the
standard Go [encoding]("https://godoc.org/golang.org/x/text/encoding") package.
We hope that we can contribute these to the standard Go library someday. It
turns out that some of these are useful for dealing with I/O streams coming
from non-UTF friendly sources.
The UTF8 Encoder is also useful for situations where valid UTF-8 might be
carried in streams that contain non-valid UTF; in particular I use it for
helping me cope with terminals that embed escape sequences in otherwise
valid UTF-8.

@ -0,0 +1,36 @@
// Copyright 2015 Garrett D'Amore
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encoding
import (
"golang.org/x/text/encoding"
)
// ASCII represents the 7-bit US-ASCII scheme. It decodes directly to
// UTF-8 without change, as all ASCII values are legal UTF-8.
// Unicode values less than 128 (i.e. 7 bits) map 1:1 with ASCII.
// It encodes runes outside of that to 0x1A, the ASCII substitution character.
var ASCII encoding.Encoding
func init() {
amap := make(map[byte]rune)
for i := 128; i <= 255; i++ {
amap[byte(i)] = RuneError
}
cm := &Charmap{Map: amap}
cm.Init()
ASCII = cm
}

@ -0,0 +1,196 @@
// Copyright 2015 Garrett D'Amore
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encoding
import (
"sync"
"unicode/utf8"
"golang.org/x/text/encoding"
"golang.org/x/text/transform"
)
const (
// RuneError is an alias for the UTF-8 replacement rune, '\uFFFD'.
RuneError = '\uFFFD'
// RuneSelf is the rune below which UTF-8 and the Unicode values are
// identical. Its also the limit for ASCII.
RuneSelf = 0x80
// ASCIISub is the ASCII substitution character.
ASCIISub = '\x1a'
)
// Charmap is a structure for setting up encodings for 8-bit character sets,
// for transforming between UTF8 and that other character set. It has some
// ideas borrowed from golang.org/x/text/encoding/charmap, but it uses a
// different implementation. This implementation uses maps, and supports
// user-defined maps.
//
// We do assume that a character map has a reasonable substitution character,
// and that valid encodings are stable (exactly a 1:1 map) and stateless
// (that is there is no shift character or anything like that.) Hence this
// approach will not work for many East Asian character sets.
//
// Measurement shows little or no measurable difference in the performance of
// the two approaches. The difference was down to a couple of nsec/op, and
// no consistent pattern as to which ran faster. With the conversion to
// UTF-8 the code takes about 25 nsec/op. The conversion in the reverse
// direction takes about 100 nsec/op. (The larger cost for conversion
// from UTF-8 is most likely due to the need to convert the UTF-8 byte stream
// to a rune before conversion.
//
type Charmap struct {
transform.NopResetter
bytes map[rune]byte
runes [256][]byte
once sync.Once
// The map between bytes and runes. To indicate that a specific
// byte value is invalid for a charcter set, use the rune
// utf8.RuneError. Values that are absent from this map will
// be assumed to have the identity mapping -- that is the default
// is to assume ISO8859-1, where all 8-bit characters have the same
// numeric value as their Unicode runes. (Not to be confused with
// the UTF-8 values, which *will* be different for non-ASCII runes.)
//
// If no values less than RuneSelf are changed (or have non-identity
// mappings), then the character set is assumed to be an ASCII
// superset, and certain assumptions and optimizations become
// available for ASCII bytes.
Map map[byte]rune
// The ReplacementChar is the byte value to use for substitution.
// It should normally be ASCIISub for ASCII encodings. This may be
// unset (left to zero) for mappings that are strictly ASCII supersets.
// In that case ASCIISub will be assumed instead.
ReplacementChar byte
}
type cmapDecoder struct {
transform.NopResetter
runes [256][]byte
}
type cmapEncoder struct {
transform.NopResetter
bytes map[rune]byte
replace byte
}
// Init initializes internal values of a character map. This should
// be done early, to minimize the cost of allocation of transforms
// later. It is not strictly necessary however, as the allocation
// functions will arrange to call it if it has not already been done.
func (c *Charmap) Init() {
c.once.Do(c.initialize)
}
func (c *Charmap) initialize() {
c.bytes = make(map[rune]byte)
ascii := true
for i := 0; i < 256; i++ {
r, ok := c.Map[byte(i)]
if !ok {
r = rune(i)
}
if r < 128 && r != rune(i) {
ascii = false
}
if r != RuneError {
c.bytes[r] = byte(i)
}
utf := make([]byte, utf8.RuneLen(r))
utf8.EncodeRune(utf, r)
c.runes[i] = utf
}
if ascii && c.ReplacementChar == '\x00' {
c.ReplacementChar = ASCIISub
}
}
// NewDecoder returns a Decoder the converts from the 8-bit
// character set to UTF-8. Unknown mappings, if any, are mapped
// to '\uFFFD'.
func (c *Charmap) NewDecoder() *encoding.Decoder {
c.Init()
return &encoding.Decoder{Transformer: &cmapDecoder{runes: c.runes}}
}
// NewEncoder returns a Transformer that converts from UTF8 to the
// 8-bit character set. Unknown mappings are mapped to 0x1A.
func (c *Charmap) NewEncoder() *encoding.Encoder {
c.Init()
return &encoding.Encoder{
Transformer: &cmapEncoder{
bytes: c.bytes,
replace: c.ReplacementChar,
},
}
}
func (d *cmapDecoder) Transform(dst, src []byte, atEOF bool) (int, int, error) {
var e error
var ndst, nsrc int
for _, c := range src {
b := d.runes[c]
l := len(b)
if ndst+l > len(dst) {
e = transform.ErrShortDst
break
}
for i := 0; i < l; i++ {
dst[ndst] = b[i]
ndst++
}
nsrc++
}
return ndst, nsrc, e
}
func (d *cmapEncoder) Transform(dst, src []byte, atEOF bool) (int, int, error) {
var e error
var ndst, nsrc int
for nsrc < len(src) {
if ndst >= len(dst) {
e = transform.ErrShortDst
break
}
r, sz := utf8.DecodeRune(src[nsrc:])
if r == utf8.RuneError && sz == 1 {
// If its inconclusive due to insufficient data in
// in the source, report it
if !atEOF && !utf8.FullRune(src[nsrc:]) {
e = transform.ErrShortSrc
break
}
}
if c, ok := d.bytes[r]; ok {
dst[ndst] = c
} else {
dst[ndst] = d.replace
}
nsrc += sz
ndst++
}
return ndst, nsrc, e
}

@ -0,0 +1,17 @@
// Copyright 2015 Garrett D'Amore
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package encoding provides a few of the encoding structures that are
// missing from the Go x/text/encoding tree.
package encoding

@ -0,0 +1,273 @@
// Copyright 2015 Garrett D'Amore
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encoding
import (
"golang.org/x/text/encoding"
)
// EBCDIC represents the 8-bit EBCDIC scheme, found in some mainframe
// environments. If you don't know what this is, consider yourself lucky.
var EBCDIC encoding.Encoding
func init() {
cm := &Charmap{
ReplacementChar: '\x3f',
Map: map[byte]rune{
// 0x00-0x03 match
0x04: RuneError,
0x05: '\t',
0x06: RuneError,
0x07: '\x7f',
0x08: RuneError,
0x09: RuneError,
0x0a: RuneError,
// 0x0b-0x13 match
0x14: RuneError,
0x15: '\x85', // Not in any ISO code
0x16: '\x08',
0x17: RuneError,
// 0x18-0x19 match
0x1a: RuneError,
0x1b: RuneError,
// 0x1c-0x1f match
0x20: RuneError,
0x21: RuneError,
0x22: RuneError,
0x23: RuneError,
0x24: RuneError,
0x25: '\n',
0x26: '\x17',
0x27: '\x1b',
0x28: RuneError,
0x29: RuneError,
0x2a: RuneError,
0x2b: RuneError,
0x2c: RuneError,
0x2d: '\x05',
0x2e: '\x06',
0x2f: '\x07',
0x30: RuneError,
0x31: RuneError,
0x32: '\x16',
0x33: RuneError,
0x34: RuneError,
0x35: RuneError,
0x36: RuneError,
0x37: '\x04',
0x38: RuneError,
0x39: RuneError,
0x3a: RuneError,
0x3b: RuneError,
0x3c: '\x14',
0x3d: '\x15',
0x3e: RuneError,
0x3f: '\x1a', // also replacement char
0x40: ' ',
0x41: '\xa0',
0x42: RuneError,
0x43: RuneError,
0x44: RuneError,
0x45: RuneError,
0x46: RuneError,
0x47: RuneError,
0x48: RuneError,
0x49: RuneError,
0x4a: RuneError,
0x4b: '.',
0x4c: '<',
0x4d: '(',
0x4e: '+',
0x4f: '|',
0x50: '&',
0x51: RuneError,
0x52: RuneError,
0x53: RuneError,
0x54: RuneError,
0x55: RuneError,
0x56: RuneError,
0x57: RuneError,
0x58: RuneError,
0x59: RuneError,
0x5a: '!',
0x5b: '$',
0x5c: '*',
0x5d: ')',
0x5e: ';',
0x5f: '¬',
0x60: '-',
0x61: '/',
0x62: RuneError,
0x63: RuneError,
0x64: RuneError,
0x65: RuneError,
0x66: RuneError,
0x67: RuneError,
0x68: RuneError,
0x69: RuneError,
0x6a: '¦',
0x6b: ',',
0x6c: '%',
0x6d: '_',
0x6e: '>',
0x6f: '?',
0x70: RuneError,
0x71: RuneError,
0x72: RuneError,
0x73: RuneError,
0x74: RuneError,
0x75: RuneError,
0x76: RuneError,
0x77: RuneError,
0x78: RuneError,
0x79: '`',
0x7a: ':',
0x7b: '#',
0x7c: '@',
0x7d: '\'',
0x7e: '=',
0x7f: '"',
0x80: RuneError,
0x81: 'a',
0x82: 'b',
0x83: 'c',
0x84: 'd',
0x85: 'e',
0x86: 'f',
0x87: 'g',
0x88: 'h',
0x89: 'i',
0x8a: RuneError,
0x8b: RuneError,
0x8c: RuneError,
0x8d: RuneError,
0x8e: RuneError,
0x8f: '±',
0x90: RuneError,
0x91: 'j',
0x92: 'k',
0x93: 'l',
0x94: 'm',
0x95: 'n',
0x96: 'o',
0x97: 'p',
0x98: 'q',
0x99: 'r',
0x9a: RuneError,
0x9b: RuneError,
0x9c: RuneError,
0x9d: RuneError,
0x9e: RuneError,
0x9f: RuneError,
0xa0: RuneError,
0xa1: '~',
0xa2: 's',
0xa3: 't',
0xa4: 'u',
0xa5: 'v',
0xa6: 'w',
0xa7: 'x',
0xa8: 'y',
0xa9: 'z',
0xaa: RuneError,
0xab: RuneError,
0xac: RuneError,
0xad: RuneError,
0xae: RuneError,
0xaf: RuneError,
0xb0: '^',
0xb1: RuneError,
0xb2: RuneError,
0xb3: RuneError,
0xb4: RuneError,
0xb5: RuneError,
0xb6: RuneError,
0xb7: RuneError,
0xb8: RuneError,
0xb9: RuneError,
0xba: '[',
0xbb: ']',
0xbc: RuneError,
0xbd: RuneError,
0xbe: RuneError,
0xbf: RuneError,
0xc0: '{',
0xc1: 'A',
0xc2: 'B',
0xc3: 'C',
0xc4: 'D',
0xc5: 'E',
0xc6: 'F',
0xc7: 'G',
0xc8: 'H',
0xc9: 'I',
0xca: '\xad', // NB: soft hyphen
0xcb: RuneError,
0xcc: RuneError,
0xcd: RuneError,
0xce: RuneError,
0xcf: RuneError,
0xd0: '}',
0xd1: 'J',
0xd2: 'K',
0xd3: 'L',
0xd4: 'M',
0xd5: 'N',
0xd6: 'O',
0xd7: 'P',
0xd8: 'Q',
0xd9: 'R',
0xda: RuneError,
0xdb: RuneError,
0xdc: RuneError,
0xdd: RuneError,
0xde: RuneError,
0xdf: RuneError,
0xe0: '\\',
0xe1: '\u2007', // Non-breaking space
0xe2: 'S',
0xe3: 'T',
0xe4: 'U',
0xe5: 'V',
0xe6: 'W',
0xe7: 'X',
0xe8: 'Y',
0xe9: 'Z',
0xea: RuneError,
0xeb: RuneError,
0xec: RuneError,
0xed: RuneError,
0xee: RuneError,
0xef: RuneError,
0xf0: '0',
0xf1: '1',
0xf2: '2',
0xf3: '3',
0xf4: '4',
0xf5: '5',
0xf6: '6',
0xf7: '7',
0xf8: '8',
0xf9: '9',
0xfa: RuneError,
0xfb: RuneError,
0xfc: RuneError,
0xfd: RuneError,
0xfe: RuneError,
0xff: RuneError,
}}
cm.Init()
EBCDIC = cm
}

@ -0,0 +1,33 @@
// Copyright 2015 Garrett D'Amore
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encoding
import (
"golang.org/x/text/encoding"
)
// ISO8859_1 represents the 8-bit ISO8859-1 scheme. It decodes directly to
// UTF-8 without change, as all ISO8859-1 values are legal UTF-8.
// Unicode values less than 256 (i.e. 8 bits) map 1:1 with 8859-1.
// It encodes runes outside of that to 0x1A, the ASCII substitution character.
var ISO8859_1 encoding.Encoding
func init() {
cm := &Charmap{}
cm.Init()
// 8859-1 is the 8-bit identity map for Unicode.
ISO8859_1 = cm
}

@ -0,0 +1,35 @@
// Copyright 2015 Garrett D'Amore
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encoding
import (
"golang.org/x/text/encoding"
)
// ISO8859_9 represents the 8-bit ISO8859-9 scheme.
var ISO8859_9 encoding.Encoding
func init() {
cm := &Charmap{Map: map[byte]rune{
0xD0: 'Ğ',
0xDD: 'İ',
0xDE: 'Ş',
0xF0: 'ğ',
0xFD: 'ı',
0xFE: 'ş',
}}
cm.Init()
ISO8859_9 = cm
}

@ -0,0 +1,35 @@
// Copyright 2015 Garrett D'Amore
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encoding
import (
"golang.org/x/text/encoding"
)
type validUtf8 struct{}
// UTF8 is an encoding for UTF-8. All it does is verify that the UTF-8
// in is valid. The main reason for its existence is that it will detect
// and report ErrSrcShort or ErrDstShort, whereas the Nop encoding just
// passes every byte, blithely.
var UTF8 encoding.Encoding = validUtf8{}
func (validUtf8) NewDecoder() *encoding.Decoder {
return &encoding.Decoder{Transformer: encoding.UTF8Validator}
}
func (validUtf8) NewEncoder() *encoding.Encoder {
return &encoding.Encoder{Transformer: encoding.UTF8Validator}
}

@ -0,0 +1,13 @@
version: 1.0.{build}
clone_folder: c:\gopath\src\github.com\gdamore\tcell
environment:
GOPATH: c:\gopath
build_script:
- go version
- go env
- SET PATH=%LOCALAPPDATA%\atom\bin;%GOPATH%\bin;%PATH%
- go get -t ./...
- go build
- go install ./...
test_script:
- go test ./...

@ -0,0 +1 @@
coverage.txt

@ -0,0 +1,18 @@
language: go
go:
- 1.15.x
- master
arch:
- amd64
- ppc64le
before_install:
- go get -t -v ./...
script:
- go test -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)

@ -0,0 +1,4 @@
Garrett D'Amore <garrett@damore.org>
Zachary Yedidia <zyedidia@gmail.com>
Junegunn Choi <junegunn.c@gmail.com>
Staysail Systems, Inc. <info@staysail.tech>

@ -0,0 +1,82 @@
## Breaking Changes in _Tcell_ v2
A number of changes were made to _Tcell_ for version two, and some of these are breaking.
### Import Path
The import path for tcell has changed to `github.com/gdamore/tcell/v2` to reflect a new major version.
### Style Is Not Numeric
The type `Style` has changed to a structure, to allow us to add additional data such as flags for color setting,
more attribute bits, and so forth.
Applications that relied on this being a number will need to be updated to use the accessor methods.
### Mouse Event Changes
The middle mouse button was reported as button 2 on Linux, but as button 3 on Windows,
and the right mouse button was reported the reverse way.
_Tcell_ now always reports the right mouse button as button 2, and the middle button as button 3.
To help make this clearer, new symbols `ButtonPrimary`, `ButtonSecondary`, and
`ButtonMiddle` are provided.
(Note that which button is right vs. left may be impacted by user preferences.
Usually the left button will be considered the Primary, and the right will be the Secondary.)
Applications may need to adjust their handling of mouse buttons 2 and 3 accordingly.
### Terminals Removed
A number of terminals have been removed.
These are mostly ancient definitions unlikely to be used by anyone, such as `adm3a`.
### High Number Function Keys
Historically terminfo reported function keys with modifiers set as a different
function key altogether. For example, Shift-F1 was reported as F13 on XTerm.
_Tcell_ now prefers to report these using the base key (such as F1) with modifiers added.
This works on XTerm and VTE based emulators, but some emulators may not support this.
The new behavior more closely aligns with behavior on Windows platforms.
## New Features in _Tcell_ v2
These features are not breaking, but are introduced in version 2.
### Improved Modifier Support
For terminals that appear to behave like the venerable XTerm, _tcell_
automatically adds modifier reporting for ALT, CTRL, SHIFT, and META keys
when the terminal reports them.
### Better Support for Palettes (Themes)
When using a color by its name or palette entry, _Tcell_ now tries to
use that palette entry as is; this should avoid some inconsistency and respect
terminal themes correctly.
When true fidelity to RGB values is needed, the new `TrueColor()` API can be used
to create a direct color, which bypasses the palette altogether.
### Automatic TrueColor Detection
For some terminals, if the `Tc` or `RGB` properties are present in terminfo,
_Tcell_ will automatically assume the terminal supports 24-bit color.
### ColorReset
A new color value, `ColorReset` can be used on the foreground or background
to reset the color the default used by the terminal.
### tmux Support
_Tcell_ now has improved support for tmux, when the `$TERM` variable is set to "tmux".
### Strikethrough Support
_Tcell_ has support for strikethrough when the terminal supports it, using the new `StrikeThrough()` API.
### Bracketed Paste Support
_Tcell_ provides the long requested capability to discriminate paste event by using the
bracketed-paste capability present in some terminals. This is automatically available on
terminals that support XTerm style mouse handling, but applications must opt-in to this
by using the new `EnablePaste()` function. A new `EventPaste` type of event will be
delivered when starting and finishing a paste operation.

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,272 @@
# ![Tcell](logos/tcell.png)
_Tcell_ is a _Go_ package that provides a cell based view for text terminals, like _XTerm_.
It was inspired by _termbox_, but includes many additional improvements.
[![Linux Status](https://img.shields.io/travis/gdamore/tcell.svg?label=linux)](https://travis-ci.org/gdamore/tcell)
[![Windows Status](https://img.shields.io/appveyor/ci/gdamore/tcell.svg?label=windows)](https://ci.appveyor.com/project/gdamore/tcell)
[![Apache License](https://img.shields.io/badge/license-APACHE2-blue.svg)](https://github.com/gdamore/tcell/blob/master/LICENSE)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/gdamore/tcell/v2)
[![Report Card](https://goreportcard.com/badge/gdamore/tcell)](http://goreportcard.com/report/gdamore/tcell/v2)
[![Discord](https://img.shields.io/discord/639503822733180969?label=discord)](https://discord.gg/urTTxDN)
[![Coverage](https://codecov.io/gh/gdamore/tcell/branch/master/graph/badge.svg)](https://codecov.io/gh/gdamore/tcell)
NOTE: This is version 2 of _Tcell_. There are breaking changes relative to version 1.
Version 1.x remains available using the import `github.com/gdamore/tcell`.
## Tutorial
A brief, and still somewhat rough, [tutorial](TUTORIAL.md) is available.
## Examples
* [proxima5](https://github.com/gdamore/proxima5) - space shooter ([video](https://youtu.be/jNxKTCmY_bQ))
* [govisor](https://github.com/gdamore/govisor) - service management UI ([screenshot](http://2.bp.blogspot.com/--OsvnfzSNow/Vf7aqMw3zXI/AAAAAAAAARo/uOMtOvw4Sbg/s1600/Screen%2BShot%2B2015-09-20%2Bat%2B9.08.41%2BAM.png))
* mouse demo - included mouse test ([screenshot](http://2.bp.blogspot.com/-fWvW5opT0es/VhIdItdKqJI/AAAAAAAAATE/7Ojc0L1SpB0/s1600/Screen%2BShot%2B2015-10-04%2Bat%2B11.47.13%2BPM.png))
* [gomatrix](https://github.com/gdamore/gomatrix) - converted from Termbox
* [micro](https://github.com/zyedidia/micro/) - lightweight text editor with syntax-highlighting and themes
* [godu](https://github.com/viktomas/godu) - utility to discover large files/folders
* [tview](https://github.com/rivo/tview/) - rich interactive widgets
* [cview](https://code.rocketnine.space/tslocum/cview) - user interface toolkit (fork of _tview_)
* [awsome gocui](https://github.com/awesome-gocui/gocui) - Go Console User Interface
* [gomandelbrot](https://github.com/rgm3/gomandelbrot) - Mandelbrot!
* [WTF](https://github.com/senorprogrammer/wtf) - personal information dashboard
* [browsh](https://github.com/browsh-org/browsh) - modern web browser ([video](https://www.youtube.com/watch?v=HZq86XfBoRo))
* [go-life](https://github.com/sachaos/go-life) - Conway's Game of Life
* [gowid](https://github.com/gcla/gowid) - compositional widgets for terminal UIs, inspired by _urwid_
* [termshark](https://termshark.io) - interface for _tshark_, inspired by Wireshark, built on _gowid_
* [go-tetris](https://github.com/MichaelS11/go-tetris) - Go Tetris with AI option
* [fzf](https://github.com/junegunn/fzf) - command-line fuzzy finder
* [ascii-fluid](https://github.com/esimov/ascii-fluid) - fluid simulation controlled by webcam
* [cbind](https://code.rocketnine.space/tslocum/cbind) - key event encoding, decoding and handling
* [tpong](https://github.com/spinzed/tpong) - old-school Pong
* [aerc](https://git.sr.ht/~sircmpwn/aerc) - email client
* [tblogs](https://github.com/ezeoleaf/tblogs) - development blogs reader
* [spinc](https://github.com/lallassu/spinc) - _irssi_ inspired chat application for Cisco Spark/WebEx
* [gorss](https://github.com/lallassu/gorss) - RSS/Atom feed reader
* [memoryalike](https://github.com/Bios-Marcel/memoryalike) - memorization game
* [lf](https://github.com/gokcehan/lf) - file manager
* [gokeybr](https://github.com/bunyk/gokeybr) - deliberately practice your typing
* [gonano](https://github.com/jbaramidze/gonano) - editor, mimics _nano_
* [uchess](https://github.com/tmountain/uchess) - UCI chess client
* [min](https://github.com/a-h/min) - Gemini browser
* [ov](https://github.com/noborus/ov) - file pager
* [tmux-wormhole](https://github.com/gcla/tmux-wormhole) - _tmux_ plugin to transfer files
* [gruid-tcell](https://github.com/anaseto/gruid-tcell) - driver for the grid based UI and game framework
* [aretext](https://github.com/aretext/aretext) - minimalist text editor with _vim_ key bindings
## Pure Go Terminfo Database
_Tcell_ includes a full parser and expander for terminfo capability strings,
so that it can avoid hard coding escape strings for formatting. It also favors
portability, and includes support for all POSIX systems.
The database is also flexible & extensible, and can be modified by either running
a program to build the entire database, or an entry for just a single terminal.
## More Portable
_Tcell_ is portable to a wide variety of systems, and is pure Go, without
any need for CGO.
_Tcell_ is believed to work with mainstream systems officially supported by golang.
## No Async IO
_Tcell_ is able to operate without requiring `SIGIO` signals (unlike _termbox_),
or asynchronous I/O, and can instead use standard Go file objects and Go routines.
This means it should be safe, especially for
use with programs that use exec, or otherwise need to manipulate the tty streams.
This model is also much closer to idiomatic Go, leading to fewer surprises.
## Rich Unicode & non-Unicode support
_Tcell_ includes enhanced support for Unicode, including wide characters and
combining characters, provided your terminal can support them.
Note that
Windows terminals generally don't support the full Unicode repertoire.
It will also convert to and from Unicode locales, so that the program
can work with UTF-8 internally, and get reasonable output in other locales.
_Tcell_ tries hard to convert to native characters on both input and output.
On output _Tcell_ even makes use of the alternate character set to facilitate
drawing certain characters.
## More Function Keys
_Tcell_ also has richer support for a larger number of special keys that some
terminals can send.
## Better Color Handling
_Tcell_ will respect your terminal's color space as specified within your terminfo entries.
For example attempts to emit color sequences on VT100 terminals
won't result in unintended consequences.
In legacy Windows mode, _Tcell_ supports 16 colors, bold, dim, and reverse,
instead of just termbox's 8 colors with reverse. (Note that there is some
conflation with bold/dim and colors.)
Modern Windows 10 can benefit from much richer colors however.
_Tcell_ maps 16 colors down to 8, for terminals that need it.
(The upper 8 colors are just brighter versions of the lower 8.)
## Better Mouse Support
_Tcell_ supports enhanced mouse tracking mode, so your application can receive
regular mouse motion events, and wheel events, if your terminal supports it.
(Note: The Windows 10 Terminal application suffers from a flaw in this regard,
and does not support mouse interaction. The stock Windows 10 console host
fired up with cmd.exe or PowerShell works fine however.)
## _Termbox_ Compatibility
A compatibility layer for _termbox_ is provided in the `compat` directory.
To use it, try importing `github.com/gdamore/tcell/termbox` instead.
Most _termbox-go_ programs will probably work without further modification.
## Working With Unicode
Internally _Tcell_ uses UTF-8, just like Go.
However, _Tcell_ understands how to
convert to and from other character sets, using the capabilities of
the `golang.org/x/text/encoding packages`.
Your application must supply
them, as the full set of the most common ones bloats the program by about 2 MB.
If you're lazy, and want them all anyway, see the `encoding` sub-directory.
## Wide & Combining Characters
The `SetContent()` API takes a primary rune, and an optional list of combining runes.
If any of the runes is a wide (East Asian) rune occupying two cells,
then the library will skip output from the following cell. Care must be
taken in the application to avoid explicitly attempting to set content in the
next cell, otherwise the results are undefined. (Normally the wide character
is displayed, and the other character is not; do not depend on that behavior.)
Older terminal applications (especially on systems like Windows 8) lack support
for advanced Unicode, and thus may not fare well.
## Colors
_Tcell_ assumes the ANSI/XTerm color model, including the 256 color map that
XTerm uses when it supports 256 colors. The terminfo guidance will be
honored, with respect to the number of colors supported. Also, only
terminals which expose ANSI style `setaf` and `setab` will support color;
if you have a color terminal that only has `setf` and `setb`, please submit
a ticket.
## 24-bit Color
_Tcell_ _supports 24-bit color!_ (That is, if your terminal can support it.)
NOTE: Technically the approach of using 24-bit RGB values for color is more
accurately described as "direct color", but most people use the term "true color".
We follow the (inaccurate) common convention.
There are a few ways you can enable (or disable) true color.
* For many terminals, we can detect it automatically if your terminal
includes the `RGB` or `Tc` capabilities (or rather it did when the database
was updated.)
* You can force this one by setting the `COLORTERM` environment variable to
`24-bit`, `truecolor` or `24bit`. This is the same method used
by most other terminal applications that support 24-bit color.
* If you set your `TERM` environment variable to a value with the suffix `-truecolor`
then 24-bit color compatible with XTerm and ECMA-48 will be assumed.
(This feature is deprecated.
It is recommended to use one of other methods listed above.)
* You can disable 24-bit color by setting `TCELL_TRUECOLOR=disable` in your
environment.
When using TrueColor, programs will display the colors that the programmer
intended, overriding any "`themes`" you may have set in your terminal
emulator. (For some cases, accurate color fidelity is more important
than respecting themes. For other cases, such as typical text apps that
only use a few colors, its more desirable to respect the themes that
the user has established.)
## Performance
Reasonable attempts have been made to minimize sending data to terminals,
avoiding repeated sequences or drawing the same cell on refresh updates.
## Terminfo
(Not relevant for Windows users.)
The Terminfo implementation operates with a built-in database.
This should satisfy most users. However, it can also (on systems
with ncurses installed), dynamically parse the output from `infocmp`
for terminals it does not already know about.
See the `terminfo/` directory for more information about generating
new entries for the built-in database.
_Tcell_ requires that the terminal support the `cup` mode of cursor addressing.
Ancient terminals without the ability to position the cursor directly
are not supported.
This is unlikely to be a problem; such terminals have not been mass-produced
since the early 1970s.
## Mouse Support
Mouse support is detected via the `kmous` terminfo variable, however,
enablement/disablement and decoding mouse events is done using hard coded
sequences based on the XTerm X11 model. All popular
terminals with mouse tracking support this model. (Full terminfo support
is not possible as terminfo sequences are not defined.)
On Windows, the mouse works normally.
Mouse wheel buttons on various terminals are known to work, but the support
in terminal emulators, as well as support for various buttons and
live mouse tracking, varies widely.
Modern _xterm_, macOS _Terminal_, and _iTerm_ all work well.
## Bracketed Paste
Terminals that appear to support the XTerm mouse model also can support
bracketed paste, for applications that opt-in. See `EnablePaste()` for details.
## Testability
There is a `SimulationScreen`, that can be used to simulate a real screen
for automated testing. The supplied tests do this. The simulation contains
event delivery, screen resizing support, and capabilities to inject events
and examine "`physical`" screen contents.
## Platforms
### POSIX (Linux, FreeBSD, macOS, Solaris, etc.)
Everything works using pure Go on mainstream platforms. Some more esoteric
platforms (e.g., AIX) may need to be added. Pull requests are welcome!
### Windows
Windows console mode applications are supported.
Modern console applications like ConEmu and the Windows 10 terminal,
support all the good features (resize, mouse tracking, etc.)
### Plan9, WASM, and others
These platforms won't work, but compilation stubs are supplied
for folks that want to include parts of this in software for those
platforms. The Simulation screen works, but as _Tcell_ doesn't know how to
allocate a real screen object on those platforms, `NewScreen()` will fail.
If anyone has wisdom about how to improve support for these,
please let me know. PRs are especially welcome.
### Commercial Support
_Tcell_ is absolutely free, but if you want to obtain commercial, professional support, there are options.
* [TideLift](https://tidelift.com/) subscriptions include support for _Tcell_, as well as many other open source packages.
* [Staysail Systems Inc.](mailto:info@staysail.tech) offers direct support, and custom development around _Tcell_ on an hourly basis.

@ -0,0 +1,293 @@
# _Tcell_ Tutorial
_Tcell_ provides a low-level, portable API for building terminal-based programs.
A [terminal emulator](https://en.wikipedia.org/wiki/Terminal_emulator)
(or a real terminal such as a DEC VT-220) is used to interact with such a program.
_Tcell_'s interface is fairly low-level.
While it provides a reasonably portable way of dealing with all the usual terminal
features, it may be easier to utilize a higher level framework.
A number of such frameworks are listed on the _Tcell_ main [README](README.md).
This tutorial provides the details of _Tcell_, and is appropriate for developers
wishing to create their own application frameworks or needing more direct access
to the terminal capabilities.
## Resize events
Applications receive an event of type `EventResize` when they are first initialized and each time the terminal is resized.
The new size is available as `Size`.
```golang
switch ev := ev.(type) {
case *tcell.EventResize:
w, h := ev.Size()
logMessage(fmt.Sprintf("Resized to %dx%d", w, h))
}
```
## Key events
When a key is pressed, applications receive an event of type `EventKey`.
This event describes the modifier keys pressed (if any) and the pressed key or rune.
When a rune key is pressed, an event with its `Key` set to `KeyRune` is dispatched.
When a non-rune key is pressed, it is available as the `Key` of the event.
```golang
switch ev := ev.(type) {
case *tcell.EventKey:
mod, key, ch := ev.Mod(), ev.Key(), ev.Rune()
logMessage(fmt.Sprintf("EventKey Modifiers: %d Key: %d Rune: %d", mod, key, ch))
}
```
### Key event restrictions
Terminal-based programs have less visibility into keyboard activity than graphical applications.
When a key is pressed and held, additional key press events are sent by the terminal emulator.
The rate of these repeated events depends on the emulator's configuration.
Key release events are not available.
It is not possible to distinguish runes typed while holding shift and runes typed using caps lock.
Capital letters are reported without the Shift modifier.
## Mouse events
Applications receive an event of type `EventMouse` when the mouse moves, or a mouse button is pressed or released.
Mouse events are only delivered if
`EnableMouse` has been called.
The mouse buttons being pressed (if any) are available as `Buttons`, and the position of the mouse is available as `Position`.
```golang
switch ev := ev.(type) {
case *tcell.EventMouse:
mod := ev.Modifiers()
btns := ev.Buttons()
x, y := ev.Position()
logMessage(fmt.Sprintf("EventMouse Modifiers: %d Buttons: %d Position: %d,%d", mod, btns, x, y))
}
```
### Mouse buttons
Identifier | Alias | Description
-----------|-----------------|-----------
Button1 | ButtonPrimary | Left button
Button2 | ButtonSecondary | Right button
Button3 | ButtonMiddle | Middle button
Button4 | | Side button (thumb/next)
Button5 | | Side button (thumb/prev)
## Usage
To create a tcell application, first initialize a screen to hold it.
```golang
s, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
if err := s.Init(); err != nil {
log.Fatalf("%+v", err)
}
// Set default text style
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
s.SetStyle(defStyle)
// Clear screen
s.Clear()
```
Text may be drawn on the screen using `SetContent`.
```golang
s.SetContent(0, 0, 'H', nil, defStyle)
s.SetContent(1, 0, 'i', nil, defStyle)
s.SetContent(2, 0, '!', nil, defStyle)
```
To draw text more easily, define a render function.
```golang
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
col := x1
for _, r := range []rune(text) {
s.SetContent(col, row, r, nil, style)
col++
if col >= x2 {
row++
col = x1
}
if row > y2 {
break
}
}
}
```
Lastly, define an event loop to handle user input and update application state.
```golang
quit := func() {
s.Fini()
os.Exit(0)
}
for {
// Update screen
s.Show()
// Poll event
ev := s.PollEvent()
// Process event
switch ev := ev.(type) {
case *tcell.EventResize:
s.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
quit()
}
}
}
```
## Demo application
The following demonstrates how to initialize a screen, draw text/graphics and handle user input.
```golang
package main
import (
"fmt"
"log"
"os"
"github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
col := x1
for _, r := range []rune(text) {
s.SetContent(col, row, r, nil, style)
col++
if col >= x2 {
row++
col = x1
}
if row > y2 {
break
}
}
}
func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
if y2 < y1 {
y1, y2 = y2, y1
}
if x2 < x1 {
x1, x2 = x2, x1
}
// Fill background
for row := y1; row <= y2; row++ {
for col := x1; col <= x2; col++ {
s.SetContent(col, row, ' ', nil, style)
}
}
// Draw borders
for col := x1; col <= x2; col++ {
s.SetContent(col, y1, tcell.RuneHLine, nil, style)
s.SetContent(col, y2, tcell.RuneHLine, nil, style)
}
for row := y1 + 1; row < y2; row++ {
s.SetContent(x1, row, tcell.RuneVLine, nil, style)
s.SetContent(x2, row, tcell.RuneVLine, nil, style)
}
// Only draw corners if necessary
if y1 != y2 && x1 != x2 {
s.SetContent(x1, y1, tcell.RuneULCorner, nil, style)
s.SetContent(x2, y1, tcell.RuneURCorner, nil, style)
s.SetContent(x1, y2, tcell.RuneLLCorner, nil, style)
s.SetContent(x2, y2, tcell.RuneLRCorner, nil, style)
}
drawText(s, x1+1, y1+1, x2-1, y2-1, style, text)
}
func main() {
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
boxStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorPurple)
// Initialize screen
s, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
if err := s.Init(); err != nil {
log.Fatalf("%+v", err)
}
s.SetStyle(defStyle)
s.EnableMouse()
s.EnablePaste()
s.Clear()
// Draw initial boxes
drawBox(s, 1, 1, 42, 7, boxStyle, "Click and drag to draw a box")
drawBox(s, 5, 9, 32, 14, boxStyle, "Press C to reset")
// Event loop
ox, oy := -1, -1
quit := func() {
s.Fini()
os.Exit(0)
}
for {
// Update screen
s.Show()
// Poll event
ev := s.PollEvent()
// Process event
switch ev := ev.(type) {
case *tcell.EventResize:
s.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
quit()
} else if ev.Key() == tcell.KeyCtrlL {
s.Sync()
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
s.Clear()
}
case *tcell.EventMouse:
x, y := ev.Position()
button := ev.Buttons()
// Only process button events, not wheel events
button &= tcell.ButtonMask(0xff)
if button != tcell.ButtonNone && ox < 0 {
ox, oy = x, y
}
switch ev.Buttons() {
case tcell.ButtonNone:
if ox >= 0 {
label := fmt.Sprintf("%d,%d to %d,%d", ox, oy, x, y)
drawBox(s, ox, oy, x, y, boxStyle, label)
ox, oy = -1, -1
}
}
}
}
}
```

@ -0,0 +1,33 @@
// Copyright 2020 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
// AttrMask represents a mask of text attributes, apart from color.
// Note that support for attributes may vary widely across terminals.
type AttrMask int
// Attributes are not colors, but affect the display of text. They can
// be combined.
const (
AttrBold AttrMask = 1 << iota
AttrBlink
AttrReverse
AttrUnderline
AttrDim
AttrItalic
AttrStrikeThrough
AttrInvalid // Mark the style or attributes invalid
AttrNone AttrMask = 0 // Just normal text.
)

@ -0,0 +1,177 @@
// Copyright 2019 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
runewidth "github.com/mattn/go-runewidth"
)
type cell struct {
currMain rune
currComb []rune
currStyle Style
lastMain rune
lastStyle Style
lastComb []rune
width int
}
// CellBuffer represents a two dimensional array of character cells.
// This is primarily intended for use by Screen implementors; it
// contains much of the common code they need. To create one, just
// declare a variable of its type; no explicit initialization is necessary.
//
// CellBuffer is not thread safe.
type CellBuffer struct {
w int
h int
cells []cell
}
// SetContent sets the contents (primary rune, combining runes,
// and style) for a cell at a given location.
func (cb *CellBuffer) SetContent(x int, y int,
mainc rune, combc []rune, style Style) {
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
c := &cb.cells[(y*cb.w)+x]
c.currComb = append([]rune{}, combc...)
if c.currMain != mainc {
c.width = runewidth.RuneWidth(mainc)
}
c.currMain = mainc
c.currStyle = style
}
}
// GetContent returns the contents of a character cell, including the
// primary rune, any combining character runes (which will usually be
// nil), the style, and the display width in cells. (The width can be
// either 1, normally, or 2 for East Asian full-width characters.)
func (cb *CellBuffer) GetContent(x, y int) (rune, []rune, Style, int) {
var mainc rune
var combc []rune
var style Style
var width int
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
c := &cb.cells[(y*cb.w)+x]
mainc, combc, style = c.currMain, c.currComb, c.currStyle
if width = c.width; width == 0 || mainc < ' ' {
width = 1
mainc = ' '
}
}
return mainc, combc, style, width
}
// Size returns the (width, height) in cells of the buffer.
func (cb *CellBuffer) Size() (int, int) {
return cb.w, cb.h
}
// Invalidate marks all characters within the buffer as dirty.
func (cb *CellBuffer) Invalidate() {
for i := range cb.cells {
cb.cells[i].lastMain = rune(0)
}
}
// Dirty checks if a character at the given location needs an
// to be refreshed on the physical display. This returns true
// if the cell content is different since the last time it was
// marked clean.
func (cb *CellBuffer) Dirty(x, y int) bool {
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
c := &cb.cells[(y*cb.w)+x]
if c.lastMain == rune(0) {
return true
}
if c.lastMain != c.currMain {
return true
}
if c.lastStyle != c.currStyle {
return true
}
if len(c.lastComb) != len(c.currComb) {
return true
}
for i := range c.lastComb {
if c.lastComb[i] != c.currComb[i] {
return true
}
}
}
return false
}
// SetDirty is normally used to indicate that a cell has
// been displayed (in which case dirty is false), or to manually
// force a cell to be marked dirty.
func (cb *CellBuffer) SetDirty(x, y int, dirty bool) {
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
c := &cb.cells[(y*cb.w)+x]
if dirty {
c.lastMain = rune(0)
} else {
if c.currMain == rune(0) {
c.currMain = ' '
}
c.lastMain = c.currMain
c.lastComb = c.currComb
c.lastStyle = c.currStyle
}
}
}
// Resize is used to resize the cells array, with different dimensions,
// while preserving the original contents. The cells will be invalidated
// so that they can be redrawn.
func (cb *CellBuffer) Resize(w, h int) {
if cb.h == h && cb.w == w {
return
}
newc := make([]cell, w*h)
for y := 0; y < h && y < cb.h; y++ {
for x := 0; x < w && x < cb.w; x++ {
oc := &cb.cells[(y*cb.w)+x]
nc := &newc[(y*w)+x]
nc.currMain = oc.currMain
nc.currComb = oc.currComb
nc.currStyle = oc.currStyle
nc.width = oc.width
nc.lastMain = rune(0)
}
}
cb.cells = newc
cb.h = h
cb.w = w
}
// Fill fills the entire cell buffer array with the specified character
// and style. Normally choose ' ' to clear the screen. This API doesn't
// support combining characters, or characters with a width larger than one.
func (cb *CellBuffer) Fill(r rune, style Style) {
for i := range cb.cells {
c := &cb.cells[i]
c.currMain = r
c.currComb = nil
c.currStyle = style
c.width = 1
}
}

@ -0,0 +1,21 @@
// +build plan9 nacl
// Copyright 2015 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
func getCharset() string {
return ""
}

@ -0,0 +1,49 @@
// +build !windows,!nacl,!plan9
// Copyright 2016 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"os"
"strings"
)
func getCharset() string {
// Determine the character set. This can help us later.
// Per POSIX, we search for LC_ALL first, then LC_CTYPE, and
// finally LANG. First one set wins.
locale := ""
if locale = os.Getenv("LC_ALL"); locale == "" {
if locale = os.Getenv("LC_CTYPE"); locale == "" {
locale = os.Getenv("LANG")
}
}
if locale == "POSIX" || locale == "C" {
return "US-ASCII"
}
if i := strings.IndexRune(locale, '@'); i >= 0 {
locale = locale[:i]
}
if i := strings.IndexRune(locale, '.'); i >= 0 {
locale = locale[i+1:]
} else {
// Default assumption, and on Linux we can see LC_ALL
// without a character set, which we assume implies UTF-8.
return "UTF-8"
}
// XXX: add support for aliases
return locale
}

@ -0,0 +1,21 @@
// +build windows
// Copyright 2015 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
func getCharset() string {
return "UTF-16"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,52 @@
// Copyright 2016 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"github.com/lucasb-eyer/go-colorful"
"math"
)
// FindColor attempts to find a given color, or the best match possible for it,
// from the palette given. This is an expensive operation, so results should
// be cached by the caller.
func FindColor(c Color, palette []Color) Color {
match := ColorDefault
dist := float64(0)
r, g, b := c.RGB()
c1 := colorful.Color{
R: float64(r) / 255.0,
G: float64(g) / 255.0,
B: float64(b) / 255.0,
}
for _, d := range palette {
r, g, b = d.RGB()
c2 := colorful.Color{
R: float64(r) / 255.0,
G: float64(g) / 255.0,
B: float64(b) / 255.0,
}
// CIE94 is more accurate, but really really expensive.
nd := c1.DistanceCIE76(c2)
if math.IsNaN(nd) {
nd = math.Inf(1)
}
if match == ColorDefault || nd < dist {
match = d
dist = nd
}
}
return match
}

@ -0,0 +1,23 @@
// +build !windows
// Copyright 2015 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
// NewConsoleScreen returns a console based screen. This platform
// doesn't have support for any, so it returns nil and a suitable error.
func NewConsoleScreen() (Screen, error) {
return nil, ErrNoScreen
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,48 @@
// Copyright 2018 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package tcell provides a lower-level, portable API for building
// programs that interact with terminals or consoles. It works with
// both common (and many uncommon!) terminals or terminal emulators,
// and Windows console implementations.
//
// It provides support for up to 256 colors, text attributes, and box drawing
// elements. A database of terminals built from a real terminfo database
// is provided, along with code to generate new database entries.
//
// Tcell offers very rich support for mice, dependent upon the terminal
// of course. (Windows, XTerm, and iTerm 2 are known to work very well.)
//
// If the environment is not Unicode by default, such as an ISO8859 based
// locale or GB18030, Tcell can convert input and output, so that your
// terminal can operate in whatever locale is most convenient, while the
// application program can just assume "everything is UTF-8". Reasonable
// defaults are used for updating characters to something suitable for
// display. Unicode box drawing characters will be converted to use the
// alternate character set of your terminal, if native conversions are
// not available. If no ACS is available, then some ASCII fallbacks will
// be used.
//
// Note that support for non-UTF-8 locales (other than C) must be enabled
// by the application using RegisterEncoding() -- we don't have them all
// enabled by default to avoid bloating the application unneccessarily.
// (These days UTF-8 is good enough for almost everyone, and nobody should
// be using legacy locales anymore.) Also, actual glyphs for various code
// point will only be displayed if your terminal or emulator (or the font
// the emulator is using) supports them.
//
// A rich set of keycodes is supported, with support for up to 65 function
// keys, and various other special keys.
//
package tcell

@ -0,0 +1,139 @@
// Copyright 2015 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"strings"
"sync"
"golang.org/x/text/encoding"
gencoding "github.com/gdamore/encoding"
)
var encodings map[string]encoding.Encoding
var encodingLk sync.Mutex
var encodingFallback EncodingFallback = EncodingFallbackFail
// RegisterEncoding may be called by the application to register an encoding.
// The presence of additional encodings will facilitate application usage with
// terminal environments where the I/O subsystem does not support Unicode.
//
// Windows systems use Unicode natively, and do not need any of the encoding
// subsystem when using Windows Console screens.
//
// Please see the Go documentation for golang.org/x/text/encoding -- most of
// the common ones exist already as stock variables. For example, ISO8859-15
// can be registered using the following code:
//
// import "golang.org/x/text/encoding/charmap"
//
// ...
// RegisterEncoding("ISO8859-15", charmap.ISO8859_15)
//
// Aliases can be registered as well, for example "8859-15" could be an alias
// for "ISO8859-15".
//
// For POSIX systems, the tcell package will check the environment variables
// LC_ALL, LC_CTYPE, and LANG (in that order) to determine the character set.
// These are expected to have the following pattern:
//
// $language[.$codeset[@$variant]
//
// We extract only the $codeset part, which will usually be something like
// UTF-8 or ISO8859-15 or KOI8-R. Note that if the locale is either "POSIX"
// or "C", then we assume US-ASCII (the POSIX 'portable character set'
// and assume all other characters are somehow invalid.)
//
// Modern POSIX systems and terminal emulators may use UTF-8, and for those
// systems, this API is also unnecessary. For example, Darwin (MacOS X) and
// modern Linux running modern xterm generally will out of the box without
// any of this. Use of UTF-8 is recommended when possible, as it saves
// quite a lot processing overhead.
//
// Note that some encodings are quite large (for example GB18030 which is a
// superset of Unicode) and so the application size can be expected ot
// increase quite a bit as each encoding is added. The East Asian encodings
// have been seen to add 100-200K per encoding to the application size.
//
func RegisterEncoding(charset string, enc encoding.Encoding) {
encodingLk.Lock()
charset = strings.ToLower(charset)
encodings[charset] = enc
encodingLk.Unlock()
}
// EncodingFallback describes how the system behavees when the locale
// requires a character set that we do not support. The system always
// supports UTF-8 and US-ASCII. On Windows consoles, UTF-16LE is also
// supported automatically. Other character sets must be added using the
// RegisterEncoding API. (A large group of nearly all of them can be
// added using the RegisterAll function in the encoding sub package.)
type EncodingFallback int
const (
// EncodingFallbackFail behavior causes GetEncoding to fail
// when it cannot find an encoding.
EncodingFallbackFail = iota
// EncodingFallbackASCII behaviore causes GetEncoding to fall back
// to a 7-bit ASCII encoding, if no other encoding can be found.
EncodingFallbackASCII
// EncodingFallbackUTF8 behavior causes GetEncoding to assume
// UTF8 can pass unmodified upon failure. Note that this behavior
// is not recommended, unless you are sure your terminal can cope
// with real UTF8 sequences.
EncodingFallbackUTF8
)
// SetEncodingFallback changes the behavior of GetEncoding when a suitable
// encoding is not found. The default is EncodingFallbackFail, which
// causes GetEncoding to simply return nil.
func SetEncodingFallback(fb EncodingFallback) {
encodingLk.Lock()
encodingFallback = fb
encodingLk.Unlock()
}
// GetEncoding is used by Screen implementors who want to locate an encoding
// for the given character set name. Note that this will return nil for
// either the Unicode (UTF-8) or ASCII encodings, since we don't use
// encodings for them but instead have our own native methods.
func GetEncoding(charset string) encoding.Encoding {
charset = strings.ToLower(charset)
encodingLk.Lock()
defer encodingLk.Unlock()
if enc, ok := encodings[charset]; ok {
return enc
}
switch encodingFallback {
case EncodingFallbackASCII:
return gencoding.ASCII
case EncodingFallbackUTF8:
return encoding.Nop
}
return nil
}
func init() {
// We always support UTF-8 and ASCII.
encodings = make(map[string]encoding.Encoding)
encodings["utf-8"] = gencoding.UTF8
encodings["utf8"] = gencoding.UTF8
encodings["us-ascii"] = gencoding.ASCII
encodings["ascii"] = gencoding.ASCII
encodings["iso646"] = gencoding.ASCII
}

@ -0,0 +1,73 @@
// Copyright 2015 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"errors"
"time"
"github.com/gdamore/tcell/v2/terminfo"
)
var (
// ErrTermNotFound indicates that a suitable terminal entry could
// not be found. This can result from either not having TERM set,
// or from the TERM failing to support certain minimal functionality,
// in particular absolute cursor addressability (the cup capability)
// is required. For example, legacy "adm3" lacks this capability,
// whereas the slightly newer "adm3a" supports it. This failure
// occurs most often with "dumb".
ErrTermNotFound = terminfo.ErrTermNotFound
// ErrNoScreen indicates that no suitable screen could be found.
// This may result from attempting to run on a platform where there
// is no support for either termios or console I/O (such as nacl),
// or from running in an environment where there is no access to
// a suitable console/terminal device. (For example, running on
// without a controlling TTY or with no /dev/tty on POSIX platforms.)
ErrNoScreen = errors.New("no suitable screen available")
// ErrNoCharset indicates that the locale environment the
// program is not supported by the program, because no suitable
// encoding was found for it. This problem never occurs if
// the environment is UTF-8 or UTF-16.
ErrNoCharset = errors.New("character set not supported")
// ErrEventQFull indicates that the event queue is full, and
// cannot accept more events.
ErrEventQFull = errors.New("event queue full")
)
// An EventError is an event representing some sort of error, and carries
// an error payload.
type EventError struct {
t time.Time
err error
}
// When returns the time when the event was created.
func (ev *EventError) When() time.Time {
return ev.t
}
// Error implements the error.
func (ev *EventError) Error() string {
return ev.err.Error()
}
// NewEventError creates an ErrorEvent with the given error payload.
func NewEventError(err error) *EventError {
return &EventError{t: time.Now(), err: err}
}

@ -0,0 +1,53 @@
// Copyright 2015 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"time"
)
// Event is a generic interface used for passing around Events.
// Concrete types follow.
type Event interface {
// When reports the time when the event was generated.
When() time.Time
}
// EventTime is a simple base event class, suitable for easy reuse.
// It can be used to deliver actual timer events as well.
type EventTime struct {
when time.Time
}
// When returns the time stamp when the event occurred.
func (e *EventTime) When() time.Time {
return e.when
}
// SetEventTime sets the time of occurrence for the event.
func (e *EventTime) SetEventTime(t time.Time) {
e.when = t
}
// SetEventNow sets the time of occurrence for the event to the current time.
func (e *EventTime) SetEventNow() {
e.SetEventTime(time.Now())
}
// EventHandler is anything that handles events. If the handler has
// consumed the event, it should return true. False otherwise.
type EventHandler interface {
HandleEvent(Event) bool
}

@ -0,0 +1,41 @@
// Copyright 2015 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"time"
)
// EventInterrupt is a generic wakeup event. Its can be used to
// to request a redraw. It can carry an arbitrary payload, as well.
type EventInterrupt struct {
t time.Time
v interface{}
}
// When returns the time when this event was created.
func (ev *EventInterrupt) When() time.Time {
return ev.t
}
// Data is used to obtain the opaque event payload.
func (ev *EventInterrupt) Data() interface{} {
return ev.v
}
// NewEventInterrupt creates an EventInterrupt with the given payload.
func NewEventInterrupt(data interface{}) *EventInterrupt {
return &EventInterrupt{t: time.Now(), v: data}
}

@ -0,0 +1,470 @@
// Copyright 2016 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"fmt"
"strings"
"time"
)
// EventKey represents a key press. Usually this is a key press followed
// by a key release, but since terminal programs don't have a way to report
// key release events, we usually get just one event. If a key is held down
// then the terminal may synthesize repeated key presses at some predefined
// rate. We have no control over that, nor visibility into it.
//
// In some cases, we can have a modifier key, such as ModAlt, that can be
// generated with a key press. (This usually is represented by having the
// high bit set, or in some cases, by sending an ESC prior to the rune.)
//
// If the value of Key() is KeyRune, then the actual key value will be
// available with the Rune() method. This will be the case for most keys.
// In most situations, the modifiers will not be set. For example, if the
// rune is 'A', this will be reported without the ModShift bit set, since
// really can't tell if the Shift key was pressed (it might have been CAPSLOCK,
// or a terminal that only can send capitals, or keyboard with separate
// capital letters from lower case letters).
//
// Generally, terminal applications have far less visibility into keyboard
// activity than graphical applications. Hence, they should avoid depending
// overly much on availability of modifiers, or the availability of any
// specific keys.
type EventKey struct {
t time.Time
mod ModMask
key Key
ch rune
}
// When returns the time when this Event was created, which should closely
// match the time when the key was pressed.
func (ev *EventKey) When() time.Time {
return ev.t
}
// Rune returns the rune corresponding to the key press, if it makes sense.
// The result is only defined if the value of Key() is KeyRune.
func (ev *EventKey) Rune() rune {
return ev.ch
}
// Key returns a virtual key code. We use this to identify specific key
// codes, such as KeyEnter, etc. Most control and function keys are reported
// with unique Key values. Normal alphanumeric and punctuation keys will
// generally return KeyRune here; the specific key can be further decoded
// using the Rune() function.
func (ev *EventKey) Key() Key {
return ev.key
}
// Modifiers returns the modifiers that were present with the key press. Note
// that not all platforms and terminals support this equally well, and some
// cases we will not not know for sure. Hence, applications should avoid
// using this in most circumstances.
func (ev *EventKey) Modifiers() ModMask {
return ev.mod
}
// KeyNames holds the written names of special keys. Useful to echo back a key
// name, or to look up a key from a string value.
var KeyNames = map[Key]string{
KeyEnter: "Enter",
KeyBackspace: "Backspace",
KeyTab: "Tab",
KeyBacktab: "Backtab",
KeyEsc: "Esc",
KeyBackspace2: "Backspace2",
KeyDelete: "Delete",
KeyInsert: "Insert",
KeyUp: "Up",
KeyDown: "Down",
KeyLeft: "Left",
KeyRight: "Right",
KeyHome: "Home",
KeyEnd: "End",
KeyUpLeft: "UpLeft",
KeyUpRight: "UpRight",
KeyDownLeft: "DownLeft",
KeyDownRight: "DownRight",
KeyCenter: "Center",
KeyPgDn: "PgDn",
KeyPgUp: "PgUp",
KeyClear: "Clear",
KeyExit: "Exit",
KeyCancel: "Cancel",
KeyPause: "Pause",
KeyPrint: "Print",
KeyF1: "F1",
KeyF2: "F2",
KeyF3: "F3",
KeyF4: "F4",
KeyF5: "F5",
KeyF6: "F6",
KeyF7: "F7",
KeyF8: "F8",
KeyF9: "F9",
KeyF10: "F10",
KeyF11: "F11",
KeyF12: "F12",
KeyF13: "F13",
KeyF14: "F14",
KeyF15: "F15",
KeyF16: "F16",
KeyF17: "F17",
KeyF18: "F18",
KeyF19: "F19",
KeyF20: "F20",
KeyF21: "F21",
KeyF22: "F22",
KeyF23: "F23",
KeyF24: "F24",
KeyF25: "F25",
KeyF26: "F26",
KeyF27: "F27",
KeyF28: "F28",
KeyF29: "F29",
KeyF30: "F30",
KeyF31: "F31",
KeyF32: "F32",
KeyF33: "F33",
KeyF34: "F34",
KeyF35: "F35",
KeyF36: "F36",
KeyF37: "F37",
KeyF38: "F38",
KeyF39: "F39",
KeyF40: "F40",
KeyF41: "F41",
KeyF42: "F42",
KeyF43: "F43",
KeyF44: "F44",
KeyF45: "F45",
KeyF46: "F46",
KeyF47: "F47",
KeyF48: "F48",
KeyF49: "F49",
KeyF50: "F50",
KeyF51: "F51",
KeyF52: "F52",
KeyF53: "F53",
KeyF54: "F54",
KeyF55: "F55",
KeyF56: "F56",
KeyF57: "F57",
KeyF58: "F58",
KeyF59: "F59",
KeyF60: "F60",
KeyF61: "F61",
KeyF62: "F62",
KeyF63: "F63",
KeyF64: "F64",
KeyCtrlA: "Ctrl-A",
KeyCtrlB: "Ctrl-B",
KeyCtrlC: "Ctrl-C",
KeyCtrlD: "Ctrl-D",
KeyCtrlE: "Ctrl-E",
KeyCtrlF: "Ctrl-F",
KeyCtrlG: "Ctrl-G",
KeyCtrlJ: "Ctrl-J",
KeyCtrlK: "Ctrl-K",
KeyCtrlL: "Ctrl-L",
KeyCtrlN: "Ctrl-N",
KeyCtrlO: "Ctrl-O",
KeyCtrlP: "Ctrl-P",
KeyCtrlQ: "Ctrl-Q",
KeyCtrlR: "Ctrl-R",
KeyCtrlS: "Ctrl-S",
KeyCtrlT: "Ctrl-T",
KeyCtrlU: "Ctrl-U",
KeyCtrlV: "Ctrl-V",
KeyCtrlW: "Ctrl-W",
KeyCtrlX: "Ctrl-X",
KeyCtrlY: "Ctrl-Y",
KeyCtrlZ: "Ctrl-Z",
KeyCtrlSpace: "Ctrl-Space",
KeyCtrlUnderscore: "Ctrl-_",
KeyCtrlRightSq: "Ctrl-]",
KeyCtrlBackslash: "Ctrl-\\",
KeyCtrlCarat: "Ctrl-^",
}
// Name returns a printable value or the key stroke. This can be used
// when printing the event, for example.
func (ev *EventKey) Name() string {
s := ""
m := []string{}
if ev.mod&ModShift != 0 {
m = append(m, "Shift")
}
if ev.mod&ModAlt != 0 {
m = append(m, "Alt")
}
if ev.mod&ModMeta != 0 {
m = append(m, "Meta")
}
if ev.mod&ModCtrl != 0 {
m = append(m, "Ctrl")
}
ok := false
if s, ok = KeyNames[ev.key]; !ok {
if ev.key == KeyRune {
s = "Rune[" + string(ev.ch) + "]"
} else {
s = fmt.Sprintf("Key[%d,%d]", ev.key, int(ev.ch))
}
}
if len(m) != 0 {
if ev.mod&ModCtrl != 0 && strings.HasPrefix(s, "Ctrl-") {
s = s[5:]
}
return fmt.Sprintf("%s+%s", strings.Join(m, "+"), s)
}
return s
}
// NewEventKey attempts to create a suitable event. It parses the various
// ASCII control sequences if KeyRune is passed for Key, but if the caller
// has more precise information it should set that specifically. Callers
// that aren't sure about modifier state (most) should just pass ModNone.
func NewEventKey(k Key, ch rune, mod ModMask) *EventKey {
if k == KeyRune && (ch < ' ' || ch == 0x7f) {
// Turn specials into proper key codes. This is for
// control characters and the DEL.
k = Key(ch)
if mod == ModNone && ch < ' ' {
switch Key(ch) {
case KeyBackspace, KeyTab, KeyEsc, KeyEnter:
// these keys are directly typeable without CTRL
default:
// most likely entered with a CTRL keypress
mod = ModCtrl
}
}
}
return &EventKey{t: time.Now(), key: k, ch: ch, mod: mod}
}
// ModMask is a mask of modifier keys. Note that it will not always be
// possible to report modifier keys.
type ModMask int16
// These are the modifiers keys that can be sent either with a key press,
// or a mouse event. Note that as of now, due to the confusion associated
// with Meta, and the lack of support for it on many/most platforms, the
// current implementations never use it. Instead, they use ModAlt, even for
// events that could possibly have been distinguished from ModAlt.
const (
ModShift ModMask = 1 << iota
ModCtrl
ModAlt
ModMeta
ModNone ModMask = 0
)
// Key is a generic value for representing keys, and especially special
// keys (function keys, cursor movement keys, etc.) For normal keys, like
// ASCII letters, we use KeyRune, and then expect the application to
// inspect the Rune() member of the EventKey.
type Key int16
// This is the list of named keys. KeyRune is special however, in that it is
// a place holder key indicating that a printable character was sent. The
// actual value of the rune will be transported in the Rune of the associated
// EventKey.
const (
KeyRune Key = iota + 256
KeyUp
KeyDown
KeyRight
KeyLeft
KeyUpLeft
KeyUpRight
KeyDownLeft
KeyDownRight
KeyCenter
KeyPgUp
KeyPgDn
KeyHome
KeyEnd
KeyInsert
KeyDelete
KeyHelp
KeyExit
KeyClear
KeyCancel
KeyPrint
KeyPause
KeyBacktab
KeyF1
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyF13
KeyF14
KeyF15
KeyF16
KeyF17
KeyF18
KeyF19
KeyF20
KeyF21
KeyF22
KeyF23
KeyF24
KeyF25
KeyF26
KeyF27
KeyF28
KeyF29
KeyF30
KeyF31
KeyF32
KeyF33
KeyF34
KeyF35
KeyF36
KeyF37
KeyF38
KeyF39
KeyF40
KeyF41
KeyF42
KeyF43
KeyF44
KeyF45
KeyF46
KeyF47
KeyF48
KeyF49
KeyF50
KeyF51
KeyF52
KeyF53
KeyF54
KeyF55
KeyF56
KeyF57
KeyF58
KeyF59
KeyF60
KeyF61
KeyF62
KeyF63
KeyF64
)
const (
// These key codes are used internally, and will never appear to applications.
keyPasteStart Key = iota + 16384
keyPasteEnd
)
// These are the control keys. Note that they overlap with other keys,
// perhaps. For example, KeyCtrlH is the same as KeyBackspace.
const (
KeyCtrlSpace Key = iota
KeyCtrlA
KeyCtrlB
KeyCtrlC
KeyCtrlD
KeyCtrlE
KeyCtrlF
KeyCtrlG
KeyCtrlH
KeyCtrlI
KeyCtrlJ
KeyCtrlK
KeyCtrlL
KeyCtrlM
KeyCtrlN
KeyCtrlO
KeyCtrlP
KeyCtrlQ
KeyCtrlR
KeyCtrlS
KeyCtrlT
KeyCtrlU
KeyCtrlV
KeyCtrlW
KeyCtrlX
KeyCtrlY
KeyCtrlZ
KeyCtrlLeftSq // Escape
KeyCtrlBackslash
KeyCtrlRightSq
KeyCtrlCarat
KeyCtrlUnderscore
)
// Special values - these are fixed in an attempt to make it more likely
// that aliases will encode the same way.
// These are the defined ASCII values for key codes. They generally match
// with KeyCtrl values.
const (
KeyNUL Key = iota
KeySOH
KeySTX
KeyETX
KeyEOT
KeyENQ
KeyACK
KeyBEL
KeyBS
KeyTAB
KeyLF
KeyVT
KeyFF
KeyCR
KeySO
KeySI
KeyDLE
KeyDC1
KeyDC2
KeyDC3
KeyDC4
KeyNAK
KeySYN
KeyETB
KeyCAN
KeyEM
KeySUB
KeyESC
KeyFS
KeyGS
KeyRS
KeyUS
KeyDEL Key = 0x7F
)
// These keys are aliases for other names.
const (
KeyBackspace = KeyBS
KeyTab = KeyTAB
KeyEsc = KeyESC
KeyEscape = KeyESC
KeyEnter = KeyCR
KeyBackspace2 = KeyDEL
)

@ -0,0 +1,103 @@
// Copyright 2020 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"time"
)
// EventMouse is a mouse event. It is sent on either mouse up or mouse down
// events. It is also sent on mouse motion events - if the terminal supports
// it. We make every effort to ensure that mouse release events are delivered.
// Hence, click drag can be identified by a motion event with the mouse down,
// without any intervening button release. On some terminals only the initiating
// press and terminating release event will be delivered.
//
// Mouse wheel events, when reported, may appear on their own as individual
// impulses; that is, there will normally not be a release event delivered
// for mouse wheel movements.
//
// Most terminals cannot report the state of more than one button at a time --
// and some cannot report motion events unless a button is pressed.
//
// Applications can inspect the time between events to resolve double or
// triple clicks.
type EventMouse struct {
t time.Time
btn ButtonMask
mod ModMask
x int
y int
}
// When returns the time when this EventMouse was created.
func (ev *EventMouse) When() time.Time {
return ev.t
}
// Buttons returns the list of buttons that were pressed or wheel motions.
func (ev *EventMouse) Buttons() ButtonMask {
return ev.btn
}
// Modifiers returns a list of keyboard modifiers that were pressed
// with the mouse button(s).
func (ev *EventMouse) Modifiers() ModMask {
return ev.mod
}
// Position returns the mouse position in character cells. The origin
// 0, 0 is at the upper left corner.
func (ev *EventMouse) Position() (int, int) {
return ev.x, ev.y
}
// NewEventMouse is used to create a new mouse event. Applications
// shouldn't need to use this; its mostly for screen implementors.
func NewEventMouse(x, y int, btn ButtonMask, mod ModMask) *EventMouse {
return &EventMouse{t: time.Now(), x: x, y: y, btn: btn, mod: mod}
}
// ButtonMask is a mask of mouse buttons and wheel events. Mouse button presses
// are normally delivered as both press and release events. Mouse wheel events
// are normally just single impulse events. Windows supports up to eight
// separate buttons plus all four wheel directions, but XTerm can only support
// mouse buttons 1-3 and wheel up/down. Its not unheard of for terminals
// to support only one or two buttons (think Macs). Old terminals, and true
// emulations (such as vt100) won't support mice at all, of course.
type ButtonMask int16
// These are the actual button values. Note that tcell version 1.x reversed buttons
// two and three on *nix based terminals. We use button 1 as the primary, and
// button 2 as the secondary, and button 3 (which is often missing) as the middle.
const (
Button1 ButtonMask = 1 << iota // Usually the left (primary) mouse button.
Button2 // Usually the right (secondary) mouse button.
Button3 // Usually the middle mouse button.
Button4 // Often a side button (thumb/next).
Button5 // Often a side button (thumb/prev).
Button6
Button7
Button8
WheelUp // Wheel motion up/away from user.
WheelDown // Wheel motion down/towards user.
WheelLeft // Wheel motion to left.
WheelRight // Wheel motion to right.
ButtonNone ButtonMask = 0 // No button or wheel events.
ButtonPrimary = Button1
ButtonSecondary = Button2
ButtonMiddle = Button3
)

@ -0,0 +1,42 @@
// Copyright 2021 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build darwin dragonfly freebsd netbsd openbsd
package tcell
import (
"syscall"
"golang.org/x/sys/unix"
)
// BSD systems use TIOC style ioctls.
// tcSetBufParams is used by the tty driver on UNIX systems to configure the
// buffering parameters (minimum character count and minimum wait time in msec.)
// This also waits for output to drain first.
func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {
_ = syscall.SetNonblock(fd, true)
tio, err := unix.IoctlGetTermios(fd, unix.TIOCGETA)
if err != nil {
return err
}
tio.Cc[unix.VMIN] = vMin
tio.Cc[unix.VTIME] = vTime
if err = unix.IoctlSetTermios(fd, unix.TIOCSETAW, tio); err != nil {
return err
}
return nil
}

@ -0,0 +1,40 @@
// Copyright 2021 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build linux aix zos solaris
package tcell
import (
"syscall"
"golang.org/x/sys/unix"
)
// tcSetBufParams is used by the tty driver on UNIX systems to configure the
// buffering parameters (minimum character count and minimum wait time in msec.)
// This also waits for output to drain first.
func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {
_ = syscall.SetNonblock(fd, true)
tio, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return err
}
tio.Cc[unix.VMIN] = vMin
tio.Cc[unix.VTIME] = vTime
if err = unix.IoctlSetTermios(fd, unix.TCSETSW, tio); err != nil {
return err
}
return nil
}

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

Loading…
Cancel
Save