From 97facbe48daf2c234203bb886f81fc269ee27f9d Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Sun, 9 Aug 2020 18:42:04 -0700 Subject: [PATCH] Add command to print holdings --- README.md | 18 +- cointop/cmd/clean.go | 29 +++ cointop/cmd/cmd.go | 223 ++---------------------- cointop/cmd/holdings.go | 42 +++++ cointop/cmd/price.go | 30 ++++ cointop/cmd/reset.go | 29 +++ cointop/cmd/root.go | 108 ++++++++++++ cointop/cmd/server.go | 44 +++++ cointop/cmd/test.go | 33 ++++ cointop/cmd/version.go | 20 +++ cointop/cointop.go | 15 +- cointop/common/asciitable/asciitable.go | 62 +++++++ cointop/navigation.go | 16 ++ cointop/portfolio.go | 51 ++++++ cointop/size.go | 4 + cointop/table.go | 4 + cointop/update.go | 2 +- go.mod | 1 + go.sum | 2 + 19 files changed, 511 insertions(+), 222 deletions(-) create mode 100644 cointop/cmd/clean.go create mode 100644 cointop/cmd/holdings.go create mode 100644 cointop/cmd/price.go create mode 100644 cointop/cmd/reset.go create mode 100644 cointop/cmd/root.go create mode 100644 cointop/cmd/server.go create mode 100644 cointop/cmd/test.go create mode 100644 cointop/cmd/version.go create mode 100644 cointop/common/asciitable/asciitable.go diff --git a/README.md b/README.md index d7d7ca3..353348d 100644 --- a/README.md +++ b/README.md @@ -240,11 +240,12 @@ go get -u github.com/miguelmota/cointop brew uninstall cointop && brew install cointop ``` -### Flatpak (Linux) +### Snap (Ubuntu) + +Use the `refresh` command to update snap. ```bash -sudo flatpak uninstall com.github.miguelmota.Cointop -sudo flatpak install flathub com.github.miguelmota.Cointop +sudo snap refresh cointop ``` ### Copr (Fedora) @@ -253,12 +254,17 @@ sudo flatpak install flathub com.github.miguelmota.Cointop sudo dnf update cointop ``` -### Snap (Ubuntu) +### AUR (Arch Linux) -Use the `refresh` command to update snap. +```bash +yay -S cointop +``` + +### Flatpak (Linux) ```bash -sudo snap refresh cointop --stable +sudo flatpak uninstall com.github.miguelmota.Cointop +sudo flatpak install flathub com.github.miguelmota.Cointop ``` ## Getting started diff --git a/cointop/cmd/clean.go b/cointop/cmd/clean.go new file mode 100644 index 0000000..14558b2 --- /dev/null +++ b/cointop/cmd/clean.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "github.com/miguelmota/cointop/cointop" + "github.com/miguelmota/cointop/cointop/common/filecache" + "github.com/spf13/cobra" +) + +// CleanCmd ... +func CleanCmd() *cobra.Command { + cacheDir := filecache.DefaultCacheDir + + 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{ + Log: true, + CacheDir: cacheDir, + }) + }, + } + + cleanCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory") + + return cleanCmd +} diff --git a/cointop/cmd/cmd.go b/cointop/cmd/cmd.go index 5871700..9647569 100644 --- a/cointop/cmd/cmd.go +++ b/cointop/cmd/cmd.go @@ -1,222 +1,19 @@ package cmd -import ( - "fmt" - "time" - - "github.com/miguelmota/cointop/cointop" - "github.com/miguelmota/cointop/cointop/common/filecache" - cssh "github.com/miguelmota/cointop/cointop/ssh" - "github.com/spf13/cobra" -) - // Execute executes the program func Execute() { - var version, test, clean, reset, hideMarketbar, hideChart, hideStatusbar, onlyTable, silent, noCache bool - var refreshRate uint - var config, cmcAPIKey, apiChoice, colorscheme, coin, currency string - cacheDir := filecache.DefaultCacheDir - perPage := cointop.DefaultPerPage - - rootCmd := &cobra.Command{ - Use: "cointop", - Short: "Cointop is an interactive terminal based app for tracking cryptocurrencies", - Long: ` - _ _ - ___ ___ (_)_ __ | |_ ___ _ __ - / __/ _ \| | '_ \| __/ _ \| '_ \ -| (_| (_) | | | | | || (_) | |_) | - \___\___/|_|_| |_|\__\___/| .__/ - |_| - -Cointop is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time. - -See git.io/cointop for more info.`, - RunE: func(cmd *cobra.Command, args []string) error { - if version { - cointop.PrintVersion() - return nil - } - - if test { - // TODO: deprecate test flag, only have test command - doTest() - return nil - } - - // NOTE: if reset flag enabled, reset and run cointop - if reset { - if err := cointop.Reset(&cointop.ResetConfig{ - Log: !silent, - }); 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 - } - } - - var refreshRateP *uint - if cmd.Flags().Changed("refresh-rate") { - refreshRateP = &refreshRate - } - - ct, err := cointop.NewCointop(&cointop.Config{ - CacheDir: cacheDir, - NoCache: noCache, - ConfigFilepath: config, - CoinMarketCapAPIKey: cmcAPIKey, - APIChoice: apiChoice, - Colorscheme: colorscheme, - HideMarketbar: hideMarketbar, - HideChart: hideChart, - HideStatusbar: hideStatusbar, - OnlyTable: onlyTable, - RefreshRate: refreshRateP, - PerPage: perPage, - }) - if err != nil { - return err - } - - return ct.Run() - }, - } - - 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().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().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", "", cointop.CoinGecko, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"") - rootCmd.Flags().StringVarP(&colorscheme, "colorscheme", "", "", "Colorscheme to use (default \"cointop\"). To install standard themes, do:\n\ngit clone git@github.com:cointop-sh/colors.git ~/.config/cointop/colors\n\nSee git.io/cointop#colorschemes for more info.") - rootCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", "/tmp", "Cache directory") - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Displays the current version", - Long: `The version command displays the current version`, - Run: func(cmd *cobra.Command, args []string) { - cointop.PrintVersion() - }, - } - - 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{ - Log: true, - CacheDir: cacheDir, - }) - }, - } - - cleanCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory") - - 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{ - Log: true, - CacheDir: cacheDir, - }) - }, - } - - resetCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory") - - priceCmd := &cobra.Command{ - Use: "price", - Short: "Displays the current price of a coin", - Long: `The price command display the current price of a coin`, - RunE: func(cmd *cobra.Command, args []string) error { - return cointop.PrintPrice(&cointop.PriceConfig{ - Coin: coin, - Currency: currency, - APIChoice: apiChoice, - }) - }, - } - - testCmd := &cobra.Command{ - Use: "test", - Short: "Runs tests", - Long: `The test command runs tests for Homebrew`, - Run: func(cmd *cobra.Command, args []string) { - doTest() - }, - } - - priceCmd.Flags().StringVarP(&coin, "coin", "c", "bitcoin", "Full name of the coin (default \"bitcoin\")") - priceCmd.Flags().StringVarP(¤cy, "currency", "f", "USD", "The currency to convert to (default \"USD\")") - priceCmd.Flags().StringVarP(&apiChoice, "api", "a", cointop.CoinGecko, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"") - - var port uint = 22 - var address string = "0.0.0.0" - var idleTimeout uint = 60 - var executableBinary string = "cointop" - var hostKeyFile string = cssh.DefaultHostKeyFile - - serverCmd := &cobra.Command{ - Use: "server", - Short: "Run cintop SSH Server", - Long: `Run cointop SSH server`, - RunE: func(cmd *cobra.Command, args []string) error { - server := cssh.NewServer(&cssh.Config{ - Address: address, - Port: port, - IdleTimeout: time.Duration(int(idleTimeout)) * time.Second, - ExecutableBinary: executableBinary, - HostKeyFile: hostKeyFile, - }) - - fmt.Printf("Running SSH server on port %v\n", port) - return server.ListenAndServe() - }, - } - - serverCmd.Flags().UintVarP(&port, "port", "p", port, "Port") - serverCmd.Flags().StringVarP(&address, "address", "a", address, "Address") - serverCmd.Flags().UintVarP(&idleTimeout, "idle-timeout", "t", idleTimeout, "Idle timeout in seconds") - serverCmd.Flags().StringVarP(&executableBinary, "binary", "b", executableBinary, "Executable binary path") - serverCmd.Flags().StringVarP(&hostKeyFile, "host-key-file", "k", hostKeyFile, "Host key file") - - rootCmd.AddCommand(versionCmd, cleanCmd, resetCmd, priceCmd, testCmd, serverCmd) + rootCmd := RootCmd() + rootCmd.AddCommand( + VersionCmd(), + CleanCmd(), + ResetCmd(), + PriceCmd(), + HoldingsCmd(), + ServerCmd(), + TestCmd(), + ) if err := rootCmd.Execute(); err != nil { panic(err) } } - -func doTest() { - ct, err := cointop.NewCointop(&cointop.Config{ - NoPrompts: true, - }) - - if err != nil { - panic(err) - } - - ct.Exit() -} diff --git a/cointop/cmd/holdings.go b/cointop/cmd/holdings.go new file mode 100644 index 0000000..00cb6b7 --- /dev/null +++ b/cointop/cmd/holdings.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "fmt" + + "github.com/miguelmota/cointop/cointop" + "github.com/spf13/cobra" +) + +// HoldingsCmd ... +func HoldingsCmd() *cobra.Command { + var total, noCache bool + var config string + + holdingsCmd := &cobra.Command{ + Use: "holdings", + Short: "Displays current holdings", + Long: `The holdings command shows your current holdings`, + RunE: func(cmd *cobra.Command, args []string) error { + ct, err := cointop.NewCointop(&cointop.Config{ + ConfigFilepath: config, + APIChoice: cointop.CoinGecko, + CacheDir: cointop.DefaultCacheDir, + }) + if err != nil { + return err + } + + if total { + return ct.PrintTotalHoldings() + } + + return ct.PrintHoldingsTable() + }, + } + + holdingsCmd.Flags().BoolVarP(&total, "total", "t", false, "Show total only") + holdingsCmd.Flags().BoolVarP(&noCache, "no-cache", "", false, "No cache") + holdingsCmd.Flags().StringVarP(&config, "config", "c", "", fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath)) + + return holdingsCmd +} diff --git a/cointop/cmd/price.go b/cointop/cmd/price.go new file mode 100644 index 0000000..23d8a4c --- /dev/null +++ b/cointop/cmd/price.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/miguelmota/cointop/cointop" + "github.com/spf13/cobra" +) + +// PriceCmd ... +func PriceCmd() *cobra.Command { + var apiChoice, coin, currency string + + priceCmd := &cobra.Command{ + Use: "price", + Short: "Displays the current price of a coin", + Long: `The price command display the current price of a coin`, + RunE: func(cmd *cobra.Command, args []string) error { + return cointop.PrintPrice(&cointop.PriceConfig{ + Coin: coin, + Currency: currency, + APIChoice: apiChoice, + }) + }, + } + + priceCmd.Flags().StringVarP(&coin, "coin", "c", "bitcoin", "Full name of the coin (default \"bitcoin\")") + priceCmd.Flags().StringVarP(¤cy, "currency", "f", "USD", "The currency to convert to (default \"USD\")") + priceCmd.Flags().StringVarP(&apiChoice, "api", "a", cointop.CoinGecko, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"") + + return priceCmd +} diff --git a/cointop/cmd/reset.go b/cointop/cmd/reset.go new file mode 100644 index 0000000..b2efc57 --- /dev/null +++ b/cointop/cmd/reset.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "github.com/miguelmota/cointop/cointop" + "github.com/miguelmota/cointop/cointop/common/filecache" + "github.com/spf13/cobra" +) + +// ResetCmd ... +func ResetCmd() *cobra.Command { + cacheDir := filecache.DefaultCacheDir + + 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{ + Log: true, + CacheDir: cacheDir, + }) + }, + } + + resetCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory") + + return resetCmd +} diff --git a/cointop/cmd/root.go b/cointop/cmd/root.go new file mode 100644 index 0000000..6b16255 --- /dev/null +++ b/cointop/cmd/root.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "fmt" + + "github.com/miguelmota/cointop/cointop" + "github.com/spf13/cobra" +) + +// RootCmd ... +func RootCmd() *cobra.Command { + var version, test, clean, reset, hideMarketbar, hideChart, hideStatusbar, onlyTable, silent, noCache bool + var refreshRate uint + var config, cmcAPIKey, apiChoice, colorscheme string + perPage := cointop.DefaultPerPage + cacheDir := cointop.DefaultCacheDir + + rootCmd := &cobra.Command{ + Use: "cointop", + Short: "Cointop is an interactive terminal based app for tracking cryptocurrencies", + Long: ` + _ _ + ___ ___ (_)_ __ | |_ ___ _ __ + / __/ _ \| | '_ \| __/ _ \| '_ \ +| (_| (_) | | | | | || (_) | |_) | + \___\___/|_|_| |_|\__\___/| .__/ + |_| + +Cointop is a fast and lightweight interactive terminal based UI application for tracking and monitoring cryptocurrency coin stats in real-time. + +See git.io/cointop for more info.`, + RunE: func(cmd *cobra.Command, args []string) error { + if version { + cointop.PrintVersion() + return nil + } + + if test { + // TODO: deprecate test flag, only have test command + doTest() + return nil + } + + // NOTE: if reset flag enabled, reset and run cointop + if reset { + if err := cointop.Reset(&cointop.ResetConfig{ + Log: !silent, + }); 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 + } + } + + var refreshRateP *uint + if cmd.Flags().Changed("refresh-rate") { + refreshRateP = &refreshRate + } + + ct, err := cointop.NewCointop(&cointop.Config{ + CacheDir: cacheDir, + NoCache: noCache, + ConfigFilepath: config, + CoinMarketCapAPIKey: cmcAPIKey, + APIChoice: apiChoice, + Colorscheme: colorscheme, + HideMarketbar: hideMarketbar, + HideChart: hideChart, + HideStatusbar: hideStatusbar, + OnlyTable: onlyTable, + RefreshRate: refreshRateP, + PerPage: perPage, + }) + if err != nil { + return err + } + + return ct.Run() + }, + } + + 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().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().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", "", cointop.CoinGecko, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"") + rootCmd.Flags().StringVarP(&colorscheme, "colorscheme", "", "", "Colorscheme to use (default \"cointop\"). To install standard themes, do:\n\ngit clone git@github.com:cointop-sh/colors.git ~/.config/cointop/colors\n\nSee git.io/cointop#colorschemes for more info.") + rootCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory") + + return rootCmd +} diff --git a/cointop/cmd/server.go b/cointop/cmd/server.go new file mode 100644 index 0000000..6427e16 --- /dev/null +++ b/cointop/cmd/server.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "fmt" + "time" + + cssh "github.com/miguelmota/cointop/cointop/ssh" + "github.com/spf13/cobra" +) + +// ServerCmd ... +func ServerCmd() *cobra.Command { + var port uint = 22 + var address string = "0.0.0.0" + var idleTimeout uint = 60 + var executableBinary string = "cointop" + var hostKeyFile string = cssh.DefaultHostKeyFile + + serverCmd := &cobra.Command{ + Use: "server", + Short: "Run cintop SSH Server", + Long: `Run cointop SSH server`, + RunE: func(cmd *cobra.Command, args []string) error { + server := cssh.NewServer(&cssh.Config{ + Address: address, + Port: port, + IdleTimeout: time.Duration(int(idleTimeout)) * time.Second, + ExecutableBinary: executableBinary, + HostKeyFile: hostKeyFile, + }) + + fmt.Printf("Running SSH server on port %v\n", port) + return server.ListenAndServe() + }, + } + + serverCmd.Flags().UintVarP(&port, "port", "p", port, "Port") + serverCmd.Flags().StringVarP(&address, "address", "a", address, "Address") + serverCmd.Flags().UintVarP(&idleTimeout, "idle-timeout", "t", idleTimeout, "Idle timeout in seconds") + serverCmd.Flags().StringVarP(&executableBinary, "binary", "b", executableBinary, "Executable binary path") + serverCmd.Flags().StringVarP(&hostKeyFile, "host-key-file", "k", hostKeyFile, "Host key file") + + return serverCmd +} diff --git a/cointop/cmd/test.go b/cointop/cmd/test.go new file mode 100644 index 0000000..10eea7f --- /dev/null +++ b/cointop/cmd/test.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "github.com/miguelmota/cointop/cointop" + "github.com/spf13/cobra" +) + +// TestCmd ... +func TestCmd() *cobra.Command { + testCmd := &cobra.Command{ + Use: "test", + Short: "Runs tests", + Long: `The test command runs tests for Homebrew`, + Run: func(cmd *cobra.Command, args []string) { + doTest() + }, + } + + return testCmd +} + +// DoTest ... +func doTest() { + ct, err := cointop.NewCointop(&cointop.Config{ + NoPrompts: true, + }) + + if err != nil { + panic(err) + } + + ct.Exit() +} diff --git a/cointop/cmd/version.go b/cointop/cmd/version.go new file mode 100644 index 0000000..b57b08d --- /dev/null +++ b/cointop/cmd/version.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/miguelmota/cointop/cointop" + "github.com/spf13/cobra" +) + +// VersionCmd ... +func VersionCmd() *cobra.Command { + versionCmd := &cobra.Command{ + Use: "version", + Short: "Displays the current version", + Long: `The version command displays the current version`, + Run: func(cmd *cobra.Command, args []string) { + cointop.PrintVersion() + }, + } + + return versionCmd +} diff --git a/cointop/cointop.go b/cointop/cointop.go index c6b0d81..332e6fd 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -65,6 +65,7 @@ type State struct { portfolioVisible bool portfolioUpdateMenuVisible bool refreshRate time.Duration + running bool searchFieldVisible bool selectedCoin *Coin selectedChartRange string @@ -151,6 +152,9 @@ var DefaultColorscheme = "cointop" // DefaultConfigFilepath ... var DefaultConfigFilepath = "~/.config/cointop/config.toml" +// DefaultCacheDir ... +var DefaultCacheDir = filecache.DefaultCacheDir + // NewCointop initializes cointop func NewCointop(config *Config) (*Cointop, error) { var debug bool @@ -390,6 +394,8 @@ func (ct *Cointop) Run() error { if err := ct.Keybindings(g); err != nil { return fmt.Errorf("keybindings: %v", err) } + + ct.State.running = true if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { return fmt.Errorf("main loop: %v", err) } @@ -397,6 +403,11 @@ func (ct *Cointop) Run() error { return nil } +// IsRunning returns true if cointop is running +func (ct *Cointop) IsRunning() bool { + return ct.State.running +} + // PriceConfig is the config options for the price command type PriceConfig struct { Coin string @@ -421,7 +432,7 @@ func PrintPrice(config *PriceConfig) error { } symbol := CurrencySymbol(config.Currency) - fmt.Fprintf(os.Stdout, "%s%s", symbol, humanize.Commaf(price)) + fmt.Fprintf(os.Stdout, "%s%s\n", symbol, humanize.Commaf(price)) return nil } @@ -440,7 +451,7 @@ func Clean(config *CleanConfig) error { cacheCleaned := false - cacheDir := filecache.DefaultCacheDir + cacheDir := DefaultCacheDir if config.CacheDir != "" { cacheDir = config.CacheDir } diff --git a/cointop/common/asciitable/asciitable.go b/cointop/common/asciitable/asciitable.go new file mode 100644 index 0000000..146e178 --- /dev/null +++ b/cointop/common/asciitable/asciitable.go @@ -0,0 +1,62 @@ +package asciitable + +import ( + "strings" + + "github.com/olekukonko/tablewriter" +) + +// Input ... +type Input struct { + Data [][]string + Headers []string + Alignment []int +} + +// AsciiTable ... +type AsciiTable struct { + table *tablewriter.Table + tableString *strings.Builder +} + +// NewAsciiTable ... +func NewAsciiTable(input *Input) *AsciiTable { + tableString := &strings.Builder{} + alignment := make([]int, len(input.Alignment)) + for i, value := range input.Alignment { + switch value { + case -1: + alignment[i] = 3 + case 0: + alignment[i] = 1 + case 1: + alignment[i] = 2 + } + } + + table := tablewriter.NewWriter(tableString) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(false) + table.SetHeaderAlignment(tablewriter.ALIGN_RIGHT) + table.SetColumnAlignment(alignment) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") + table.SetNoWhiteSpace(true) + table.SetHeader(input.Headers) + table.AppendBulk(input.Data) + + return &AsciiTable{ + table: table, + tableString: tableString, + } +} + +// String ... +func (t *AsciiTable) String() string { + t.table.Render() + return t.tableString.String() +} diff --git a/cointop/navigation.go b/cointop/navigation.go index 3f1085a..6586921 100644 --- a/cointop/navigation.go +++ b/cointop/navigation.go @@ -358,6 +358,9 @@ func (ct *Cointop) LastPage() error { // IsFirstRow returns true if cursor is on first row func (ct *Cointop) IsFirstRow() bool { ct.debuglog("isFirstRow()") + if ct.Views.Table.Backing() == nil { + return false + } _, y := ct.Views.Table.Backing().Origin() _, cy := ct.Views.Table.Backing().Cursor() @@ -368,6 +371,9 @@ func (ct *Cointop) IsFirstRow() bool { // IsLastRow returns true if cursor is on last row func (ct *Cointop) IsLastRow() bool { ct.debuglog("isLastRow()") + if ct.Views.Table.Backing() == nil { + return false + } _, y := ct.Views.Table.Backing().Origin() _, cy := ct.Views.Table.Backing().Cursor() @@ -391,6 +397,9 @@ func (ct *Cointop) IsLastPage() bool { // IsPageFirstLine returns true if the cursor is on the visible first row func (ct *Cointop) IsPageFirstLine() bool { ct.debuglog("isPageFirstLine()") + if ct.Views.Table.Backing() == nil { + return false + } _, cy := ct.Views.Table.Backing().Cursor() return cy == 0 @@ -399,6 +408,9 @@ 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()") + if ct.Views.Table.Backing() == nil { + return false + } _, cy := ct.Views.Table.Backing().Cursor() _, sy := ct.Views.Table.Backing().Size() @@ -443,6 +455,10 @@ func (ct *Cointop) GoToGlobalIndex(idx int) error { // HighlightRow highlights the row at index func (ct *Cointop) HighlightRow(idx int) error { ct.debuglog("highlightRow()") + if ct.Views.Table.Backing() == nil { + return nil + } + ct.Views.Table.Backing().SetOrigin(0, 0) ct.Views.Table.Backing().SetCursor(0, 0) ox, _ := ct.Views.Table.Backing().Origin() diff --git a/cointop/portfolio.go b/cointop/portfolio.go index 0b3d9ad..235ba28 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -2,11 +2,15 @@ package cointop import ( "fmt" + "math" + "os" "regexp" "sort" "strconv" "strings" + "github.com/miguelmota/cointop/cointop/common/asciitable" + "github.com/miguelmota/cointop/cointop/common/humanize" "github.com/miguelmota/cointop/cointop/common/pad" ) @@ -303,3 +307,50 @@ func normalizeFloatString(input string) string { return "" } + +// PrintHoldingsTable prints the holdings in an ASCII table +func (ct *Cointop) PrintHoldingsTable() error { + ct.UpdateCoins() // fetches latest data + holdings := ct.GetPortfolioSlice() + total := ct.GetPortfolioTotal() + data := make([][]string, len(holdings)) + symbol := ct.CurrencySymbol() + + for _, entry := range holdings { + percentHoldings := (entry.Balance / total) * 1e2 + if math.IsNaN(percentHoldings) { + percentHoldings = 0 + } + + data = append(data, []string{ + entry.Name, + entry.Symbol, + humanize.Commaf(entry.Price), + humanize.Commaf(entry.Holdings), + humanize.Commaf(entry.Balance), + fmt.Sprintf("%.2f%%", entry.PercentChange24H), + fmt.Sprintf("%.2f%%", percentHoldings), + }) + } + + alignment := []int{-1, -1, 1, 1, 1, 1, 1} + headers := []string{"name", "symbol", fmt.Sprintf("%sprice", symbol), "holdings", fmt.Sprintf("%sbalance", symbol), "24h%", "%holdings"} + table := asciitable.NewAsciiTable(&asciitable.Input{ + Data: data, + Headers: headers, + Alignment: alignment, + }) + + fmt.Println(table.String()) + return nil +} + +// PrintTotalHoldings prints the total holdings amount +func (ct *Cointop) PrintTotalHoldings() error { + ct.UpdateCoins() // fetches latest data + total := ct.GetPortfolioTotal() + symbol := ct.CurrencySymbol() + + fmt.Fprintf(os.Stdout, "%s%s\n", symbol, humanize.Commaf(total)) + return nil +} diff --git a/cointop/size.go b/cointop/size.go index e6036dd..8b99877 100644 --- a/cointop/size.go +++ b/cointop/size.go @@ -3,6 +3,10 @@ package cointop // Size returns window width and height func (ct *Cointop) size() (int, int) { ct.debuglog("size()") + if ct.g == nil { + return 0, 0 + } + return ct.g.Size() } diff --git a/cointop/table.go b/cointop/table.go index 9eb7ee4..b7bc14a 100644 --- a/cointop/table.go +++ b/cointop/table.go @@ -288,6 +288,10 @@ func (ct *Cointop) GetTableCoinsSlice() []*Coin { // HighlightedRowIndex returns the index of the highlighted row func (ct *Cointop) HighlightedRowIndex() int { ct.debuglog("HighlightedRowIndex()") + if ct.Views.Table.Backing() == nil { + return 0 + } + _, y := ct.Views.Table.Backing().Origin() _, cy := ct.Views.Table.Backing().Cursor() idx := y + cy diff --git a/cointop/update.go b/cointop/update.go index fb95833..db7f7c4 100644 --- a/cointop/update.go +++ b/cointop/update.go @@ -11,7 +11,7 @@ func (ct *Cointop) Update(f func() error) { ct.debuglog(fmt.Sprintf("Update()")) if ct.g == nil { - panic("gocui is not initialized") + return } ct.g.Update(func(g *gocui.Gui) error { diff --git a/go.mod b/go.mod index 587f298..7047cd6 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( 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/patrickmn/go-cache v2.1.0+incompatible github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index d5a8128..56ecc59 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb 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/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/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=