diff --git a/cmd/root.go b/cmd/root.go index 2d86deb..d5494fc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -68,6 +68,7 @@ func init() { //nolint: gochecknoinits rootCmd.Flags().BoolVar(&options.ShowPositions, "show-positions", false, "display average unit cost, quantity, portfolio weight") rootCmd.Flags().BoolVar(&options.ShowHoldings, "show-holdings", false, "display average unit cost, quantity, portfolio weight (deprecated: use --show-positions)") rootCmd.Flags().StringVar(&options.Sort, "sort", "", "sort quotes on the UI. Set \"alpha\" to sort by ticker name. Set \"value\" to sort by position value. Keep empty to sort according to change percent") + rootCmd.Flags().BoolVar(&options.ShowAlternateRowBackground, "show-row-background", false, "display alternating row background color") printCmd.PersistentFlags().StringVar(&optionsPrint.Format, "format", "", "output format for printing holdings. Set \"csv\" to print as a CSV or \"json\" for JSON. Defaults to JSON.") printCmd.PersistentFlags().StringVar(&configPath, "config", "", "config file (default is $HOME/.ticker.yaml)") diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 153ece1..7aa60eb 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -22,15 +22,16 @@ import ( // Options to configured ticker behavior type Options struct { - RefreshInterval int - Watchlist string - Separate bool - ExtraInfoExchange bool - ExtraInfoFundamentals bool - ShowSummary bool - ShowHoldings bool // Deprecated: use ShowPositions instead, kept for backwards compatibility - ShowPositions bool // Preferred field name - Sort string + RefreshInterval int + Watchlist string + Separate bool + ExtraInfoExchange bool + ExtraInfoFundamentals bool + ShowSummary bool + ShowHoldings bool // Deprecated: use ShowPositions instead, kept for backwards compatibility + ShowPositions bool // Preferred field name + ShowAlternateRowBackground bool + Sort string } type symbolSource struct { @@ -227,6 +228,7 @@ func GetConfig(dep c.Dependencies, configPath string, options Options) (c.Config config.ShowPositions = showHoldingsFromCLI || showHoldingsFromConfig } config.Sort = getStringOption(options.Sort, config.Sort) + config.ShowAlternateRowBackground = getBoolOption(options.ShowAlternateRowBackground, config.ShowAlternateRowBackground) return config, nil } diff --git a/internal/common/common.go b/internal/common/common.go index 9da5641..9be40b3 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -25,6 +25,7 @@ type Config struct { ShowSummary bool `yaml:"show-summary"` ShowHoldings bool `yaml:"show-holdings"` // Deprecated: use ShowPositions instead, kept for backwards compatibility ShowPositions bool `yaml:"show-positions"` // Preferred field name + ShowAlternateRowBackground bool `yaml:"show-alternate-row-background"` Sort string `yaml:"sort"` Currency string `yaml:"currency"` CurrencyConvertSummaryOnly bool `yaml:"currency-summary-only"` @@ -42,6 +43,7 @@ type ConfigColorScheme struct { TextLine string `yaml:"text-line"` TextTag string `yaml:"text-tag"` BackgroundTag string `yaml:"background-tag"` + BackgroundRow string `yaml:"background-row"` } type ConfigAssetGroup struct { diff --git a/internal/ui/component/watchlist/row/row.go b/internal/ui/component/watchlist/row/row.go index 90293ff..9da97e6 100644 --- a/internal/ui/component/watchlist/row/row.go +++ b/internal/ui/component/watchlist/row/row.go @@ -50,6 +50,7 @@ type Config struct { ExtraInfoFundamentals bool Styles c.Styles Asset *c.Asset + RowBackground string } type UpdateAssetMsg *c.Asset @@ -221,18 +222,27 @@ func (m *Model) View() string { }) } + contentResult := grid.Render(grid.Grid{Rows: rows, GutterHorizontal: WidthGutter}) + + if m.config.RowBackground != "" { + contentResult = u.ApplyBackground(contentResult, m.config.RowBackground) + } + if m.config.Separate { - rows = append( - rows, - grid.Row{ - Width: m.width, - Cells: []grid.Cell{ - {Text: textSeparator(m.width, m.config.Styles)}, + separatorResult := grid.Render(grid.Grid{ + Rows: []grid.Row{ + { + Width: m.width, + Cells: []grid.Cell{ + {Text: textSeparator(m.width, m.config.Styles)}, + }, }, - }) + }, + }) + contentResult = contentResult + "\n" + separatorResult } - return grid.Render(grid.Grid{Rows: rows, GutterHorizontal: WidthGutter}) + return contentResult } func (m *Model) buildCells() []grid.Cell { diff --git a/internal/ui/component/watchlist/watchlist.go b/internal/ui/component/watchlist/watchlist.go index 92725eb..dcaecc8 100644 --- a/internal/ui/component/watchlist/watchlist.go +++ b/internal/ui/component/watchlist/watchlist.go @@ -14,12 +14,13 @@ import ( // Config represents the configuration for the watchlist component type Config struct { - Separate bool - ShowPositions bool - ExtraInfoExchange bool - ExtraInfoFundamentals bool - Sort string - Styles c.Styles + Separate bool + ShowPositions bool + ExtraInfoExchange bool + ExtraInfoFundamentals bool + Sort string + Styles c.Styles + RowAlternateBackgroundColor string } // Model for watchlist section @@ -85,6 +86,10 @@ func (m *Model) Update(msg tea.Msg) (*Model, tea.Cmd) { cmds = append(cmds, cmd) m.rowsBySymbol[assets[i].Symbol] = m.rows[i] } else { + rowBackground := "" + if i%2 == 1 { + rowBackground = m.config.RowAlternateBackgroundColor + } m.rows = append(m.rows, row.New(row.Config{ Separate: m.config.Separate, ExtraInfoExchange: m.config.ExtraInfoExchange, @@ -92,6 +97,7 @@ func (m *Model) Update(msg tea.Msg) (*Model, tea.Cmd) { ShowPositions: m.config.ShowPositions, Styles: m.config.Styles, Asset: asset, + RowBackground: rowBackground, })) m.rowsBySymbol[assets[i].Symbol] = m.rows[len(m.rows)-1] } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index d325ad8..447355b 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -84,12 +84,13 @@ func NewModel(dep c.Dependencies, ctx c.Context, monitors *mon.Monitor) *Model { assetQuotesLookup: make(map[string]int), positionSummary: asset.PositionSummary{}, watchlist: watchlist.NewModel(watchlist.Config{ - Sort: ctx.Config.Sort, - Separate: ctx.Config.Separate, - ShowPositions: ctx.Config.ShowPositions, - ExtraInfoExchange: ctx.Config.ExtraInfoExchange, - ExtraInfoFundamentals: ctx.Config.ExtraInfoFundamentals, - Styles: ctx.Reference.Styles, + Sort: ctx.Config.Sort, + Separate: ctx.Config.Separate, + ShowPositions: ctx.Config.ShowPositions, + ExtraInfoExchange: ctx.Config.ExtraInfoExchange, + ExtraInfoFundamentals: ctx.Config.ExtraInfoFundamentals, + Styles: ctx.Reference.Styles, + RowAlternateBackgroundColor: util.GetRowAlternateBackgroundColor(ctx.Config), }), summary: summary.NewModel(ctx), groupMaxIndex: groupMaxIndex, diff --git a/internal/ui/util/style.go b/internal/ui/util/style.go index 43e5f61..bde13f7 100644 --- a/internal/ui/util/style.go +++ b/internal/ui/util/style.go @@ -3,6 +3,7 @@ package util import ( "math" "regexp" + "strings" c "github.com/achannarasappa/ticker/v5/internal/common" "github.com/lucasb-eyer/go-colorful" @@ -144,3 +145,45 @@ func getColorOrDefault(colorConfig string, colorDefault string) string { return colorDefault } + +// GetRowAlternateBackground returns the resolved background color hex for alternate rows +func GetRowAlternateBackground(colorScheme c.ConfigColorScheme) string { + return getColorOrDefault(colorScheme.BackgroundRow, "#303030") +} + +// GetRowAlternateBackgroundColor returns the alternate row background color when the feature +// is enabled in the config, or an empty string when the feature is disabled. +func GetRowAlternateBackgroundColor(config c.Config) string { + if !config.ShowAlternateRowBackground { + return "" + } + + return GetRowAlternateBackground(config.ColorScheme) +} + +// ApplyBackground applies a background color to a rendered terminal string, +// including re-applying after any terminal reset sequences so that fill spaces +// between styled chunks also receive the background color. +func ApplyBackground(text string, bgHex string) string { + if bgHex == "" { + return text + } + + bgColor := p.Color(bgHex) + + // te.Style{}.Background(color).Styled("") returns "\x1b[48;...m\x1b[0m" or "" for ASCII profile + openCode := te.Style{}.Background(bgColor).Styled("") + openBgCode := strings.TrimSuffix(openCode, "\x1b[0m") + + if openBgCode == "" { + // ASCII profile or unknown color - no background available + return text + } + + const resetCode = "\x1b[0m" + + // Prepend background, and re-apply it after every reset in the text + result := openBgCode + strings.ReplaceAll(text, resetCode, resetCode+openBgCode) + + return result + resetCode +}