diff --git a/README.md b/README.md
index 30f0c68..8eae6f4 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,19 @@
[`cointop`](https://github.com/miguelmota/cointop) is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time. The interface is inspired by [`htop`](https://en.wikipedia.org/wiki/Htop).
+## Table of Contents
+
+- [Features](#features)
+- [Install](#install)
+- [Updating](#updating)
+- [Usage](#usage)
+- [Shortcuts](#shortcuts)
+- [Config](#config)
+- [Actions](#actions)
+- [FAQ](#faq)
+- [Authors](#authors)
+- [License](#license)
+
## Features
- Quick sort shortcuts
@@ -15,6 +28,7 @@
- Pagination
- 7 day charts
- Color coded
+- Custom key bindings
#### Future releases
@@ -23,7 +37,6 @@
- Currency conversion (i.e. Euro, Yen)
- Markets/Exchanges
- CryptoCompare API
-- Custom shortcuts
## Install
@@ -51,11 +64,13 @@ sudo curl -s "https://raw.githubusercontent.com/miguelmota/cointop/master/instal
## Usage
+Just run the `cointop` command to get started:
+
```bash
$ cointop
```
-### Shortcuts
+## Shortcuts
List of default shortcut keys:
@@ -100,6 +115,7 @@ Key|Action
m|Sort table by *[m]arket cap*
M|Go to middle of visible table window (vim style)
n|Sort table by *[n]ame*
+o|[o]pen row link
p|Sort table by *[p]rice*
r|Sort table by *[r]ank*
s|Sort table by *[s]ymbol*
@@ -110,6 +126,110 @@ Key|Action
$|Go to last page (vim style)
?|Show help|
+## Config
+
+The first time you run cointop, it'll create a config file in:
+
+```
+~/.cointop/config
+```
+
+You can then configure the actions you want for each key:
+
+(default `~/.cointop/config`)
+
+```toml
+[shortcuts]
+ "$" = "last_page"
+ 0 = "first_page"
+ 1 = "sort_column_1h_change"
+ 2 = "sort_column_24h_change"
+ 7 = "sort_column_7d_change"
+ "?" = "help"
+ G = "move_to_page_last_row"
+ H = "move_to_page_visible_first_row"
+ L = "move_to_page_visible_last_row"
+ M = "move_to_page_visible_middle_row"
+ a = "sort_column_available_supply"
+ "alt+arrowdown" = "sort_column_desc"
+ "alt+arrowleft" = "sort_left_column"
+ "alt+arrowright" = "sort_right_column"
+ "alt+arrowup" = "sort_column_asc"
+ arrowdown = "move_down"
+ arrowleft = "previous_page"
+ arrowright = "next_page"
+ arrowup = "move_up"
+ c = "toggle_row_chart"
+ "ctrl+c" = "quit"
+ "ctrl+d" = "page_down"
+ "ctrl+n" = "next_page"
+ "ctrl+p" = "previous_page"
+ "ctrl+r" = "refresh"
+ "ctrl+u" = "page_up"
+ end = "move_to_page_last_row"
+ enter = "open_link"
+ esc = "quit"
+ f1 = "help"
+ g = "move_to_page_first_row"
+ h = "previous_page"
+ home = "move_to_page_first_row"
+ j = "move_down"
+ k = "move_up"
+ l = "next_page"
+ m = "sort_column_market_cap"
+ n = "sort_column_name"
+ o = "open_link"
+ p = "sort_column_price"
+ pagedown = "page_down"
+ pageup = "page_up"
+ q = "quit"
+ r = "sort_column_rank"
+ s = "sort_column_symbol"
+ space = "open_link"
+ t = "sort_column_total_supply"
+ u = "sort_column_last_updated"
+ v = "sort_column_24h_volume"
+```
+
+## List of actions
+
+Action|Description
+----|------|
+`first_page`|Go to first page
+`help`|Show help
+`last_page`|Go to last page
+`move_to_page_first_row`|Move to first row on page
+`move_to_page_last_row`|Move to last row on page
+`move_to_page_visible_first_row`|Move to first visible row on page
+`move_to_page_visible_last_row`|Move to last visible row on page
+`move_to_page_visible_middle_row`|Move to middle visible row on page
+`move_up`|Move one row up
+`move_down`|Move one row down
+`next_page`|Go to next page
+`open_link`|Open row link
+`page_down`|Move one row down
+`page_up`|Scroll one page up
+`previous_page`|Go to previous page
+`quit`|Quit application
+`refresh`|Do a manual refresh on the data
+`sort_column_1h_change`|Sort table by column *1 hour change*
+`sort_column_24h_change`|Sort table by column *24 hour change*
+`sort_column_24h_volume`|Sort table by column *24 hour volume*
+`sort_column_7d_change`|Sort table by column *7 day change*
+`sort_column_asc`|Sort highlighted column by ascending order
+`sort_column_available_supply`|Sort table by column *available supply*
+`sort_column_desc`|Sort highlighted column by descending order
+`sort_column_last_updated`|Sort table by column *last updated*
+`sort_column_market_cap`|Sort table by column *market cap*
+`sort_column_name`|Sort table by column *name*
+`sort_column_price`|Sort table by column *price*
+`sort_column_rank`|Sort table by column *rank*
+`sort_column_symbol`|Sort table by column *symbol*
+`sort_column_total_supply`|Sort table by column *total supply*
+`sort_left_column`|Sort the column to the left of the highlighted column
+`sort_right_column`|Sort the column to the right of the highlighted column
+`toggle_row_chart`|Toggle the chart for the highlighted row
+
## FAQ
- Q: Where is the data from?
diff --git a/cointop/cointop.go b/cointop/cointop.go
index 215c073..ed3494e 100644
--- a/cointop/cointop.go
+++ b/cointop/cointop.go
@@ -35,20 +35,13 @@ type Cointop struct {
forcerefresh chan bool
selectedcoin *apt.Coin
maxtablewidth int
+ shortcutkeys map[string]string
+ config config // toml config
}
// Run runs cointop
func Run() {
- g, err := gocui.NewGui(gocui.Output256)
- if err != nil {
- log.Fatalf("new gocui: %v", err)
- }
- defer g.Close()
- g.Cursor = true
- g.Mouse = true
- g.Highlight = true
ct := Cointop{
- g: g,
api: api.NewCMC(),
refreshticker: time.NewTicker(1 * time.Minute),
sortby: "rank",
@@ -57,7 +50,18 @@ func Run() {
perpage: 100,
forcerefresh: make(chan bool),
maxtablewidth: 175,
+ shortcutkeys: defaultShortcuts(),
}
+ _ = ct.setupConfig()
+ g, err := gocui.NewGui(gocui.Output256)
+ if err != nil {
+ log.Fatalf("new gocui: %v", err)
+ }
+ ct.g = g
+ defer g.Close()
+ g.Cursor = true
+ g.Mouse = true
+ g.Highlight = true
g.SetManagerFunc(ct.layout)
if err := ct.keybindings(g); err != nil {
log.Fatalf("keybindings: %v", err)
diff --git a/cointop/config.go b/cointop/config.go
new file mode 100644
index 0000000..fc6cf83
--- /dev/null
+++ b/cointop/config.go
@@ -0,0 +1,125 @@
+package cointop
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/user"
+ "strings"
+
+ "github.com/BurntSushi/toml"
+)
+
+type config struct {
+ Shortcuts map[string]interface{} `toml:"shortcuts"`
+}
+
+func (ct *Cointop) setupConfig() error {
+ err := ct.makeConfigDir()
+ if err != nil {
+ return err
+ }
+ err = ct.makeConfigFile()
+ if err != nil {
+ return err
+ }
+ err = ct.parseConfig()
+ if err != nil {
+ return err
+ }
+ err = ct.loadShortcutsFromConfig()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (ct *Cointop) configDirPath() string {
+ usr, err := user.Current()
+ if err != nil {
+ return ".cointop"
+ }
+
+ return fmt.Sprintf("%s%s", usr.HomeDir, "/.cointop")
+}
+
+func (ct *Cointop) configPath() string {
+ return fmt.Sprintf("%v%v", ct.configDirPath(), "/config")
+}
+
+func (ct *Cointop) makeConfigDir() error {
+ path := ct.configDirPath()
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ _ = os.Mkdir(path, os.ModePerm)
+ }
+ return nil
+}
+
+func (ct *Cointop) makeConfigFile() error {
+ path := ct.configPath()
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ fo, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer fo.Close()
+ b, err := ct.configToToml()
+ if err != nil {
+ return err
+ }
+ if _, err := fo.Write(b); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (ct *Cointop) parseConfig() error {
+ var conf config
+ path := ct.configPath()
+ if _, err := toml.DecodeFile(path, &conf); err != nil {
+ return err
+ }
+
+ ct.config = conf
+ return nil
+}
+
+func (ct *Cointop) configToToml() ([]byte, error) {
+ s := defaultShortcuts()
+ ifcs := map[string]interface{}{}
+ for k, v := range s {
+ var i interface{} = v
+ ifcs[k] = i
+ }
+ var inputs = &config{
+ Shortcuts: ifcs,
+ }
+
+ var b bytes.Buffer
+ encoder := toml.NewEncoder(&b)
+ err := encoder.Encode(inputs)
+ if err != nil {
+ return nil, err
+ }
+
+ return b.Bytes(), nil
+}
+
+func (ct *Cointop) loadShortcutsFromConfig() error {
+ actionsmap := actionsMap()
+ for k, ifc := range ct.config.Shortcuts {
+ k = strings.ToLower(k)
+ v, ok := ifc.(string)
+ if ok {
+ if !actionsmap[k] {
+ continue
+ }
+ if ct.shortcutkeys[k] == "" {
+ continue
+ }
+ ct.shortcutkeys[k] = v
+ }
+ }
+ return nil
+}
diff --git a/cointop/keybindings.go b/cointop/keybindings.go
index b53b9b3..8681298 100644
--- a/cointop/keybindings.go
+++ b/cointop/keybindings.go
@@ -6,58 +6,6 @@ import (
"github.com/jroimartin/gocui"
)
-// defaults
-var shortcutkeys = map[string]string{
- "arrowup": "moveup",
- "arrowdown": "movedown",
- "arrowleft": "prevpage",
- "arrowright": "nextpage",
- "pagedown": "pagedown",
- "pageup": "pageup",
- "home": "movepagefirstrow",
- "end": "movepagelastrow",
- "enter": "openlink",
- "esc": "quit",
- "space": "openlink",
- "ctrl+c": "quit",
- "ctrl+d": "pagedown",
- "ctrl+n": "nextpage",
- "ctrl+p": "prevpage",
- "ctrl+r": "refresh",
- "ctrl+u": "pageup",
- "alt+arrowup": "sortcolasc",
- "alt+arrowdown": "sortcoldesc",
- "alt+arrowleft": "sortleftcol",
- "alt+arrowright": "sortrightcol",
- "f1": "help",
- "0": "movefirstpage",
- "1": "sortcol1hchange",
- "2": "sortcol24hchange",
- "7": "sortcol7dchange",
- "a": "sortcolavailablesupply",
- "c": "togglerowchart",
- "g": "movepagefirstrow",
- "G": "movepagelastrow",
- "h": "prevpage",
- "H": "movepagevisiblefirstrow",
- "j": "movedown",
- "k": "moveup",
- "l": "nextpage",
- "L": "movepagevisiblelastrow",
- "m": "sortcolmarketcap",
- "M": "movepagevisiblemiddlerow",
- "n": "sortcolname",
- "p": "sortcolprice",
- "r": "sortcolrank",
- "s": "sortcolsymbol",
- "t": "sortcoltotalsupply",
- "u": "sortcollastupdated",
- "v": "sortcol24hvolume",
- "q": "quit",
- "$": "movelastpage",
- "?": "help",
-}
-
func (ct *Cointop) parseKeys(s string) (interface{}, gocui.Modifier) {
var key interface{}
mod := gocui.ModNone
@@ -238,75 +186,76 @@ func (ct *Cointop) parseKeys(s string) (interface{}, gocui.Modifier) {
}
func (ct *Cointop) keybindings(g *gocui.Gui) error {
- for k, v := range shortcutkeys {
+ for k, v := range ct.shortcutkeys {
+ v = strings.TrimSpace(strings.ToLower(v))
var fn func(g *gocui.Gui, v *gocui.View) error
key, mod := ct.parseKeys(k)
switch v {
- case "moveup":
+ case "move_up":
fn = ct.cursorUp
- case "movedown":
+ case "move_down":
fn = ct.cursorDown
- case "prevpage":
+ case "previous_page":
fn = ct.prevPage
- case "nextpage":
+ case "next_page":
fn = ct.nextPage
- case "pagedown":
+ case "page_down":
fn = ct.pageDown
- case "pageup":
+ case "page_up":
fn = ct.pageUp
- case "sortcolsymbol":
+ case "sort_column_symbol":
fn = ct.sortfn("symbol", false)
- case "movepagefirstrow":
+ case "move_to_page_first_row":
fn = ct.navigateFirstLine
- case "movepagelastrow":
+ case "move_to_page_last_row":
fn = ct.navigateLastLine
- case "openlink":
+ case "open_link":
fn = ct.openLink
case "refresh":
fn = ct.refresh
- case "sortcolasc":
+ case "sort_column_asc":
fn = ct.sortAsc
- case "sortcoldesc":
+ case "sort_column_desc":
fn = ct.sortDesc
- case "sortleftcol":
+ case "sort_left_column":
fn = ct.sortPrevCol
- case "sortrightcol":
+ case "sort_right_column":
fn = ct.sortNextCol
case "help":
fn = ct.openHelp
- case "movefirstpage":
+ case "first_page":
fn = ct.firstPage
- case "sortcol1hchange":
+ case "sort_column_1h_change":
fn = ct.sortfn("1hchange", true)
- case "sortcol24hchange":
+ case "sort_column_24h_change":
fn = ct.sortfn("24hchange", true)
- case "sortcol7dchange":
+ case "sort_column_7d_change":
fn = ct.sortfn("7dchange", true)
- case "sortcolavailablesupply":
+ case "sort_column_available_supply":
fn = ct.sortfn("availablesupply", true)
- case "togglerowchart":
+ case "toggle_row_chart":
fn = ct.toggleCoinChart
- case "movepagevisiblefirstrow":
+ case "move_to_page_visible_first_row":
fn = ct.navigatePageFirstLine
- case "movepagevisiblelastrow":
+ case "move_to_page_visible_last_row":
fn = ct.navigatePageLastLine
- case "sortcolmarketcap":
+ case "sort_column_market_cap":
fn = ct.sortfn("marketcap", true)
- case "movepagevisiblemiddlerow":
+ case "move_to_page_visible_middle_row":
fn = ct.navigatePageMiddleLine
- case "sortcolname":
+ case "sort_column_name":
fn = ct.sortfn("name", true)
- case "sortcolprice":
+ case "sort_column_price":
fn = ct.sortfn("price", true)
- case "sortcolrank":
+ case "sort_column_rank":
fn = ct.sortfn("rank", false)
- case "sortcoltotalsupply":
+ case "sort_column_total_supply":
fn = ct.sortfn("totalsupply", true)
- case "sortcollastupdated":
+ case "sort_column_last_updated":
fn = ct.sortfn("lastupdated", true)
- case "sortcol24hvolume":
+ case "sort_column_24h_volume":
fn = ct.sortfn("24hvolume", true)
- case "movelastpage":
+ case "last_page":
fn = ct.lastPage
case "quit":
fn = ct.quit
diff --git a/cointop/shortcuts.go b/cointop/shortcuts.go
new file mode 100644
index 0000000..7f808e0
--- /dev/null
+++ b/cointop/shortcuts.go
@@ -0,0 +1,94 @@
+package cointop
+
+func actionsMap() map[string]bool {
+ return map[string]bool{
+ "first_page": true,
+ "help": true,
+ "last_page": true,
+ "move_to_page_first_row": true,
+ "move_to_page_last_row": true,
+ "move_to_page_visible_first_row": true,
+ "move_to_page_visible_last_row": true,
+ "move_to_page_visible_middle_row": true,
+ "move_up": true,
+ "move_down": true,
+ "next_page": true,
+ "open_link": true,
+ "page_down": true,
+ "page_up": true,
+ "previous_page": true,
+ "quit": true,
+ "refresh": true,
+ "sort_column_1h_change": true,
+ "sort_column_24h_change": true,
+ "sort_column_24h_volume": true,
+ "sort_column_7d_change": true,
+ "sort_column_asc": true,
+ "sort_column_available_supply": true,
+ "sort_column_desc": true,
+ "sort_column_last_updated": true,
+ "sort_column_market_cap": true,
+ "sort_column_name": true,
+ "sort_column_price": true,
+ "sort_column_rank": true,
+ "sort_column_symbol": true,
+ "sort_column_total_supply": true,
+ "sort_left_column": true,
+ "sort_right_column": true,
+ "toggle_row_chart": true,
+ }
+}
+
+func defaultShortcuts() map[string]string {
+ return map[string]string{
+ "arrowup": "move_up",
+ "arrowdown": "move_down",
+ "arrowleft": "previous_page",
+ "arrowright": "next_page",
+ "pagedown": "page_down",
+ "pageup": "page_up",
+ "home": "move_to_page_first_row",
+ "end": "move_to_page_last_row",
+ "enter": "open_link",
+ "esc": "quit",
+ "space": "open_link",
+ "ctrl+c": "quit",
+ "ctrl+d": "page_down",
+ "ctrl+n": "next_page",
+ "ctrl+p": "previous_page",
+ "ctrl+r": "refresh",
+ "ctrl+u": "page_up",
+ "alt+arrowup": "sort_column_asc",
+ "alt+arrowdown": "sort_column_desc",
+ "alt+arrowleft": "sort_left_column",
+ "alt+arrowright": "sort_right_column",
+ "f1": "help",
+ "0": "first_page",
+ "1": "sort_column_1h_change",
+ "2": "sort_column_24h_change",
+ "7": "sort_column_7d_change",
+ "a": "sort_column_available_supply",
+ "c": "toggle_row_chart",
+ "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",
+ "p": "sort_column_price",
+ "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",
+ "$": "last_page",
+ "?": "help",
+ }
+}