From 3f0f4d6756fb104d4c6b619831512e8dcea59018 Mon Sep 17 00:00:00 2001 From: Guy Edwards Date: Tue, 4 Jun 2024 10:31:07 +0100 Subject: [PATCH] feat: add basic themeing colors and md (#80) adds color options for the app title, selected item and markdown viewer. Currently basic but could be fleshed out more in future if desired closes #76 --- README.md | 10 +++++++ internal/commands/commands.go | 34 ++++++++++++++++++++--- internal/commands/tui.go | 31 +++++++++++++++------ internal/config/config.go | 52 +++++++++++++++++++++++++++-------- 4 files changed, 103 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1df7d3f..16df563 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,16 @@ Automatically mark items as read on selection or navigation through items. ```yaml autoread: true ``` +### Theme +Theme allows some basic color overrides in the feed view and then setting a custom markdown render theme for the overall markdown view. `theme.glamour` can be one of "dark", "dracula", "light", "pink", "ascii" or "notty". See [here](https://github.com/charmbracelet/glamour/tree/master/styles/gallery) for previews and more info. +Colors can be hex or ASCII codes, they will be coerced depending on your terminal color settings. +```yaml +theme: + glamour: dark + titleColor: "62" + selectedItemColor: "170" + filterColor: "#555555" +``` ### Backends As well as adding feeds directly, you can pull in feeds from another source. You can add multiple backends and the feeds will all be added. diff --git a/internal/commands/commands.go b/internal/commands/commands.go index c44cde9..435fdc3 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -14,6 +14,7 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" + "github.com/charmbracelet/glamour/ansi" "github.com/guyfedwards/nom/v2/internal/config" "github.com/guyfedwards/nom/v2/internal/rss" @@ -188,7 +189,7 @@ func (c Commands) TUI() error { es = append(es, fmt.Sprintf("Error fetching %s: %s", e.FeedURL, e.Err)) } - if err := Render(items, c, es); err != nil { + if err := Render(items, c, es, c.config); err != nil { return fmt.Errorf("commands.TUI: %w", err) } @@ -401,7 +402,7 @@ func (c Commands) GetGlamourisedArticle(ID int) (string, error) { } } - content, err := glamouriseItem(article) + content, err := glamouriseItem(article, c.config.Theme) if err != nil { return "", fmt.Errorf("[commands.go] GetGlamourisedArticle: %w", err) } @@ -409,7 +410,28 @@ func (c Commands) GetGlamourisedArticle(ID int) (string, error) { return content, nil } -func glamouriseItem(item store.Item) (string, error) { +func getStyleConfigWithOverrides(theme config.Theme) (sc ansi.StyleConfig) { + switch theme.Glamour { + case "light": + sc = glamour.LightStyleConfig + case "dracula": + sc = glamour.DraculaStyleConfig + case "pink": + sc = glamour.PinkStyleConfig + case "ascii": + sc = glamour.ASCIIStyleConfig + case "notty": + sc = glamour.NoTTYStyleConfig + default: + sc = glamour.DarkStyleConfig + } + + sc.H1.BackgroundColor = &theme.TitleColor + + return sc +} + +func glamouriseItem(item store.Item, theme config.Theme) (string, error) { var mdown string mdown += "# " + item.Title @@ -424,7 +446,11 @@ func glamouriseItem(item store.Item) (string, error) { mdown += "\n\n" mdown += htmlToMd(item.Content) - out, err := glamour.Render(mdown, "dark") + r, _ := glamour.NewTermRenderer( + glamour.WithStyles(getStyleConfigWithOverrides(theme)), + ) + + out, err := r.Render(mdown) if err != nil { return "", fmt.Errorf("GlamouriseItem: %w", err) } diff --git a/internal/commands/tui.go b/internal/commands/tui.go index 6fd2c3e..da50c2c 100644 --- a/internal/commands/tui.go +++ b/internal/commands/tui.go @@ -17,6 +17,7 @@ import ( "github.com/sahilm/fuzzy" "golang.org/x/term" + "github.com/guyfedwards/nom/v2/internal/config" "github.com/guyfedwards/nom/v2/internal/store" ) @@ -24,9 +25,9 @@ var ( appStyle = lipgloss.NewStyle().Padding(1, 0, 0, 0).Margin(0) titleStyle = list.DefaultStyles().Title.Margin(0).Width(5) itemStyle = lipgloss.NewStyle().PaddingLeft(4) - selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2) readStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("240")) - selectedReadStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + selectedReadStyle = lipgloss.NewStyle().PaddingLeft(2) favouriteStyle = itemStyle.Copy().PaddingLeft(2).Bold(true) selectedFavouriteStyle = selectedItemStyle.Copy().Bold(true) paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) @@ -48,7 +49,9 @@ type TUIItem struct { func (i TUIItem) FilterValue() string { return fmt.Sprintf("%s||%s", i.Title, i.FeedName) } -type itemDelegate struct{} +type itemDelegate struct { + theme config.Theme +} func (d itemDelegate) Height() int { return 1 } func (d itemDelegate) Spacing() int { return 0 } @@ -84,9 +87,9 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list return selectedReadStyle.Render("> " + strings.Join(s, " ")) } if i.Favourite { - return selectedFavouriteStyle.Render("* " + strings.Join(s, " ")) + return selectedFavouriteStyle.Foreground(lipgloss.Color(d.theme.SelectedItemColor)).Render("* " + strings.Join(s, " ")) } - return selectedItemStyle.Render("> " + strings.Join(s, " ")) + return selectedItemStyle.Foreground(lipgloss.Color(d.theme.SelectedItemColor)).Render("> " + strings.Join(s, " ")) } } @@ -100,6 +103,7 @@ type model struct { selectedArticle *int viewport viewport.Model errors []string + cfg config.Config } func (m model) Init() tea.Cmd { @@ -552,7 +556,7 @@ func CustomFilter(term string, targets []string) []list.Rank { const defaultTitle = "nom" -func Render(items []list.Item, cmds Commands, errors []string) error { +func Render(items []list.Item, cmds Commands, errors []string, cfg config.Config) error { const defaultWidth = 20 _, ts, _ := term.GetSize(int(os.Stdout.Fd())) _, y := appStyle.GetFrameSize() @@ -560,19 +564,28 @@ func Render(items []list.Item, cmds Commands, errors []string) error { appStyle.Height(height) - l := list.New(items, itemDelegate{}, defaultWidth, height) + l := list.New(items, itemDelegate{theme: cfg.Theme}, defaultWidth, height) l.SetShowStatusBar(false) l.Title = defaultTitle - l.Styles.Title = titleStyle + l.Styles.Title = titleStyle.Background(lipgloss.Color(cfg.Theme.TitleColor)) l.Styles.PaginationStyle = paginationStyle l.Styles.HelpStyle = helpStyle + + l.FilterInput.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.Theme.FilterColor)) l.Filter = CustomFilter ListKeyMap.SetOverrides(&l) vp := viewport.New(78, height) - m := model{list: l, commands: cmds, viewport: vp, errors: errors, help: help.New()} + m := model{ + cfg: cfg, + commands: cmds, + errors: errors, + help: help.New(), + list: l, + viewport: vp, + } if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { return fmt.Errorf("tui.Render: %w", err) diff --git a/internal/config/config.go b/internal/config/config.go index 12804a7..f225b66 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -36,20 +36,28 @@ type Opener struct { Cmd string `yaml:"cmd"` } +type Theme struct { + Glamour string `yaml:"glamour,omitempty"` + TitleColor string `yaml:"titleColor,omitempty"` + FilterColor string `yaml:"filterColor,omitempty"` + SelectedItemColor string `yaml:"selectedItemColor,omitempty"` +} + // need to add to Load() below if loading from config file type Config struct { - configPath string - ConfigDir string `yaml:"-"` - Pager string `yaml:"pager,omitempty"` - Feeds []Feed `yaml:"feeds"` - // Preview feeds are distinguished from Feeds because we don't want to inadvertenly write those into the config file. - PreviewFeeds []Feed `yaml:"previewfeeds,omitempty"` - Backends *Backends `yaml:"backends,omitempty"` - ShowRead bool `yaml:"showread,omitempty"` - AutoRead bool `yaml:"autoread,omitempty"` + configPath string ShowFavourites bool - Openers []Opener `yaml:"openers,omitempty"` Version string + ConfigDir string `yaml:"-"` + Pager string `yaml:"pager,omitempty"` + Feeds []Feed `yaml:"feeds"` + // Preview feeds are distinguished from Feeds because we don't want to inadvertenly write those into the config file. + PreviewFeeds []Feed `yaml:"previewfeeds,omitempty"` + Backends *Backends `yaml:"backends,omitempty"` + ShowRead bool `yaml:"showread,omitempty"` + AutoRead bool `yaml:"autoread,omitempty"` + Openers []Opener `yaml:"openers,omitempty"` + Theme Theme `yaml:"theme,omitempty"` } func (c *Config) ToggleShowRead() { @@ -89,6 +97,12 @@ func New(configPath string, pager string, previewFeeds []string, version string) Pager: pager, Feeds: []Feed{}, PreviewFeeds: f, + Theme: Theme{ + Glamour: "dark", + SelectedItemColor: "170", + TitleColor: "62", + FilterColor: "62", + }, }, nil } @@ -116,9 +130,25 @@ func (c *Config) Load() error { c.ShowRead = fileConfig.ShowRead c.AutoRead = fileConfig.AutoRead - c.Feeds = fileConfig.Feeds c.Openers = fileConfig.Openers + + if fileConfig.Theme.Glamour != "" { + c.Theme.Glamour = fileConfig.Theme.Glamour + } + + if fileConfig.Theme.SelectedItemColor != "" { + c.Theme.SelectedItemColor = fileConfig.Theme.SelectedItemColor + } + + if fileConfig.Theme.TitleColor != "" { + c.Theme.TitleColor = fileConfig.Theme.TitleColor + } + + if fileConfig.Theme.FilterColor != "" { + c.Theme.FilterColor = fileConfig.Theme.FilterColor + } + // only set pager if it's not defined already, config file is lower // precidence than flags/env that can be passed to New if c.Pager == "" {