You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cointop/cointop/keybindings.go

419 lines
12 KiB
Go

package cointop
import (
"strings"
"unicode"
"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{}, 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 := 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
}
}
// Then try one-rune variants
if len(keyName) == 1 {
r := []rune(keyName)
key = r[0]
return key, mod
}
if key == nil {
log.Debugf("Could not map key '%s' to key", s)
}
return key, mod
}
// 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 err := ct.SetKeybindingAction(k, v); err != nil {
return err
}
}
// keys to force 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(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(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(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(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(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.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.MouseLeftClick), ct.Views.Table.Name()) // click to focus
// clicking table headers sorts table
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.TableHeaderMouseLeftClick), ct.Views.TableHeader.Name())
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.StatusbarMouseLeftClick), ct.Views.Statusbar.Name())
// debug mouse clicks
ct.SetMousebindingMod(tcell.Button1, tcell.ModNone, ct.Keyfn(ct.MouseDebug), "")
ct.SetMousebindingMod(tcell.WheelUp, tcell.ModNone, ct.Keyfn(ct.CursorUpOrPreviousPage), ct.Views.Table.Name())
ct.SetMousebindingMod(tcell.WheelDown, tcell.ModNone, ct.Keyfn(ct.CursorDownOrNextPage), ct.Views.Table.Name())
// character key press to select option
// TODO: use scrolling table
keys := ct.SortedSupportedCurrencyConversions()
for i, k := range keys {
ct.SetKeybindingMod(alphanumericcharacters[i], 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 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 tcell.Key:
err = ct.g.SetKeybinding(view, t, 0, mod, callback)
case rune:
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
}
// 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 {
if fn != nil {
return fn()
}
return nil
}
}
// handleHkey handles the h key
func (ct *Cointop) handleHkey(key interface{}) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if k, ok := key.(rune); ok && k == 'h' && ct.IsPortfolioVisible() {
ct.SortToggle("holdings", true)
} else {
ct.PrevPage()
}
return nil
}
}
// Noop is a no-operation function
func (ct *Cointop) Noop() error {
return nil
}