Compare commits

...

241 Commits

Author SHA1 Message Date
Miguel Mota cbe3557015
Add option for coingecko demo api key 2 months ago
Miguel Mota 409479636d
Add coingecko api key option 3 months ago
Miguel Mota fccbc3b63c
Update docs 3 months ago
Miguel Mota 9641b60b7e
Update README 2 years ago
cui fliter 6a17ed2d3c
fix some typos (#313)
Signed-off-by: cui fliter <imcusg@gmail.com>
2 years ago
Miguel Mota 4e917f6f44
Update docs 2 years ago
Miguel Mota 7c063df60c
Update alt coin link to use braces for template tags 2 years ago
Miguel Mota 4e5aa665ef
Merge branch 'arjundashrath-patch-1' 2 years ago
Miguel Mota 453c35c8d3
Merge branch 'patch-1' of https://github.com/arjundashrath/cointop into arjundashrath-patch-1 2 years ago
Miguel Mota 7de02c2491
Merge branch 'vuon9-feature/update-cache-if-coin-struct-changed' 2 years ago
Miguel Mota bfcce60276
Merge branch 'feature/update-cache-if-coin-struct-changed' of https://github.com/vuon9/cointop into vuon9-feature/update-cache-if-coin-struct-changed 2 years ago
Vuong a52646f479
use gob encoder to get struct hash 2 years ago
Miguel Mota 292ba8343b
Update README 2 years ago
arjundashrath f2380d80be
Update go.yml 2 years ago
Miguel Mota 6e7130c31d
Merge pull request #281 from vuon9/feature/sort-by-view
Ability to have sort col by view
2 years ago
Vuong 2425ac4c88
Improve test for readable purpose 2 years ago
Vuong 4039bc1b08
Improve test for readable purpose 2 years ago
Vuong 06c56b599a
Fix issues of sort by view 2 years ago
Miguel Mota 6c44ff1b45
Merge pull request #294 from johnrichardrinehart/flake
feat(/flake.nix): add a nix flake to facilitate reproducible builds
2 years ago
Jason Shipman 0fa9542d30
Update install docs for compatible go version (#301)
* Update install docs for compatible go version

* Remove explicit reference to ~/go - that's platform specific, and may not be correct

Mine was ~/golang

Co-authored-by: Simon Roberts <lyricnz@users.noreply.github.com>
2 years ago
Vuong Bui f0b936db9d
#291 refresh cache if coin struct hash changed 2 years ago
John Rinehart 8d08e6dc0b feat(/flake.nix): add a nix flake to facilitate reproducible builds 3 years ago
Vuong ad95052e02
Fix wrong scope of var (#293)
* Fix wrong scope of var
* Fix some keys couldn't bind
3 years ago
Miguel Mota 60de82342d
Log debug output file if DEBUG set 3 years ago
Miguel Mota 6d671b28ae
Use preferred temp dir for debug log file 3 years ago
Simon Roberts c735c4a8bb
Merge branch 'cointop-sh:master' into feature/custom-link-url 3 years ago
Simon Roberts 62ce8e1adb
Update all modules/deps (#288) 3 years ago
Vuong 2d9b1501c6
Fix parseKeys related to price alert open action (#290)
* Fix parseKeys related to price alert open action

* Trim s before checking length

Co-authored-by: Simon Roberts <lyricnz@users.noreply.github.com>
3 years ago
Vuong 24a4bdedab
Fix CMC Coin ID (#282)
* Fix #297 CMC Coin ID
* Remove unused check

Co-authored-by: Simon Roberts <lyricnz@users.noreply.github.com>
3 years ago
Vuong 9bffc26035
Fix conversion key triggered in any menu (#289) 3 years ago
Vuong 0339d7f6c3
Fix panic due to using wrong value for key (#285)
Signed-off-by: Vuong <3168632+vuon9@users.noreply.github.com>
3 years ago
Simon Roberts 734345fe5a
Fix comment 3 years ago
Simon Roberts e05acbf95c
Apply strings.ToUpper() to coin.Symbol when creating alt-link 3 years ago
Simon Roberts 9cc10ccdd0
Implement alt-link and bind it to ^o 3 years ago
Vuong 08c7a026cc
Ability to have sort col by view 3 years ago
Vuong fdc9664842
Fix/coin link (#275) 3 years ago
Miguel Mota 0f68624c28
Update features list 3 years ago
Miguel Mota 067a5b27b1
Merge branch 'lyricnz-feature/doc-updates' 3 years ago
Miguel Mota 0cfb00c2b4
Merge branch 'feature/doc-updates' of https://github.com/lyricnz/cointop into lyricnz-feature/doc-updates 3 years ago
Miguel Mota 4250a05630
Merge branch 'vuon9-fix/shift-key-problems' 3 years ago
Simon Roberts 2e0a797efb
Clarified "ssh cointop.sh" demo 3 years ago
Vuong 2a08ef0a52 Update go mod 3 years ago
Simon Roberts 7baf947c70
Combine some Features in README. Add Price Alerts and 24-bit color. 3 years ago
Vuong fee0bd9806
Set key binding if key is Uppercase rune and missing Shift
- Also revert matchEvent func
3 years ago
Vuong d0dea1ff0c
Remove binding key with shift 3 years ago
Vuong 955eb7dcef
Fix search and keybinding when using Shift 3 years ago
Simon Roberts 8b8db3bd13
Minor code style/comment cleanups; remove redundant code (#257)
* Fix comments
* Fix bug with localization param
* Remove unused modules, fix a few minor code style issues
3 years ago
Miguel Mota b8a3672ea9
Update circleci config 3 years ago
Miguel Mota 8f6b8f6835
Update Makefile 3 years ago
Miguel Mota e6eb642e37
Update circleci config 3 years ago
Miguel Mota 6c77a55f9a
Update circleci config 3 years ago
Miguel Mota 740a055eb8
docs: Update development.md 3 years ago
Miguel Mota 5e38d331aa
Merge branch 'vuon9-fix/docker-build' 3 years ago
Miguel Mota 2d3b76251a
Merge branch 'fix/docker-build' of https://github.com/vuon9/cointop into vuon9-fix/docker-build 3 years ago
Miguel Mota d9076ccd74
Update CHANGELOG.md 3 years ago
Miguel Mota fe1a5b8077
Update Makefile 3 years ago
Miguel Mota dc8f0e50eb
Merge branch 'master' into develop 3 years ago
Miguel Mota 55ab27095d
go mod tidy 3 years ago
Miguel Mota ea782f72f4
Merge branch 'master' into develop 3 years ago
Miguel Mota 4fa9a85f0c
Add circleci config 3 years ago
ѵµσɳɠ 001e2f7a71
Fix Docker build 3 years ago
Simon Roberts 9a906c3a68
Migrate from termbox-go to tcell (#232)
Lots of new functionality including:
- mouse support: click select, scroll, menus, table-sorting, etc
- 24-bit color support in themes

Co-authored-by: Simon Roberts <simon.roberts@anz.com>
Co-authored-by: ѵµσɳɠ <3168632+vuon9@users.noreply.github.com>
3 years ago
Miguel Mota 9a9ee307d4
Merge pull request #254 from vuon9/feature/types-4-search-keyword
Support search by /s:keyword - symbol, /n:keyword - name
3 years ago
Simon Roberts e26816b26c
Fix suffix on y-axis for Millions (#259) 3 years ago
Simon Roberts 19561ce300
Immediately after changing currency refresh the current coins #178 (#256) 3 years ago
Simon Roberts ac946a7d73
Reduce the number of pages to 10 (1000 coins) to reduce the load on backend... (#255)
* Reduce the number of pages to 10 (1000 coins) to reduce the load on backend, and increase refresh time. See #104 #228

* Add note about --max-pages and --per-page
3 years ago
Vuong dd1c83ee67 Update FAQ for search guide 3 years ago
Vuong f26006823b Remove hasPrefix 3 years ago
ѵµσɳɠ 6ef43d5ba0 Support search by /s:keyword - symbol, /n:keyword - name 3 years ago
Miguel Mota b921c091d6
Merge branch 'lyricnz-feature/portfolio-buy2' 3 years ago
Simon Roberts 2acbb39496
Fill in cost_price, cost, pnl, pnl_percent in "cointop holdings" output 3 years ago
Miguel Mota e99d46b424
portfolio: Fix cost/pnl hidden value if empty text. #243 3 years ago
Miguel Mota 0e956d6358
portfolio: clean up fixes #243 3 years ago
Simon Roberts b5b68833f5
Add support for purchase price/currency to portfolio (#243)
* Add support for declaring a BuyPrice and BuyCurrency in portfolio.
eg: ["Algorand", "125.4", "0.8", "USD"]

Add optional (default off) columns to portfolio:
"buy_price", "buy_currency", "profit", "profit_percent"

Note: there is no UI for entering this yet.
3 years ago
Miguel Mota 0a5ba717d8
Merge pull request #242 from lyricnz/feature/no-mouse
Add configuration for enable_mouse
3 years ago
Miguel Mota f34eb3ef8f
Merge pull request #237 from lyricnz/bugfix/hide-portfolio-balances
Fix bug with chart Y axis still showing when hidePortfolioBalances
3 years ago
Simon Roberts acd8af949d
Add configuration for enable_mouse 3 years ago
Simon Roberts d7cec61e83
When hidePortfolioBalances scale the chart to the maximum price; avoids issue with resampling and using the last value 3 years ago
Simon Roberts 30fa30c057
Use stable sort for sorting coins (#235) 3 years ago
Simon Roberts 73a00588ba
If $DEBUG_FILE is set, use that rather than /tmp/cointop.log (#236) 3 years ago
Simon Roberts 57ca7d8dba
Make default shortcuts editable (#234)
* Add missing actions
* Make default shortcuts editable
3 years ago
Miguel Mota 5064dbf353
Merge pull request #227 from lyricnz/feature/verbose-http-logging
Emit verbose HTTP logging for coingecko when DEBUG_HTTP is set
3 years ago
Miguel Mota e704e00f74
Merge pull request #225 from lyricnz/feature/show-currency-marketbar
Move currencyConversion out of chartInfo - so it shows when hideChart=true
3 years ago
Simon Roberts e49211ec71
Update faq.md 3 years ago
Simon Roberts daf131f21f
Add FAQ about $DEBUG and $DEBUG_HTTP 3 years ago
Simon Roberts 65bf1394b8
Emit verbose HTTP logging for coingecko when DEBUG_HTTP is set 3 years ago
Simon Roberts 751053f185
Revert "Emit verbose HTTP logging for coingecko when DEBUG_HTTP is set"
This reverts commit 042e2184c7.
3 years ago
Simon Roberts 042e2184c7
Emit verbose HTTP logging for coingecko when DEBUG_HTTP is set 3 years ago
Simon Roberts 781b87d95d
Move currencyConversion out of chartInfo - so it shows when hideChart=true 3 years ago
Simon Roberts 68fd858304
Change "0" to go to top coin on first page (#218)
* Add new action move_to_first_page_first_row which goes to the first coin on the first page. Bind to "0" by default, replacing first_page
3 years ago
Miguel Mota 27ad1a782d
Merge branch 'master' of github.com:cointop-sh/cointop 3 years ago
Miguel Mota 15c7707883
Update CHANGELOG.md 3 years ago
Simon Roberts b19864014e
After jumping to the right page, search for the right row (#220) 3 years ago
Simon Roberts 9fa50063e0
Allow duplicate-Symbol entries in portfolio again (#222)
* Revert PR 138 - no longer needed since PR 219 and prevents duplicate-symbol portfolio
* Rather than creating a PortfolioEntry for EVERY coin, and throwing 99% of them away, just create the PE for our actual portfolio.
3 years ago
Miguel Mota 4d3291cb55
Merge branch 'master' of github.com:cointop-sh/cointop 3 years ago
Simon Roberts e409a0bdde
Improvements to search mechanism (#216)
* Enable repeated search using empty search expression (`/` then `<enter>`)
* Quick note about how to repeat-search in the FAQ
3 years ago
Simon Roberts da06d5ef14
Remove fallback to coin.Symbol when loading portfolio. Remove deprecated favoritesBySymbol. (#219)
* Remove fallback to coin.Symbol when loading portfolio (use coin.Name)
* Remove deprecated favoritesBySymbol
3 years ago
Simon Roberts 04ee0eb5f7
Remove junk \0 from the end of search-string 3 years ago
Simon Roberts f7d997683f
Minimize diff 3 years ago
Simon Roberts b19028cca5
Reinstate levenshtein and prefix fallback 3 years ago
Simon Roberts 3add8dcaaf
Replace search function with simpler approach 3 years ago
Simon Roberts 6b6a18d38a
Pass $HTTPS_PROXY to ssh server (#205)
* Pass $HTTPS_PROXY to ssh server
* Emit additional error when failing to set HostKeyFile
3 years ago
Miguel Mota dfaa8d0c3a
Merge pull request #213 from lyricnz/bugfix/zero-left
Fix edge case with resample min-time
3 years ago
Simon Roberts e843b79ac1
Merge branch 'cointop-sh:master' into bugfix/zero-left 3 years ago
Simon Roberts cf5270623d
Fix global chart always showing in USD, and add current currency to chart name (#209)
* Use exchange-rates to convert GlobalMarketGraphData
* Ask for global data in usd just in case it starts working again :)
* Include currency conversion in chart title #207
* Better error handling
3 years ago
Simon Roberts bbf4144ebb
Fix edge case with resample min-time 3 years ago
Miguel Mota b22c040ffb
Merge pull request #211 from cointop-sh/fix/rune-width
Set currency symbol width to rune string width
3 years ago
Miguel Mota 4ba9f52a87
Merge branch 'lyricnz-feature/locale-dates' 3 years ago
Miguel Mota 8ceece82af
Set currency symbol width to rune string width 3 years ago
Simon Roberts 1713392f08
Update comment 3 years ago
Simon Roberts aece767608
Use new FormatTime for X-axis labels and last_updated 3 years ago
Simon Roberts 9e910402f5
New method to format date-time in locale and LC_TIME sensitive way 3 years ago
Simon Roberts 1cf12fd173
Add github.com/jeandeaual/go-locale for locale-detection across platform 3 years ago
Simon Roberts 11bf5e23df
Start working on locale-sensitive date-time formatting 3 years ago
Simon Roberts 1a5b4a5a09
Merge branch 'feature/locale-dates' of https://github.com/lyricnz/cointop into feature/locale-dates 3 years ago
Simon Roberts 3b37cc34c3
Scale large numbers by adding Million Billion Trillion suffix (#200)
Add option for scaling Thousand Million Billion Trillion numbers by adding suffix.
3 years ago
Simon Roberts 24f1286067
Add github.com/goodsign/monday for locale-specific date formatting 3 years ago
Simon Roberts e1aded93e8
More minor cleanups (no functional change) (#198)
* Redundant type conversions

* Remove redudant type declarations. Inline one-line constructors. Remove unnecessary brackets.

* Simplify name - it's used via package name anyway

* Simplify variable initializations

* Change `var x uint = Y` to `x := uint(Y)`

* More shorthand initialization
3 years ago
Simon Roberts ff24fb3b69
Add simple test workflow (#201)
* Add simple test workflow

* main->master
3 years ago
sgmoore 8651b20735
Update install.md (#202)
Spelling fix at lines 10 and 72
3 years ago
Simon Roberts 95c2973657
Make direct links to docs site (#194)
* Make direct links to docs site

* Fix getting-started link
3 years ago
Simon Roberts b986017d6c
DefaultCacheDir = ":PREFERRED_CACHE_HOME:/cointop" (#197) 3 years ago
Simon Roberts 9824c409ad
Feature/code cleanups (#191)
Lots of style/comment/redundancy cleanups. No functional change.
3 years ago
Miguel Mota cfc93c92c6
fix: ClearSyncMap pass by reference (#195) 3 years ago
Miguel Mota 9b03adc2bc
Merge pull request #192 from cointop-sh/fix/clean-cache-dir
fix: Delete cache_dir from config when using clean/reset commands
3 years ago
Miguel Mota d9bab7d3c2
Rename org miguelmota → cointop-sh 3 years ago
Miguel Mota a07bed9dab
command[clean]: Delete cache_dir from config 3 years ago
Miguel Mota f375eec1eb
Rename org miguelmota → cointop-sh 3 years ago
Simon Roberts 0bacbe5b9d
Make favorite character configurable (#190)
* Make favorite character configurable
3 years ago
Simon Roberts cc325b9d4e
Merge pull request #189 from lyricnz/feature/full-width
#129 Allow configurable max_chart_width (default 175, if 0 use full width)
3 years ago
Simon Roberts 95a31d5488
Save chart height after change. 3 years ago
Simon Roberts a85fb5ea50
Move maxChartWidth to [chart] section in config. Start working on persistent chart height 3 years ago
Simon Roberts 4fa05a5e88
Use constant for DefaultChartHeight 3 years ago
Simon Roberts f0631cf2de
The world is not ready for configurable MaxTableWidth 3 years ago
Simon Roberts caccc13ea0
Move maxChartWidth to ct.State 3 years ago
Simon Roberts 9a270d4b19
Allow configurable max_chart_width (default 175, if 0 use full width)
Start working on MaxTableWidth
3 years ago
Simon Roberts 562c5fd3f4
Merge pull request #184 from lyricnz/feature/x-labels
Show X axis labels on charts
3 years ago
Simon Roberts 5ef09ea40a
Handle 0/1 data items returned before resampling 3 years ago
Miguel Mota a260925900
Merge pull request #186 from lyricnz/bugfix/chart-time-ranges
Use more accurate Duration for Year and Month ranges
3 years ago
Simon Roberts 2f616e2c35
When we are not aggregating multiple prices, use the oldest data available as the left hand side of chart, not the requested start. 3 years ago
Simon Roberts 72d2c997f5
Set "All Time" to 10 Years 3 years ago
Simon Roberts 9d8843389d
Use more accurate Duration for Year and Month ranges 3 years ago
Simon Roberts 620e5c737c
Remove debug code 3 years ago
Simon Roberts 6e99e9100f
Add time labels to X axis 3 years ago
Simon Roberts c0c514ba3f
Merge pull request #180 from lyricnz/feature/resample-data
Resample portfolio data to consistent time-interval before charting
3 years ago
Simon Roberts e8fcd4a7a4
Remove interpolateData() 3 years ago
Simon Roberts ec55f0df03
Merge branch 'miguelmota:master' into feature/resample-data 3 years ago
Simon Roberts 370b9f3a56
Fix comments 3 years ago
Miguel Mota d4b6afa077
Merge pull request #181 from miguelmota/bugfix/changlog
fix typo in changelog
3 years ago
Simon Roberts f81ca9ede8
fix typo in changelog 3 years ago
Simon Roberts a2e432bb1b
Merge branch 'feature/resample-data' of https://github.com/lyricnz/cointop into feature/resample-data 3 years ago
Simon Roberts 49ac2fbc0f
Ask the chart how many data points it needs 3 years ago
Simon Roberts 162e6cd442
Merge branch 'miguelmota:master' into feature/resample-data 3 years ago
Simon Roberts 065f23eba2
Merge branch 'master' into feature/resample-data 3 years ago
Simon Roberts f38bc4ca3f
Continue from https://github.com/miguelmota/cointop/pull/165 3 years ago
Miguel Mota b6c0579b38
Remove redundant line. #166 3 years ago
Miguel Mota d17bda59f8
README: Add contributor thanks 3 years ago
Miguel Mota e5360472d1
Update CHANGELOG.md 3 years ago
Miguel Mota ac9a4d44cc
Merge branch 'lyricnz-feature/duplicate-symbols-cache-fix' 3 years ago
Miguel Mota 6a02661dc6
Merge branch 'feature/duplicate-symbols-cache-fix' of https://github.com/lyricnz/cointop into lyricnz-feature/duplicate-symbols-cache-fix 3 years ago
Miguel Mota 987fd2fba0
Merge branch 'lyricnz-bugfix/hide-portfolio-balances' 3 years ago
Miguel Mota 72805843c9
Merge branch 'bugfix/hide-portfolio-balances' of https://github.com/lyricnz/cointop into lyricnz-bugfix/hide-portfolio-balances 3 years ago
Miguel Mota 36729f8ed8
Add an extra space if it is satoshi character because it overlaps text on right 3 years ago
Miguel Mota 98a94251c4
Merge branch 'lyricnz-feature/satoshi' 3 years ago
Simon Roberts 9c86ae5de7
Use common function to generate cache keys, and fix GlobalMarketData vs Conversion caching bug 3 years ago
Simon Roberts 26981df6a5
Add currency conversion to Satoshi 3 years ago
Simon Roberts f2bab24d11
Remove portfolio balance from chart X-axis too
Hide percentChange24H when in hidePortfolioBalances mode
Update portfolio balances (private mode) screenshot
Scale portfolio chart (in hide-balances mode) so current-value is 1.0
3 years ago
Miguel Mota aba283443d
faq: Add image of hidden portfolio balances 3 years ago
Miguel Mota 83a35df5c4
Hide all holding amounts when hidden flag toggled 3 years ago
Miguel Mota 1d29363185
Add keybinding to toggle hide portfolio balances 3 years ago
Miguel Mota 5f76e89a0b
Optimize Dockerfile #127 3 years ago
Miguel Mota 42b9958770
Merge branch 'simon-anz-feature/logrus' 3 years ago
Simon Roberts dfc5ce4f21 Remove unused variable 3 years ago
Simon Roberts f7a145a002 Fix another printf log 3 years ago
Simon Roberts 6e979f0bf9 Remove leftover code 3 years ago
Simon Roberts 32990cdbaf Remove downsample code (feature WIP) 3 years ago
Simon Roberts c5445c6ffc One log.Debugf() required 3 years ago
Simon Roberts a73b9ede17 Convert ct.debuglog() to logrus.Debug() 3 years ago
Miguel Mota a86077e77e
Update debuglog method names 3 years ago
Miguel Mota 1a789cb587
go mod vendor 3 years ago
Miguel Mota 914b2e650f
Merge branch 'simon-anz-feature/migrate-from-govaluate-to-expr' 3 years ago
Simon Roberts 6d90edfefd Pass the current coin as context! 3 years ago
Simon Roberts e638040372 When evaluating, take an environment to provide as context 3 years ago
Simon Roberts 0074ecfff9 Patch expression to substitute Float64 for Integer. Update docs to include '/' 3 years ago
Simon Roberts 9e41452eed Add vendor/github.com/antonmedv 3 years ago
Simon Roberts 09b66439fc Switch to github.com/antonmedv/expr for expression evaluation 3 years ago
Miguel Mota 89bab4e2af
go mod vendor. #151 3 years ago
Miguel Mota b0dd16f813
Unbind forward slash keybinding when not in table view. #150 #149 3 years ago
Miguel Mota 08e81cabb8
Add subpackage for expression string eval 3 years ago
Miguel Mota 1f0f6d39d6
Merge branch 'simon-anz-feature/value-expressions' 3 years ago
Miguel Mota fb79419c84
Only open search if active view is table. #150 3 years ago
Miguel Mota 271cf90460
Merge branch 'feature/value-expressions' of https://github.com/simon-anz/cointop into simon-anz-feature/value-expressions 3 years ago
Miguel Mota 6286450412
Merge branch 'simon-anz-feature/30d-and-1y-change' 3 years ago
Miguel Mota 296590b466
Merge branch 'feature/30d-and-1y-change' of https://github.com/simon-anz/cointop into simon-anz-feature/30d-and-1y-change 3 years ago
Simon Roberts 31fbce6006 Remove vscode file 3 years ago
Simon Roberts 79b2fb8ea6 Change hotkey for 1Y change to "y" 3 years ago
Miguel Mota ebcb850d94
Merge branch 'simon-anz-bugfix/range-update-corrupt-cache' 3 years ago
Miguel Mota 2c7f7039fd
Merge branch 'bugfix/range-update-corrupt-cache' of https://github.com/simon-anz/cointop into simon-anz-bugfix/range-update-corrupt-cache 3 years ago
Miguel Mota bda145d2d2
Merge branch 'simon-anz-feature/document-column-selection' 3 years ago
Simon Roberts 9b6e9c472a Add FAQ comment about using expressions in portfolio entries 3 years ago
Simon Roberts b23e65cf28 Use govaluate to evaluate expressions in portfolio entry 3 years ago
Simon Roberts 7f2f4c551c Merge doc update, and add 1y_change 3 years ago
Simon Roberts 486338f26e Merge branch 'feature/document-column-selection' into feature/30d-and-1y-change 3 years ago
Simon Roberts 7467eb4e1d Use TOML markdown 3 years ago
Simon Roberts f66df5abd7 Document table column selection 3 years ago
Simon Roberts fcfb0f48fe Add support for 1Y columns - see https://github.com/miguelmota/cointop/issues/131 3 years ago
Simon Roberts b32da4010c Include currencyConversion in the cache key so changing currencies invalidates the cache. See https://github.com/miguelmota/cointop/issues/144 3 years ago
Simon Roberts e7d32f18fc Merge branch 'master' into bugfix/range-update-corrupt-cache 3 years ago
Simon Roberts b03546c4f1 Merge remote-tracking branch 'upstream/master' 3 years ago
Miguel Mota 28a7bfbbd9 Return error if default chart range is invalid 3 years ago
Simon Roberts e60bc6dcd6 Include DefaultChartRange in default config 3 years ago
Simon Roberts 142777d965 Simple documentation for default_chart_range 3 years ago
Simon Roberts b83d15cdc1 Store default chart range in configuration file 3 years ago
Simon Roberts b078dbd2f6 Use the highest-rank coin to calculate PortfolioSlice 3 years ago
Simon Roberts a34417ad61 When building portfolio slice, include first coin only 3 years ago
Simon Roberts ba75de3c00 Cache ct.State.selectedChartRange and ct.State.currencyConversion so data-fetch is not impacted by concurrent change 3 years ago
Miguel Mota 5916c49972
Makefile: Docker tag version 3 years ago
Miguel Mota ac93b8fbe0
Return error if default chart range is invalid 3 years ago
Miguel Mota 1c14561662
Merge branch 'simon-anz-feature/configurable-chart-range' 3 years ago
Miguel Mota becca5e46c
Merge branch 'feature/configurable-chart-range' of https://github.com/simon-anz/cointop into simon-anz-feature/configurable-chart-range 3 years ago
Miguel Mota 56084e44a3
Merge branch 'simon-anz-bugfix/duplicate-coins' 3 years ago
Miguel Mota d465970cbb
Merge branch 'bugfix/duplicate-coins' of https://github.com/simon-anz/cointop into simon-anz-bugfix/duplicate-coins 3 years ago
Miguel Mota 7990b9df04
Merge branch 'simon-anz-bugfix/portfolio-graph' 3 years ago
Simon Roberts 6ec915abe9 Include DefaultChartRange in default config 3 years ago
Simon Roberts 4828c3e014 Use the highest-rank coin to calculate PortfolioSlice 3 years ago
Simon Roberts 719f0cc3cb Simple documentation for default_chart_range 3 years ago
Simon Roberts 2a9f995286 Store default chart range in configuration file 3 years ago
Simon Roberts 9553ec8a02 When building portfolio slice, include first coin only 3 years ago
Simon Roberts bea5c67759 When rendering portfolio chart, only append data when resizing array 3 years ago
Miguel Mota b8b7a87f1b
Merge branch 'Pomyk-fix_filecache_locks' 3 years ago
Patryk Pomykalski ed1bcedf7b Fix filecache locking
Added mutex for map access. Removed mutex for openfile.
3 years ago
Miguel Mota 758e8367f7
Increase number of decimals shown when price < 1. #132 3 years ago
Miguel Mota f5adceea65
docs: Update colorscheme docs 3 years ago
Miguel Mota fa1fdca5e7
Row active colorscheme fix 3 years ago
Miguel Mota 96082d9089
docs: Update flatpak docs 3 years ago
Miguel Mota 2adee94c18
Remove .flathub directory (moved to https://github.com/flathub/com.github.miguelmota.Cointop) 3 years ago
Miguel Mota e7531ca635
Add keybinding to toggle chart fullscreen 3 years ago
Miguel Mota baacfbd9ca
Add 24H% change to holdings command 3 years ago
Miguel Mota f127de3048
ssh: Add support for holdings command 3 years ago
Miguel Mota 11e2efc9af
docs: Update config path documentation 3 years ago
Miguel Mota 0e778052a1
Read cointop config environment variables 3 years ago

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

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

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

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

6
.gitignore vendored

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

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

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

@ -4,55 +4,132 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.6.10] - 2021-11-06
### Added
- Search by symbol or name
- Purchase price option for portfolio entries
- Mouse support for column sorting and menu options
### Changed
- `0` keybinding to go to first row of first page
### Fixed
- Coin sorting
- Editable shortcuts
- Duplicate portfolio entries
## [1.6.9] - 2021-10-12
### Added
- Chart x-axis date labels
- Configurable favorite character
- Configurable chart width
- Save chart height
### Changed
- Renamed organization `miguelmota``cointop-sh`
### Fixed
- Global chart currency
- Chart resampling and interpolation
- Chart time periods
- Use preferred cache directory
- Currency symbol width
## [1.6.8] - 2021-09-13
### Fixed
- Hide holdings amount when using command hide flag
## [1.6.7] - 2021-09-13
### Added
- Toggle hide portfolio balances keybinding
- Evaluate expression in portfolio value edit field
- Add 1Y% change column
## [1.6.6] - 2021-08-22
### Added
- Default chart range config
### Fixed
- Duplicate coin portfolio entries
- Increase decimals places shown for small values
- Filecache locking
## [1.6.5] - 2021-04-25
### Added
- Chart fullscreen toggle keybinding
- 24% change to holdings command
- Read environment variables for config
## [1.6.4] - 2021-04-25
### Added
- Preferred cache directory
- Read host numeric monetary locale
- Column filter for holdings command
- SSH server user config type
### Fixed
- Config file path
- String rune count
## [1.6.3] - 2021-03-10
### Added
- Max pages flag
- SSH server connection max timeout
### Fixed
- Negative holdings balance input
- Coins and portfolio row selection
- Table scroll
## [1.6.2] - 2021-02-12
### Added
- Config option to keep row focus on sort
## [1.6.1] - 2021-02-12
### Added
- Multiple coin support in price command
### Fixed
- Chart data interpolation
- CoinMarketCap graph data endpoint
## [1.6.0] - 2021-02-12
### Added
- 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
@ -78,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
@ -163,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

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

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

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

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

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

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

@ -3,7 +3,7 @@ package cmd
import (
"fmt"
"github.com/miguelmota/cointop/cointop"
"github.com/cointop-sh/cointop/cointop"
"github.com/spf13/cobra"
)
@ -11,16 +11,18 @@ import (
func HoldingsCmd() *cobra.Command {
var help bool
var total bool
var percentChange24H bool
var noCache bool
var noHeader bool
var config string
var sortBy string
var sortDesc bool
var format string = "table"
var format = "table"
var humanReadable bool
var filter []string
var cols []string
var convert string
var hideBalances bool
holdingsCmd := &cobra.Command{
Use: "holdings",
@ -39,13 +41,23 @@ func HoldingsCmd() *cobra.Command {
return err
}
if total {
return ct.PrintTotalHoldings(&cointop.TablePrintOptions{
HumanReadable: humanReadable,
Format: format,
Filter: filter,
Convert: convert,
})
if total || percentChange24H {
if percentChange24H {
return ct.PrintHoldings24HChange(&cointop.TablePrintOptions{
HumanReadable: humanReadable,
Format: format,
Filter: filter,
Convert: convert,
})
}
if total {
return ct.PrintHoldingsTotal(&cointop.TablePrintOptions{
HumanReadable: humanReadable,
Format: format,
Filter: filter,
Convert: convert,
})
}
}
return ct.PrintHoldingsTable(&cointop.TablePrintOptions{
@ -57,15 +69,18 @@ func HoldingsCmd() *cobra.Command {
Cols: cols,
Convert: convert,
NoHeader: noHeader,
HideBalances: hideBalances,
})
},
}
holdingsCmd.Flags().BoolVarP(&help, "help", "", help, "Help for holdings")
holdingsCmd.Flags().BoolVarP(&total, "total", "t", total, "Show total only")
holdingsCmd.Flags().BoolVarP(&total, "total", "t", total, "Show portfolio total only")
holdingsCmd.Flags().BoolVarP(&percentChange24H, "24h", "", percentChange24H, "Show portfolio 24H change only")
holdingsCmd.Flags().BoolVarP(&noCache, "no-cache", "", noCache, "No cache")
holdingsCmd.Flags().BoolVarP(&humanReadable, "human", "h", humanReadable, "Human readable output")
holdingsCmd.Flags().BoolVarP(&noHeader, "no-header", "", noHeader, "Don't display header columns")
holdingsCmd.Flags().BoolVarP(&hideBalances, "hide-balances", "", hideBalances, "Hide portfolio balances. Useful for when sharing screen or taking screenshotss")
holdingsCmd.Flags().StringVarP(&config, "config", "c", "", fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
holdingsCmd.Flags().StringVarP(&sortBy, "sort-by", "s", sortBy, `Sort by column. Options are "name", "symbol", "price", "holdings", "balance", "24h"`)
holdingsCmd.Flags().BoolVarP(&sortDesc, "sort-desc", "d", sortDesc, "Sort in descending order")

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

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

@ -2,32 +2,39 @@ package cmd
import (
"fmt"
"os"
"strconv"
"github.com/miguelmota/cointop/cointop"
"github.com/cointop-sh/cointop/cointop"
"github.com/spf13/cobra"
)
// RootCmd ...
func RootCmd() *cobra.Command {
var version bool
var test bool
var clean bool
var reset bool
var hideMarketbar bool
var hideChart bool
var hideStatusbar bool
var onlyTable bool
var silent bool
var noCache bool
var refreshRate uint
var config string
var cmcAPIKey string
var apiChoice string
var colorscheme string
var perPage = cointop.DefaultPerPage
var maxPages = cointop.DefaultMaxPages
var cacheDir string
var colorsDir string
test := getEnvBool("COINTOP_TEST")
clean := getEnvBool("COINTOP_CLEAN")
reset := getEnvBool("COINTOP_RESET")
hideMarketbar := getEnvBool("COINTOP_HIDE_MARKETBAR")
hideChart := getEnvBool("COINTOP_HIDE_CHART")
hideTable := getEnvBool("COINTOP_HIDE_TABLE")
hideStatusbar := getEnvBool("COINTOP_HIDE_STATUSBAR")
hidePortfolioBalances := getEnvBool("COINTOP_HIDE_PORTFOLIO_BALANCES")
onlyTable := getEnvBool("COINTOP_ONLY_TABLE")
onlyChart := getEnvBool("COINTOP_ONLY_CHART")
silent := getEnvBool("COINTOP_SILENT")
noCache := getEnvBool("COINTOP_NO_CACHE")
colorscheme := os.Getenv("COINTOP_COLORSCHEME")
cacheDir := os.Getenv("COINTOP_CACHE_DIR")
colorsDir := os.Getenv("COINTOP_COLORS_DIR")
config := os.Getenv("COINTOP_CONFIG")
apiChoice := os.Getenv("COINTOP_API")
cmcAPIKey := os.Getenv("CMC_PRO_API_KEY")
coingeckoAPIKey := os.Getenv("COINGECKO_API_KEY")
coingeckoProAPIKey := os.Getenv("COINGECKO_PRO_API_KEY")
perPage := cointop.DefaultPerPage
maxPages := cointop.DefaultMaxPages
rootCmd := &cobra.Command{
Use: "cointop",
@ -55,21 +62,27 @@ See git.io/cointop for more info.`,
return nil
}
// NOTE: if reset flag enabled, reset and run cointop
if reset {
if err := cointop.Reset(&cointop.ResetConfig{
Log: !silent,
}); err != nil {
// wipe before starting program
if reset || clean {
ct, err := cointop.NewCointop(&cointop.Config{
CacheDir: cacheDir,
ConfigFilepath: config,
})
if err != nil {
return err
}
}
// NOTE: if clean flag enabled, clean and run cointop
if clean {
if err := cointop.Clean(&cointop.CleanConfig{
Log: !silent,
}); err != nil {
return err
if reset {
if err := ct.Reset(&cointop.ResetConfig{
Log: !silent,
}); err != nil {
return err
}
} else if clean {
if err := ct.Clean(&cointop.CleanConfig{
Log: !silent,
}); err != nil {
return err
}
}
}
@ -77,22 +90,34 @@ See git.io/cointop for more info.`,
if cmd.Flags().Changed("refresh-rate") {
refreshRateP = &refreshRate
}
if refreshRateP == nil {
value, ok := getEnvInt("COINTOP_REFRESH_RATE")
if ok {
uv := uint(value)
refreshRateP = &uv
}
}
ct, err := cointop.NewCointop(&cointop.Config{
CacheDir: cacheDir,
ColorsDir: colorsDir,
NoCache: noCache,
ConfigFilepath: config,
CoinMarketCapAPIKey: cmcAPIKey,
APIChoice: apiChoice,
Colorscheme: colorscheme,
HideMarketbar: hideMarketbar,
HideChart: hideChart,
HideStatusbar: hideStatusbar,
OnlyTable: onlyTable,
RefreshRate: refreshRateP,
PerPage: perPage,
MaxPages: maxPages,
CacheDir: cacheDir,
ColorsDir: colorsDir,
NoCache: noCache,
ConfigFilepath: config,
CoinMarketCapAPIKey: cmcAPIKey,
CoinGeckoAPIKey: coingeckoAPIKey,
CoinGeckoProAPIKey: coingeckoProAPIKey,
APIChoice: apiChoice,
Colorscheme: colorscheme,
HideMarketbar: hideMarketbar,
HideChart: hideChart,
HideTable: hideTable,
HideStatusbar: hideStatusbar,
OnlyTable: onlyTable,
OnlyChart: onlyChart,
RefreshRate: refreshRateP,
PerPage: perPage,
MaxPages: maxPages,
HidePortfolioBalances: hidePortfolioBalances,
})
if err != nil {
return err
@ -102,25 +127,58 @@ See git.io/cointop for more info.`,
},
}
rootCmd.Flags().BoolVarP(&version, "version", "v", false, "Display current version")
rootCmd.Flags().BoolVarP(&test, "test", "", false, "Run test (for Homebrew)")
rootCmd.Flags().BoolVarP(&clean, "clean", "", false, "Wipe clean the cache")
rootCmd.Flags().BoolVarP(&reset, "reset", "", false, "Reset the config. Make sure to backup any relevant changes first!")
rootCmd.Flags().BoolVarP(&hideMarketbar, "hide-marketbar", "", false, "Hide the top marketbar")
rootCmd.Flags().BoolVarP(&hideChart, "hide-chart", "", false, "Hide the chart view")
rootCmd.Flags().BoolVarP(&hideStatusbar, "hide-statusbar", "", false, "Hide the bottom statusbar")
rootCmd.Flags().BoolVarP(&onlyTable, "only-table", "", false, "Show only the table. Hides the chart and top and bottom bars")
rootCmd.Flags().BoolVarP(&silent, "silent", "s", false, "Silence log ouput")
rootCmd.Flags().BoolVarP(&noCache, "no-cache", "", false, "No cache")
rootCmd.Flags().BoolVarP(&version, "version", "v", version, "Display current version")
rootCmd.Flags().BoolVarP(&test, "test", "", test, "Run test (for Homebrew)")
rootCmd.Flags().BoolVarP(&clean, "clean", "", clean, "Wipe clean the cache")
rootCmd.Flags().BoolVarP(&reset, "reset", "", reset, "Reset the config. Make sure to backup any relevant changes first!")
rootCmd.Flags().BoolVarP(&hideMarketbar, "hide-marketbar", "", hideMarketbar, "Hide the top marketbar")
rootCmd.Flags().BoolVarP(&hideChart, "hide-chart", "", hideChart, "Hide the chart view")
rootCmd.Flags().BoolVarP(&hideTable, "hide-table", "", hideTable, "Hide the table view")
rootCmd.Flags().BoolVarP(&hideStatusbar, "hide-statusbar", "", hideStatusbar, "Hide the bottom statusbar")
rootCmd.Flags().BoolVarP(&hidePortfolioBalances, "hide-portfolio-balances", "", hidePortfolioBalances, "Hide portfolio balances. Useful for when sharing screen or taking screenshots")
rootCmd.Flags().BoolVarP(&onlyTable, "only-table", "", onlyTable, "Show only the table. Hides the chart and top and bottom bars")
rootCmd.Flags().BoolVarP(&onlyChart, "only-chart", "", onlyChart, "Show only the chart. Hides the table and top and bottom bars")
rootCmd.Flags().BoolVarP(&silent, "silent", "s", silent, "Silence log ouput")
rootCmd.Flags().BoolVarP(&noCache, "no-cache", "", noCache, "No cache")
rootCmd.Flags().UintVarP(&refreshRate, "refresh-rate", "r", 60, "Refresh rate in seconds. Set to 0 to not auto-refresh")
rootCmd.Flags().UintVarP(&perPage, "per-page", "", perPage, "Per page")
rootCmd.Flags().UintVarP(&maxPages, "max-pages", "", maxPages, "Max number of pages")
rootCmd.Flags().StringVarP(&config, "config", "c", "", fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
rootCmd.Flags().StringVarP(&cmcAPIKey, "coinmarketcap-api-key", "", "", "Set the CoinMarketCap API key")
rootCmd.Flags().StringVarP(&apiChoice, "api", "", "", "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"")
rootCmd.Flags().StringVarP(&colorscheme, "colorscheme", "", "", fmt.Sprintf("Colorscheme to use (default \"cointop\").\n%s", cointop.ColorschemeHelpString()))
rootCmd.Flags().StringVarP(&config, "config", "c", config, fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath))
rootCmd.Flags().StringVarP(&cmcAPIKey, "coinmarketcap-api-key", "", cmcAPIKey, "Set the CoinMarketCap Pro API key")
rootCmd.Flags().StringVarP(&coingeckoAPIKey, "coingecko-api-key", "", coingeckoAPIKey, "Set the CoinGecko Demo API key")
rootCmd.Flags().StringVarP(&coingeckoProAPIKey, "coingecko-pro-api-key", "", coingeckoProAPIKey, "Set the CoinGecko Pro API key")
rootCmd.Flags().StringVarP(&apiChoice, "api", "", apiChoice, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"")
rootCmd.Flags().StringVarP(&colorscheme, "colorscheme", "", colorscheme, fmt.Sprintf("Colorscheme to use (default \"cointop\").\n%s", cointop.ColorschemeHelpString()))
rootCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, fmt.Sprintf("Cache directory (default %s)", cointop.DefaultCacheDir))
rootCmd.Flags().StringVarP(&colorsDir, "colors-dir", "", colorsDir, "Colorschemes directory")
return rootCmd
}
func getEnvBool(key string) bool {
val := os.Getenv(key)
if val == "" {
return false
}
v, err := strconv.ParseBool(val)
if err != nil {
return false
}
return v
}
func getEnvInt(key string) (int, bool) {
val := os.Getenv(key)
if val == "" {
return 0, false
}
v, err := strconv.Atoi(val)
if err != nil {
return 0, false
}
return v, true
}

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

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

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

@ -4,6 +4,7 @@ package cointop
func ActionsMap() map[string]bool {
return map[string]bool{
"first_page": true,
"move_to_first_page_first_row": true,
"help": true,
"toggle_show_help": true,
"close_help": true,
@ -28,6 +29,7 @@ func ActionsMap() map[string]bool {
"sort_column_24h_volume": true,
"sort_column_7d_change": true,
"sort_column_30d_change": true,
"sort_column_1y_change": true,
"sort_column_asc": true,
"sort_column_available_supply": true,
"sort_column_desc": true,
@ -55,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,
}
}

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

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

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

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

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

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

@ -2,33 +2,32 @@ package cointop
import (
"fmt"
"strconv"
"strings"
"sync"
fcolor "github.com/fatih/color"
gocui "github.com/miguelmota/gocui"
xtermcolor "github.com/tomnomnom/xtermcolor"
"github.com/gdamore/tcell/v2"
)
// TODO: fix hex color support
// colorschemeColors is a map of color string names to Attribute types
type colorschemeColors map[string]interface{}
// ColorschemeColors is a map of color string names to Attribute types
type ColorschemeColors map[string]interface{}
// ISprintf is a sprintf interface
type ISprintf func(...interface{}) string
// colorCache is a map of color string names to sprintf functions
type colorCache map[string]ISprintf
// ColorCache is a map of color string names to sprintf functions
type ColorCache map[string]ISprintf
// Colorscheme is the struct for colorscheme
type Colorscheme struct {
colors colorschemeColors
cache colorCache
colors ColorschemeColors
cache ColorCache
cacheMutex sync.RWMutex
}
var fgcolorschemeColorsMap = map[string]fcolor.Attribute{
var FgColorschemeColorsMap = map[string]fcolor.Attribute{
"black": fcolor.FgBlack,
"blue": fcolor.FgBlue,
"cyan": fcolor.FgCyan,
@ -39,7 +38,7 @@ var fgcolorschemeColorsMap = map[string]fcolor.Attribute{
"yellow": fcolor.FgYellow,
}
var bgcolorschemeColorsMap = map[string]fcolor.Attribute{
var BgColorschemeColorsMap = map[string]fcolor.Attribute{
"black": fcolor.BgBlack,
"blue": fcolor.BgBlue,
"cyan": fcolor.BgCyan,
@ -50,187 +49,202 @@ 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 {
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),
cache: make(ColorCache),
cacheMutex: sync.RWMutex{},
}
}
// 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 ...
func (c *Colorscheme) Chart(a ...interface{}) string {
return c.color("chart", a...)
return c.Color("chart", a...)
}
// Marketbar ...
func (c *Colorscheme) Marketbar(a ...interface{}) string {
return c.color("marketbar", a...)
return c.Color("marketbar", a...)
}
// MarketbarSprintf ...
func (c *Colorscheme) MarketbarSprintf() ISprintf {
return c.toSprintf("marketbar")
return c.ToSprintf("marketbar")
}
// MarketbarChangeSprintf ...
func (c *Colorscheme) MarketbarChangeSprintf() ISprintf {
// NOTE: reusing table styles
return c.toSprintf("table_column_change")
return c.ToSprintf("table_column_change")
}
// MarketbarChangeDownSprintf ...
func (c *Colorscheme) MarketbarChangeDownSprintf() ISprintf {
// NOTE: reusing table styles
return c.toSprintf("table_column_change_down")
return c.ToSprintf("table_column_change_down")
}
// MarketbarChangeUpSprintf ...
func (c *Colorscheme) MarketbarChangeUpSprintf() ISprintf {
// NOTE: reusing table styles
return c.toSprintf("table_column_change_up")
return c.ToSprintf("table_column_change_up")
}
// MarketBarLabelActive ...
func (c *Colorscheme) MarketBarLabelActive(a ...interface{}) string {
return c.color("marketbar_label_active", a...)
return c.Color("marketbar_label_active", a...)
}
// Menu ...
func (c *Colorscheme) Menu(a ...interface{}) string {
return c.color("menu", a...)
return c.Color("menu", a...)
}
// MenuHeader ...
func (c *Colorscheme) MenuHeader(a ...interface{}) string {
return c.color("menu_header", a...)
return c.Color("menu_header", a...)
}
// MenuLabel ...
func (c *Colorscheme) MenuLabel(a ...interface{}) string {
return c.color("menu_label", a...)
return c.Color("menu_label", a...)
}
// MenuLabelActive ...
func (c *Colorscheme) MenuLabelActive(a ...interface{}) string {
return c.color("menu_label_active", a...)
return c.Color("menu_label_active", a...)
}
// Searchbar ...
func (c *Colorscheme) Searchbar(a ...interface{}) string {
return c.color("searchbar", a...)
return c.Color("searchbar", a...)
}
// Statusbar ...
func (c *Colorscheme) Statusbar(a ...interface{}) string {
return c.color("statusbar", a...)
return c.Color("statusbar", a...)
}
// TableColumnPrice ...
func (c *Colorscheme) TableColumnPrice(a ...interface{}) string {
return c.color("table_column_price", a...)
return c.Color("table_column_price", a...)
}
// TableColumnPriceSprintf ...
func (c *Colorscheme) TableColumnPriceSprintf() ISprintf {
return c.toSprintf("table_column_price")
return c.ToSprintf("table_column_price")
}
// TableColumnChange ...
func (c *Colorscheme) TableColumnChange(a ...interface{}) string {
return c.color("table_column_change", a...)
return c.Color("table_column_change", a...)
}
// TableColumnChangeSprintf ...
func (c *Colorscheme) TableColumnChangeSprintf() ISprintf {
return c.toSprintf("table_column_change")
return c.ToSprintf("table_column_change")
}
// TableColumnChangeDown ...
func (c *Colorscheme) TableColumnChangeDown(a ...interface{}) string {
return c.color("table_column_change_down", a...)
return c.Color("table_column_change_down", a...)
}
// TableColumnChangeDownSprintf ...
func (c *Colorscheme) TableColumnChangeDownSprintf() ISprintf {
return c.toSprintf("table_column_change_down")
return c.ToSprintf("table_column_change_down")
}
// TableColumnChangeUp ...
func (c *Colorscheme) TableColumnChangeUp(a ...interface{}) string {
return c.color("table_column_change_up", a...)
return c.Color("table_column_change_up", a...)
}
// TableColumnChangeUpSprintf ...
func (c *Colorscheme) TableColumnChangeUpSprintf() ISprintf {
return c.toSprintf("table_column_change_up")
return c.ToSprintf("table_column_change_up")
}
// TableHeader ...
func (c *Colorscheme) TableHeader(a ...interface{}) string {
return c.color("table_header", a...)
return c.Color("table_header", a...)
}
// TableHeaderSprintf ...
func (c *Colorscheme) TableHeaderSprintf() ISprintf {
return c.toSprintf("table_header")
return c.ToSprintf("table_header")
}
// TableHeaderColumnActive ...
func (c *Colorscheme) TableHeaderColumnActive(a ...interface{}) string {
return c.color("table_header_column_active", a...)
return c.Color("table_header_column_active", a...)
}
// TableHeaderColumnActiveSprintf ...
func (c *Colorscheme) TableHeaderColumnActiveSprintf() ISprintf {
return c.toSprintf("table_header_column_active")
return c.ToSprintf("table_header_column_active")
}
// TableRow ...
func (c *Colorscheme) TableRow(a ...interface{}) string {
return c.color("table_row", a...)
return c.Color("table_row", a...)
}
// TableRowSprintf ...
func (c *Colorscheme) TableRowSprintf() ISprintf {
return c.toSprintf("table_row")
return c.ToSprintf("table_row")
}
// TableRowActive ...
func (c *Colorscheme) TableRowActive(a ...interface{}) string {
return c.color("table_row_active", a...)
return c.Color("table_row_active", a...)
}
// TableRowFavorite ...
func (c *Colorscheme) TableRowFavorite(a ...interface{}) string {
return c.color("table_row_favorite", a...)
return c.Color("table_row_favorite", a...)
}
// TableRowFavoriteSprintf ...
func (c *Colorscheme) TableRowFavoriteSprintf() ISprintf {
return c.toSprintf("table_row_favorite")
return c.ToSprintf("table_row_favorite")
}
// Default ...
@ -238,31 +252,56 @@ func (c *Colorscheme) Default(a ...interface{}) string {
return fmt.Sprintf(a[0].(string), a[1:]...)
}
func (c *Colorscheme) toSprintf(name string) ISprintf {
func (c *Colorscheme) ToSprintf(name string) ISprintf {
c.cacheMutex.Lock()
defer c.cacheMutex.Unlock()
if cached, ok := c.cache[name]; ok {
return cached
}
// TODO: use c.Style(name)?
var attrs []fcolor.Attribute
if v, ok := c.colors[name+"_fg"].(string); ok {
if fg, ok := c.toFgAttr(v); ok {
if fg, ok := c.ToFgAttr(v); ok {
attrs = append(attrs, fg)
} else {
color := tcell.GetColor(v)
if color != tcell.ColorDefault {
// 24-bit foreground 38;2;⟨r⟩;⟨g⟩;⟨b⟩
r, g, b := color.RGB()
attrs = append(attrs, 38)
attrs = append(attrs, 2)
attrs = append(attrs, fcolor.Attribute(r))
attrs = append(attrs, fcolor.Attribute(g))
attrs = append(attrs, fcolor.Attribute(b))
}
}
}
if v, ok := c.colors[name+"_bg"].(string); ok {
if bg, ok := c.toBgAttr(v); ok {
if bg, ok := c.ToBgAttr(v); ok {
attrs = append(attrs, bg)
} else {
color := tcell.GetColor(v)
if color != tcell.ColorDefault {
// 24-bit background 48;2;⟨r⟩;⟨g⟩;⟨b⟩
r, g, b := color.RGB()
attrs = append(attrs, 48)
attrs = append(attrs, 2)
attrs = append(attrs, fcolor.Attribute(r))
attrs = append(attrs, fcolor.Attribute(g))
attrs = append(attrs, fcolor.Attribute(b))
}
}
}
if v, ok := c.colors[name+"_bold"].(bool); ok {
if bold, ok := c.toBoldAttr(v); ok {
if bold, ok := c.ToBoldAttr(v); ok {
attrs = append(attrs, bold)
}
}
if v, ok := c.colors[name+"_underline"].(bool); ok {
if underline, ok := c.toUnderlineAttr(v); ok {
if underline, ok := c.ToUnderlineAttr(v); ok {
attrs = append(attrs, underline)
}
}
@ -271,96 +310,80 @@ func (c *Colorscheme) toSprintf(name string) ISprintf {
return c.cache[name]
}
func (c *Colorscheme) color(name string, a ...interface{}) string {
return c.toSprintf(name)(a...)
func (c *Colorscheme) Color(name string, a ...interface{}) string {
return c.ToSprintf(name)(a...)
}
func (c *Colorscheme) gocuiFgColor(name string) gocui.Attribute {
if v, ok := c.colors[name+"_fg"].(string); ok {
if fg, ok := c.toGocuiAttr(v); ok {
return 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 {
st = st.Bold(v)
}
return gocui.ColorDefault
if v, ok := c.colors[name+"_underline"].(bool); ok {
st = st.Underline(v)
}
// TODO: Blink Dim Italic Reverse Strikethrough
return st
}
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
}
// 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
}
func (c *Colorscheme) toFgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := fgcolorschemeColorsMap[v]; ok {
return attr, true
if color, found := TcellColorschemeColorsMap[v]; found {
return color
}
if code, ok := HexToAnsi(v); ok {
return fcolor.Attribute(code), true
color := tcell.GetColor(v)
if color != tcell.ColorDefault {
return color
}
return 0, false
// find closest X11 color to RGB
// if code, ok := HexToAnsi(v); ok {
// return tcell.PaletteColor(int(code) & 0xff)
// }
return color
}
func (c *Colorscheme) toBgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := bgcolorschemeColorsMap[v]; ok {
func (c *Colorscheme) ToFgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := FgColorschemeColorsMap[v]; ok {
return attr, true
}
if code, ok := HexToAnsi(v); ok {
return fcolor.Attribute(code), true
}
// find closest X11 color to RGB
// if code, ok := HexToAnsi(v); ok {
// return fcolor.Attribute(code), true
// }
return 0, false
}
// toBoldAttr converts a boolean to an Attribute type
func (c *Colorscheme) toBoldAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Bold, v
}
// toUnderlineAttr converts a boolean to an Attribute type
func (c *Colorscheme) toUnderlineAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Underline, v
}
// toGocuiAttr converts a color string name to a gocui Attribute type
func (c *Colorscheme) toGocuiAttr(v string) (gocui.Attribute, bool) {
if attr, ok := gocuiColorschemeColorsMap[v]; ok {
func (c *Colorscheme) ToBgAttr(v string) (fcolor.Attribute, bool) {
if attr, ok := BgColorschemeColorsMap[v]; ok {
return attr, true
}
if code, ok := HexToAnsi(v); ok {
return gocui.Attribute(code), true
}
// find closest X11 color to RGB
// if code, ok := HexToAnsi(v); ok {
// return fcolor.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
// ToBoldAttr converts a boolean to an Attribute type
func (c *Colorscheme) ToBoldAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Bold, v
}
// gocui can use xterm colors
// ToUnderlineAttr converts a boolean to an Attribute type
func (c *Colorscheme) ToUnderlineAttr(v bool) (fcolor.Attribute, bool) {
return fcolor.Underline, v
}

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

@ -3,14 +3,18 @@ package cointop
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
color "github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/pad"
fcolor "github.com/fatih/color"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/mattn/go-runewidth"
log "github.com/sirupsen/logrus"
)
// FiatCurrencyNames is a mpa of currency symbols to names.
// FiatCurrencyNames is a map of currency symbols to names.
// Keep these in alphabetical order.
var FiatCurrencyNames = map[string]string{
"AUD": "Australian Dollar",
@ -55,53 +59,55 @@ var FiatCurrencyNames = map[string]string{
// CryptocurrencyNames is a map of cryptocurrency symbols to name
var CryptocurrencyNames = map[string]string{
"BTC": "Bitcoin",
"ETH": "Ethereum",
"BTC": "Bitcoin",
"ETH": "Ethereum",
"SATS": "Satoshi",
}
// CurrencySymbolMap is map of fiat currency symbols to names.
// Keep these in alphabetical order.
var CurrencySymbolMap = map[string]string{
"AUD": "$",
"BGN": "Лв.",
"BRL": "R$",
"BTC": "Ƀ",
"CAD": "$",
"CFH": "₣",
"CLP": "$",
"CNY": "¥",
"CZK": "Kč",
"DKK": "Kr",
"ETH": "Ξ",
"EUR": "€",
"GBP": "£",
"HKD": "$",
"HRK": "kn",
"HUF": "Ft",
"IDR": "Rp.",
"ILS": "₪",
"INR": "₹",
"ISK": "kr",
"JPY": "¥",
"KRW": "₩",
"MXN": "$",
"MYR": "RM",
"NOK": "kr",
"NZD": "$",
"PHP": "₱",
"PKR": "₨",
"PLN": "zł",
"RON": "lei",
"RUB": "Ꝑ",
"SEK": "kr",
"SGD": "S$",
"THB": "฿",
"TRY": "₺",
"TWD": "NT$",
"UAH": "₴",
"USD": "$",
"VND": "₫",
"ZAR": "R",
"AUD": "$",
"BGN": "Лв.",
"BRL": "R$",
"BTC": "Ƀ",
"CAD": "$",
"CFH": "₣",
"CLP": "$",
"CNY": "¥",
"CZK": "Kč",
"DKK": "Kr",
"ETH": "Ξ",
"EUR": "€",
"GBP": "£",
"HKD": "$",
"HRK": "kn",
"HUF": "Ft",
"IDR": "Rp.",
"ILS": "₪",
"INR": "₹",
"ISK": "kr",
"JPY": "¥",
"KRW": "₩",
"MXN": "$",
"MYR": "RM",
"NOK": "kr",
"NZD": "$",
"PHP": "₱",
"PKR": "₨",
"PLN": "zł",
"RON": "lei",
"RUB": "Ꝑ",
"SEK": "kr",
"SGD": "S$",
"SATS": "丰",
"THB": "฿",
"TRY": "₺",
"TWD": "NT$",
"UAH": "₴",
"USD": "$",
"VND": "₫",
"ZAR": "R",
}
var alphanumericcharacters = []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}
@ -151,8 +157,8 @@ func (ct *Cointop) SortedSupportedCurrencyConversions() []string {
// UpdateConvertMenu updates the convert menu
func (ct *Cointop) UpdateConvertMenu() error {
ct.debuglog("updateConvertMenu()")
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close ", ct.width()-24, " ")))
log.Debug("UpdateConvertMenu()")
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close ", ct.Width()-24, " ")))
helpline := " Press the corresponding key to select currency for conversion\n\n"
cnt := 0
h := ct.Views.Menu.Height()
@ -172,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)
@ -226,8 +233,12 @@ func (ct *Cointop) SetCurrencyConverstion(convert string) error {
// SetCurrencyConverstionFn sets the currency conversion function
func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
ct.debuglog("setCurrencyConverstionFn()")
log.Debug("SetCurrencyConverstionFn()")
return func() error {
if !ct.State.convertMenuVisible {
return nil
}
ct.HideConvertMenu()
if err := ct.SetCurrencyConverstion(convert); err != nil {
@ -237,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
}
@ -245,13 +256,20 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
// CurrencySymbol returns the symbol for the currency conversion
func (ct *Cointop) CurrencySymbol() string {
ct.debuglog("currencySymbol()")
return CurrencySymbol(ct.State.currencyConversion)
log.Debug("CurrencySymbol()")
symbol := CurrencySymbol(ct.State.currencyConversion)
width := runewidth.StringWidth(symbol)
if width > 1 {
symbol = pad.Right(symbol, width, " ")
}
return symbol
}
// ShowConvertMenu shows the convert menu view
func (ct *Cointop) ShowConvertMenu() error {
ct.debuglog("showConvertMenu()")
log.Debug("ShowConvertMenu()")
ct.State.convertMenuVisible = true
ct.UpdateConvertMenu()
ct.SetActiveView(ct.Views.Menu.Name())
@ -260,7 +278,7 @@ func (ct *Cointop) ShowConvertMenu() error {
// HideConvertMenu hides the convert menu view
func (ct *Cointop) HideConvertMenu() error {
ct.debuglog("hideConvertMenu()")
log.Debug("HideConvertMenu()")
ct.State.convertMenuVisible = false
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.SetActiveView(ct.Views.Table.Name())
@ -273,7 +291,7 @@ func (ct *Cointop) HideConvertMenu() error {
// ToggleConvertMenu toggles the convert menu view
func (ct *Cointop) ToggleConvertMenu() error {
ct.debuglog("toggleConvertMenu()")
log.Debug("ToggleConvertMenu()")
ct.State.convertMenuVisible = !ct.State.convertMenuVisible
if ct.State.convertMenuVisible {
return ct.ShowConvertMenu()
@ -290,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,25 +1,31 @@
package cointop
import (
"log"
"fmt"
"os"
"github.com/cointop-sh/cointop/pkg/pathutil"
log "github.com/sirupsen/logrus"
)
func (ct *Cointop) initlog() {
filename := "/tmp/cointop.log"
func (ct *Cointop) setLogConfiguration() {
if os.Getenv("DEBUG") != "" {
log.SetLevel(log.DebugLevel)
ct.setLogOutputFile()
}
}
func (ct *Cointop) setLogOutputFile() {
filename := pathutil.NormalizePath(":PREFERRED_TEMP_DIR:/cointop.log")
debugFile := os.Getenv("DEBUG_FILE")
if debugFile != "" {
filename = pathutil.NormalizePath(debugFile)
}
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
panic(err)
}
log.SetOutput(f)
ct.logfile = f
}
// debuglog writes a debug log message to /tmp/cointop.log if the DEBUG environment is set.
func (ct *Cointop) debuglog(format string, args ...interface{}) {
if !ct.debug {
return
}
log.Printf(format+"\n", args...)
fmt.Printf("Writing debug log to %s\n", filename)
}

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

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

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

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

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

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

@ -1,8 +1,7 @@
package cointop
import (
"fmt"
"strings"
log "github.com/sirupsen/logrus"
)
// TODO: break up into small functions
@ -11,20 +10,29 @@ var lastWidth int
// layout sets initial layout
func (ct *Cointop) layout() error {
ct.debuglog("layout()")
maxY := ct.height()
maxX := ct.width()
log.Debug("Layout()")
maxY := ct.Height()
maxX := ct.Width()
topOffset := 0
headerHeight := 1
marketbarHeight := ct.State.marketBarHeight
chartHeight := ct.State.chartHeight
chartHeight := ct.State.lastChartHeight
statusbarHeight := 1
if ct.State.onlyTable {
ct.State.hideMarketbar = true
ct.State.hideChart = true
ct.State.hideTable = false
ct.State.hideStatusbar = true
ct.State.onlyChart = false
marketbarHeight = 0
} else if ct.State.onlyChart {
ct.State.hideMarketbar = true
ct.State.hideChart = false
ct.State.hideTable = true
ct.State.hideStatusbar = true
ct.State.onlyTable = false
marketbarHeight = 0
}
@ -50,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())
@ -73,14 +80,21 @@ func (ct *Cointop) layout() error {
ct.Views.Chart.SetBacking(nil)
}
} else {
if err := ct.ui.SetView(ct.Views.Chart, 0, topOffset-1, maxX, topOffset+chartHeight); err != nil {
chartTopOffset := topOffset - 1
if ct.State.hideStatusbar {
chartTopOffset = topOffset
}
if ct.State.onlyChart {
chartHeight = maxY - topOffset
}
ct.State.chartHeight = chartHeight
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 := strings.ToLower(fmt.Sprintf("%s_%s", "globaldata", strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
cachekey := ct.CompositeCacheKey("globaldata", "", "", ct.State.selectedChartRange)
_, found := ct.cache.Get(cachekey)
if found {
ct.cache.Delete(cachekey)
@ -90,36 +104,48 @@ func (ct *Cointop) layout() error {
}
}
tableOffsetX := ct.State.tableOffsetX
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()))
go ct.UpdateTableHeader()
}
if ct.State.hideTable {
if ct.Views.TableHeader.Backing() != nil {
if err := ct.g.DeleteView(ct.Views.TableHeader.Name()); err != nil {
return err
}
ct.Views.TableHeader.SetBacking(nil)
}
if ct.Views.Table.Backing() != nil {
if err := ct.g.DeleteView(ct.Views.Table.Name()); err != nil {
return err
}
ct.Views.Table.SetBacking(nil)
}
} else {
tableOffsetX := ct.State.tableOffsetX
topOffset = topOffset + chartHeight
if err := ct.ui.SetView(ct.Views.TableHeader, tableOffsetX, topOffset-1, maxX, topOffset+1); err != nil {
ct.Views.TableHeader.SetFrame(false)
ct.Views.TableHeader.SetStyle(ct.colorscheme.Style(ct.Views.TableHeader.Name()))
go ct.UpdateTableHeader()
}
topOffset = topOffset + headerHeight
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"))
_, found := ct.cache.Get("allCoinsSlugMap")
if found {
ct.cache.Delete("allCoinsSlugMap")
topOffset = topOffset + headerHeight
if err := ct.ui.SetView(ct.Views.Table, tableOffsetX, topOffset-1, maxX, maxY-statusbarHeight); err != nil {
ct.Views.Table.SetFrame(false)
ct.Views.Table.SetHighlight(true)
ct.Views.Table.SetSelStyle(ct.colorscheme.Style("table_row_active"))
_, found := ct.cache.Get("allCoinsSlugMap")
if found {
ct.cache.Delete("allCoinsSlugMap")
}
go func() {
ct.UpdateCoins()
ct.UpdateTable()
}()
}
go func() {
ct.UpdateCoins()
ct.UpdateTable()
}()
}
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 {
@ -135,22 +161,19 @@ func (ct *Cointop) layout() error {
ct.Views.SearchField.SetEditable(true)
ct.Views.SearchField.SetWrap(true)
ct.Views.SearchField.SetFrame(false)
ct.Views.SearchField.SetFgColor(ct.colorscheme.gocuiFgColor("searchbar"))
ct.Views.SearchField.SetBgColor(ct.colorscheme.gocuiBgColor("searchbar"))
ct.Views.SearchField.SetStyle(ct.colorscheme.Style("searchbar"))
}
if err := ct.ui.SetView(ct.Views.Menu, 1, 1, maxX-1, maxY-1); err != nil {
ct.Views.Menu.SetFrame(false)
ct.Views.Menu.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.Menu.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
ct.Views.Menu.SetStyle(ct.colorscheme.Style("menu"))
}
if err := ct.ui.SetView(ct.Views.Input, 3, 6, 30, 8); err != nil {
ct.Views.Input.SetFrame(true)
ct.Views.Input.SetEditable(true)
ct.Views.Input.SetWrap(true)
ct.Views.Input.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.Input.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
ct.Views.Input.SetStyle(ct.colorscheme.Style("menu"))
// run only once on init.
// this bit of code should be at the bottom

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

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

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

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

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

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

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

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

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

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

@ -4,8 +4,9 @@ import (
"regexp"
"strings"
"github.com/miguelmota/cointop/pkg/levenshtein"
"github.com/miguelmota/cointop/pkg/ui"
"github.com/cointop-sh/cointop/pkg/levenshtein"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// SearchFieldView is structure for search field view
@ -13,8 +14,7 @@ type SearchFieldView = ui.View
// NewSearchFieldView returns a new search field view
func NewSearchFieldView() *SearchFieldView {
var view *SearchFieldView = ui.NewView("searchfield")
return view
return ui.NewView("searchfield")
}
// InputView is structure for help view
@ -22,13 +22,15 @@ type InputView = ui.View
// NewInputView returns a new help view
func NewInputView() *InputView {
var view *InputView = ui.NewView("input")
return view
return ui.NewView("input")
}
// OpenSearch opens the search field
func (ct *Cointop) openSearch() error {
ct.debuglog("openSearch()")
func (ct *Cointop) OpenSearch() error {
log.Debug("OpenSearch()")
if ct.ui.ActiveViewName() != ct.Views.Table.Name() {
return nil
}
ct.State.searchFieldVisible = true
ct.ui.SetCursor(true)
ct.SetActiveView(ct.Views.SearchField.Name())
@ -37,7 +39,7 @@ func (ct *Cointop) openSearch() error {
// CancelSearch closes the search field
func (ct *Cointop) CancelSearch() error {
ct.debuglog("cancelSearch()")
log.Debug("CancelSearch()")
ct.State.searchFieldVisible = false
ct.ui.SetCursor(false)
ct.SetActiveView(ct.Views.Table.Name())
@ -46,7 +48,7 @@ func (ct *Cointop) CancelSearch() error {
// DoSearch triggers the search and sets views
func (ct *Cointop) DoSearch() error {
ct.debuglog("doSearch()")
log.Debug("DoSearch()")
ct.Views.SearchField.Rewind()
b := make([]byte, 100)
n, err := ct.Views.SearchField.Read(b)
@ -64,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)
@ -76,26 +78,68 @@ func (ct *Cointop) DoSearch() error {
// Search performs the search and filtering
func (ct *Cointop) Search(q string) error {
ct.debuglog("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 {
@ -110,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
}

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

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

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

@ -2,11 +2,14 @@ package cointop
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
"github.com/miguelmota/cointop/pkg/open"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
"github.com/cointop-sh/cointop/pkg/open"
"github.com/cointop-sh/cointop/pkg/pad"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// StatusbarView is structure for statusbar view
@ -14,13 +17,12 @@ type StatusbarView = ui.View
// NewStatusbarView returns a new statusbar view
func NewStatusbarView() *StatusbarView {
var view *StatusbarView = ui.NewView("statusbar")
return view
return ui.NewView("statusbar")
}
// UpdateStatusbar updates the statusbar view
func (ct *Cointop) UpdateStatusbar(s string) error {
ct.debuglog("UpdateStatusbar()")
log.Debug("UpdateStatusbar()")
currpage := ct.CurrentDisplayPage()
totalpages := ct.TotalPagesDisplay()
var quitText string
@ -52,7 +54,7 @@ func (ct *Cointop) UpdateStatusbar(s string) error {
content = fmt.Sprintf("%s %s[+]Add", helpStr, editStr)
} else {
base := fmt.Sprintf("%s %sChart %sRange %sSearch %sConvert %s %s", helpStr, "[Enter]", "[[ ]]", "[/]", "[C]", favoritesText, portfolioText)
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.width(), " ")
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.Width(), " ")
v := ct.Version()
size := utf8.RuneCountInString(str)
end := size - utf8.RuneCountInString(v) + 2
@ -72,7 +74,7 @@ func (ct *Cointop) UpdateStatusbar(s string) error {
// RefreshRowLink updates the row link in the statusbar
func (ct *Cointop) RefreshRowLink() error {
ct.debuglog("RefreshRowLink()")
log.Debug("RefreshRowLink()")
var shortcut string
if !open.CommandExists() {
shortcut = "[O]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
}

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

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

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

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

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

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

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

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

@ -31,6 +31,8 @@ Press <kbd>Ctrl</kbd>+<kbd>j</kbd> to increase the chart height.
Press <kbd>Ctrl</kbd>+<kbd>k</kbd> to decrease the chart height.
Press <kbd>|</kbd> to toggle chart fullscreen.
## Hide chart
Run cointop with the `--hide-chart` flag to always keep the chart hidden.

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

@ -11,20 +11,42 @@ The first time you run cointop, it'll create a config file in:
~/.config/cointop/config.toml
```
You can then configure the actions you want for each key:
On Unix systems, the default config path is `$XDG_CONFIG_HOME/cointop/config.toml`
(default `~/.config/cointop/config.toml`)
On macOS (darwin), the default config path is `$HOME/Library/Application Support/cointop/config.toml`
On Windows, the default config path is `%AppData%\cointop\config.toml`
_Note: The config directory is determined by [`os.UserConfigDir()`](https://pkg.go.dev/os#UserConfigDir)_
You may specify a different config file to use by using the `--config` flag:
```bash
cointop --config="/path/to/config.toml"
```
Alternatively, you can set the config file path via the environment variable `COINTOP_CONFIG`
```bash
export COINTOP_CONFIG="/path/to/config.toml"
cointop
```
## Key bindings
You can configure the actions you want for each key in `config.toml`:
```toml
currency = "USD"
default_view = ""
default_chart_range = "1Y"
api = "coingecko"
colorscheme = "cointop"
refresh_rate = 60
[shortcuts]
"$" = "last_page"
0 = "first_page"
0 = "move_to_first_page_first_row"
1 = "sort_column_1h_change"
2 = "sort_column_24h_change"
7 = "sort_column_7d_change"
@ -101,22 +123,20 @@ refresh_rate = 60
[coinmarketcap]
pro_api_key = ""
```
You may specify a different config file to use by using the `--config` flag:
```bash
cointop --config="/path/to/config.toml"
[coingecko]
pro_api_key = ""
```
## List of actions
This are the action keywords you may use in the config file to change what the shortcut keys do.
This are the action keywords you may use in the config file to change what the shortcut keys do:
Action|Description
----|------|
`first_chart_range`|Select first chart date range (e.g. 24H)
`first_page`|Go to first page
`move_to_first_page_first_row`|Go to first row on the first page
`enlarge_chart`|Increase chart height
`help`|Show help
`hide_currency_convert_menu`|Hide currency convert menu

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

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

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

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

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

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

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

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

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

745
go.sum

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

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

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

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

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

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

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

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

@ -29,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) {
@ -60,7 +64,7 @@ func FormatRank(rank interface{}) int {
// FormatPrice formats the price value
func FormatPrice(price float64, convert string) float64 {
convert = strings.ToUpper(convert)
pricestr := fmt.Sprintf("%.2f", price)
pricestr := fmt.Sprintf("%.5f", price)
if convert == "ETH" || convert == "BTC" || price < 1 {
pricestr = fmt.Sprintf("%.8f", price)
}

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save