diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ad0a5f0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +** +!go.* +!*.go +!assets +!cmd +!cointop +!pkg +!vendor diff --git a/Dockerfile b/Dockerfile index c066462..cdff1eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,15 @@ RUN mkdir /app WORKDIR /app ARG VERSION -ADD . /app/ +COPY . ./ RUN go build -ldflags=-s -ldflags=-w -ldflags=-X=github.com/miguelmota/cointop/cointop.version=$VERSION -o main . -RUN git clone https://github.com/cointop-sh/colors && rm -Rf colors/.git* +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 /root/.config/cointop/colors -ENTRYPOINT cointop +COPY --from=build /app/colors-master /root/.config/cointop/colors +ENTRYPOINT ["/bin/cointop"] CMD [] diff --git a/cointop/coins_table.go b/cointop/coins_table.go index cd663e7..5abda59 100644 --- a/cointop/coins_table.go +++ b/cointop/coins_table.go @@ -120,7 +120,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { Text: symbol, }) case "price": - text := humanize.Commaf(coin.Price) + text := humanize.Monetaryf(coin.Price, 2) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -132,7 +132,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { Text: text, }) case "24h_volume": - text := humanize.Commaf(coin.Volume24H) + text := humanize.Monetaryf(coin.Volume24H, 0) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -151,7 +151,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { if coin.PercentChange1H < 0 { color1h = ct.colorscheme.TableColumnChangeDown } - text := fmt.Sprintf("%.2f%%", coin.PercentChange1H) + text := fmt.Sprintf("%v%%", humanize.Numericf(coin.PercentChange1H, 2)) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -170,7 +170,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { if coin.PercentChange24H < 0 { color24h = ct.colorscheme.TableColumnChangeDown } - text := fmt.Sprintf("%.2f%%", coin.PercentChange24H) + text := fmt.Sprintf("%v%%", humanize.Numericf(coin.PercentChange24H, 2)) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -189,7 +189,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { if coin.PercentChange7D < 0 { color7d = ct.colorscheme.TableColumnChangeDown } - text := fmt.Sprintf("%.2f%%", coin.PercentChange7D) + text := fmt.Sprintf("%v%%", humanize.Numericf(coin.PercentChange7D, 2)) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -208,7 +208,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { if coin.PercentChange30D < 0 { color30d = ct.colorscheme.TableColumnChangeDown } - text := fmt.Sprintf("%.2f%%", coin.PercentChange30D) + text := fmt.Sprintf("%v%%", humanize.Numericf(coin.PercentChange30D, 2)) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -220,7 +220,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { Text: text, }) case "market_cap": - text := humanize.Commaf(coin.MarketCap) + text := humanize.Monetaryf(coin.MarketCap, 0) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -232,7 +232,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { Text: text, }) case "total_supply": - text := humanize.Commaf(coin.TotalSupply) + text := humanize.Numericf(coin.TotalSupply, 0) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, @@ -244,7 +244,7 @@ func (ct *Cointop) GetCoinsTable() *table.Table { Text: text, }) case "available_supply": - text := humanize.Commaf(coin.AvailableSupply) + text := humanize.Numericf(coin.AvailableSupply, 0) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, diff --git a/cointop/marketbar.go b/cointop/marketbar.go index 67917be..002b343 100644 --- a/cointop/marketbar.go +++ b/cointop/marketbar.go @@ -35,10 +35,10 @@ func (ct *Cointop) UpdateMarketbar() error { if ct.IsPortfolioVisible() { ct.State.marketBarHeight = 1 total := ct.GetPortfolioTotal() - totalstr := humanize.Commaf(total) + totalstr := humanize.Monetaryf(total, 2) if !(ct.State.currencyConversion == "BTC" || ct.State.currencyConversion == "ETH" || total < 1) { total = math.Round(total*1e2) / 1e2 - totalstr = humanize.Commaf2(total) + totalstr = humanize.Monetaryf(total, 2) } timeframe := ct.State.selectedChartRange @@ -149,9 +149,9 @@ func (ct *Cointop) UpdateMarketbar() error { content = fmt.Sprintf( "%sGlobal ▶ Market Cap: %s %s 24H Volume: %s %s BTC Dominance: %.2f%%", chartInfo, - fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Commaf0(market.TotalMarketCapUSD)), + fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.TotalMarketCapUSD, 0)), separator1, - fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Commaf0(market.Total24HVolumeUSD)), + fmt.Sprintf("%s%s", ct.CurrencySymbol(), humanize.Monetaryf(market.Total24HVolumeUSD, 0)), separator2, market.BitcoinPercentageOfMarketCap, ) diff --git a/cointop/portfolio.go b/cointop/portfolio.go index b398fae..b4e8495 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -125,7 +125,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table { Text: symbol, }) case "price": - text := humanize.Commaf(coin.Price) + text := humanize.Monetaryf(coin.Price, 2) symbolPadding := 1 ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)+symbolPadding) ct.SetTableColumnAlignLeft(header, false) @@ -150,7 +150,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table { Text: text, }) case "balance": - text := humanize.Commaf(coin.Balance) + text := humanize.Monetaryf(coin.Balance, 2) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) colorBalance := ct.colorscheme.TableColumnPrice @@ -715,31 +715,31 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error { item[i] = entry.Symbol case "price": if humanReadable { - item[i] = fmt.Sprintf("%s%s", symbol, humanize.Commaf(entry.Price)) + item[i] = fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Price, 2)) } else { item[i] = strconv.FormatFloat(entry.Price, 'f', -1, 64) } case "holdings": if humanReadable { - item[i] = humanize.Commaf(entry.Holdings) + item[i] = humanize.Monetaryf(entry.Holdings, 2) } else { item[i] = strconv.FormatFloat(entry.Holdings, 'f', -1, 64) } case "balance": if humanReadable { - item[i] = fmt.Sprintf("%s%s", symbol, humanize.Commaf(entry.Balance)) + item[i] = fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(entry.Balance, 2)) } else { item[i] = strconv.FormatFloat(entry.Balance, 'f', -1, 64) } case "24h%": if humanReadable { - item[i] = fmt.Sprintf("%.2f%%", entry.PercentChange24H) + item[i] = fmt.Sprintf("%s%%", humanize.Numericf(entry.PercentChange24H, 2)) } else { item[i] = fmt.Sprintf("%.2f", entry.PercentChange24H) } case "%holdings": if humanReadable { - item[i] = fmt.Sprintf("%.2f%%", percentHoldings) + item[i] = fmt.Sprintf("%s%%", humanize.Numericf(percentHoldings, 2)) } else { item[i] = fmt.Sprintf("%.2f", percentHoldings) } @@ -852,7 +852,7 @@ func (ct *Cointop) PrintTotalHoldings(options *TablePrintOptions) error { value := strconv.FormatFloat(total, 'f', -1, 64) if humanReadable { - value = fmt.Sprintf("%s%s", symbol, humanize.Commaf(total)) + value = fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(total, 2)) } if format == "csv" { diff --git a/cointop/price.go b/cointop/price.go index 2d43304..c75e2dc 100644 --- a/cointop/price.go +++ b/cointop/price.go @@ -69,7 +69,7 @@ func GetCoinPrices(config *PricesConfig) ([]string, error) { } symbol := CurrencySymbol(config.Currency) - value := fmt.Sprintf("%s%s", symbol, humanize.Commaf(price)) + value := fmt.Sprintf("%s%s", symbol, humanize.Monetaryf(price, 2)) prices = append(prices, value) } diff --git a/cointop/price_alerts.go b/cointop/price_alerts.go index 17b7b5c..adb55a0 100644 --- a/cointop/price_alerts.go +++ b/cointop/price_alerts.go @@ -96,7 +96,7 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table { }) case "target_price": - targetPrice := fmt.Sprintf("%s %s", entry.Operator, humanize.Commaf(entry.TargetPrice)) + targetPrice := fmt.Sprintf("%s %s", entry.Operator, humanize.Monetaryf(entry.TargetPrice, 2)) ct.SetTableColumnWidthFromString(header, targetPrice) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ @@ -107,7 +107,7 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table { Text: targetPrice, }) case "price": - text := humanize.Commaf(coin.Price) + text := humanize.Monetaryf(coin.Price, 2) ct.SetTableColumnWidthFromString(header, text) ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ @@ -187,7 +187,7 @@ func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error { } var msg string title := "Cointop Alert" - priceStr := fmt.Sprintf("%s%s (%s%s)", ct.CurrencySymbol(), humanize.Commaf(alert.TargetPrice), ct.CurrencySymbol(), humanize.Commaf(coin.Price)) + priceStr := fmt.Sprintf("%s%s (%s%s)", ct.CurrencySymbol(), humanize.Numericf(alert.TargetPrice, 2), 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) @@ -251,7 +251,8 @@ func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool, coin *Coin) error { if ok { coin.Name = entry.CoinName currentPrice = strconv.FormatFloat(coin.Price, 'f', -1, 64) - value = fmt.Sprintf("%s %v", entry.Operator, entry.TargetPrice) + targetPrice := strconv.FormatFloat(entry.TargetPrice, 'f', -1, 64) + value = fmt.Sprintf("%s %v", entry.Operator, targetPrice) ct.State.priceAlertEditID = entry.ID isEdit = true } diff --git a/pkg/humanize/humanize.go b/pkg/humanize/humanize.go index 0eb8ee0..61a0660 100644 --- a/pkg/humanize/humanize.go +++ b/pkg/humanize/humanize.go @@ -1,7 +1,8 @@ package humanize import ( - "bytes" + "fmt" + "os" "strconv" "strings" @@ -9,47 +10,44 @@ import ( "golang.org/x/text/message" ) -// Commaf produces a string form of the given number in base 10 with -// commas after every three orders of magnitude. +// 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". // -// e.g. Commaf(834142.32) -> 834,142.32 -func Commaf(v float64) string { - buf := &bytes.Buffer{} - if v < 0 { - buf.Write([]byte{'-'}) - v = 0 - v - } +// e.g. Numericf(834142.32, 2) -> "834,142.32" +func Numericf(value float64, precision int) string { + return f(value, precision, "LC_NUMERIC", true) +} - comma := []byte{','} +// 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". +// +// e.g. Monetaryf(834142.3256, 2) -> "834,142.3256" +func Monetaryf(value float64, precision int) string { + return f(value, precision, "LC_MONETARY", false) +} - parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".") - pos := 0 - if len(parts[0])%3 != 0 { - pos += len(parts[0]) % 3 - buf.WriteString(parts[0][:pos]) - buf.Write(comma) +// f formats given value v, with d 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 +// 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), ".") + if !fixed && len(parts) > 1 { + for ; precision < len(parts[1]); precision += 1 { + if parts[1][precision] == '0' { + break + } + } } - for ; pos < len(parts[0]); pos += 3 { - buf.WriteString(parts[0][pos : pos+3]) - buf.Write(comma) - } - buf.Truncate(buf.Len() - 1) - if len(parts) > 1 { - buf.Write([]byte{'.'}) - buf.WriteString(parts[1]) + envlang, ok := os.LookupEnv(envvar) + if !ok { + envlang = "en" } - return buf.String() -} - -// Commaf2 ... -func Commaf2(v float64) string { - p := message.NewPrinter(language.English) - return p.Sprintf("%.2f", v) -} + lang := language.Make(envlang) -// Commaf0 ... -func Commaf0(v float64) string { - p := message.NewPrinter(language.English) - return p.Sprintf("%.0f", v) + format := fmt.Sprintf("%%.%df", precision) + return message.NewPrinter(lang).Sprintf(format, value) } diff --git a/pkg/pathutil/pathutil.go b/pkg/pathutil/pathutil.go index 86b0832..7606705 100644 --- a/pkg/pathutil/pathutil.go +++ b/pkg/pathutil/pathutil.go @@ -22,6 +22,22 @@ func UserPreferredConfigDir() string { return config } +// UserPreferredCacheDir returns the preferred cache directory for the user +func UserPreferredCacheDir() string { + defaultCacheDir := "/tmp" + + cache, err := os.UserCacheDir() + if err != nil { + return defaultCacheDir + } + + if cache == "" { + return defaultCacheDir + } + + return cache +} + // UserPreferredHomeDir returns the preferred home directory for the user func UserPreferredHomeDir() string { home, err := os.UserHomeDir() @@ -36,6 +52,7 @@ func UserPreferredHomeDir() string { func NormalizePath(path string) string { userHome := UserPreferredHomeDir() userConfigHome := UserPreferredConfigDir() + userCacheHome := UserPreferredCacheDir() // expand tilde if strings.HasPrefix(path, "~/") { @@ -44,6 +61,7 @@ func NormalizePath(path string) string { path = strings.Replace(path, ":HOME:", userHome, -1) path = strings.Replace(path, ":PREFERRED_CONFIG_HOME:", userConfigHome, -1) + path = strings.Replace(path, ":PREFERRED_CACHE_HOME:", userCacheHome, -1) path = strings.Replace(path, "/", string(filepath.Separator), -1) return filepath.Clean(path)