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