From 7eff39064683edda9d14c32072963092cb27b908 Mon Sep 17 00:00:00 2001 From: Ayyan Date: Thu, 19 Jun 2025 22:27:21 -0400 Subject: [PATCH] feat(config): granular control over theme config (#564) --- cmd/guilds_tree.go | 14 ++++--- cmd/message_input.go | 4 +- cmd/messages_text.go | 4 +- cmd/state.go | 6 ++- internal/config/config.toml | 29 +++++++-------- internal/config/theme.go | 73 +++++++++++++++++++++++-------------- internal/ui/util.go | 30 ++++++++------- 7 files changed, 94 insertions(+), 66 deletions(-) diff --git a/cmd/guilds_tree.go b/cmd/guilds_tree.go index 7bfe501..9507ddc 100644 --- a/cmd/guilds_tree.go +++ b/cmd/guilds_tree.go @@ -28,7 +28,7 @@ func newGuildsTree(cfg *config.Config) *guildsTree { cfg: cfg, } - gt.Box = ui.NewConfiguredBox(gt.Box, &cfg.Theme) + gt.Box = ui.ConfigureBox(gt.Box, &cfg.Theme) gt. SetRoot(tview.NewTreeNode("")). @@ -63,9 +63,11 @@ func (gt *guildsTree) createFolderNode(folder gateway.GuildFolder) { } func (gt *guildsTree) createGuildNode(n *tview.TreeNode, g discord.Guild) { - guildNode := tview.NewTreeNode(g.Name) - guildNode.SetReference(g.ID) - guildNode.SetColor(tcell.GetColor(gt.cfg.Theme.GuildsTree.GuildColor)) + style := gt.cfg.Theme.GuildsTree.GuildStyle.Style + guildNode := tview.NewTreeNode(g.Name). + SetReference(g.ID). + SetTextStyle(style). + SetSelectedTextStyle(style.Reverse(true)) n.AddChild(guildNode) } @@ -110,9 +112,11 @@ func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Ch } } + style := gt.cfg.Theme.GuildsTree.ChannelStyle.Style channelNode := tview.NewTreeNode(gt.channelToString(channel)). SetReference(channel.ID). - SetColor(tcell.GetColor(gt.cfg.Theme.GuildsTree.ChannelColor)) + SetTextStyle(style). + SetSelectedTextStyle(style.Reverse(true)) node.AddChild(channelNode) } diff --git a/cmd/message_input.go b/cmd/message_input.go index 69aec64..aea96ce 100644 --- a/cmd/message_input.go +++ b/cmd/message_input.go @@ -48,7 +48,7 @@ func newMessageInput(cfg *config.Config) *messageInput { autocomplete: tview.NewList(), } - mi.Box = ui.NewConfiguredBox(mi.Box, &cfg.Theme) + mi.Box = ui.ConfigureBox(mi.Box, &cfg.Theme) mi. SetTextStyle(tcell.StyleDefault.Background(tcell.GetColor(cfg.Theme.BackgroundColor))). SetClipboard(func(s string) { @@ -59,7 +59,7 @@ func newMessageInput(cfg *config.Config) *messageInput { }). SetInputCapture(mi.onInputCapture) - mi.autocomplete.Box = ui.NewConfiguredBox(mi.autocomplete.Box, &mi.cfg.Theme) + mi.autocomplete.Box = ui.ConfigureBox(mi.autocomplete.Box, &mi.cfg.Theme) mi.autocomplete. ShowSecondaryText(false). SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack)). diff --git a/cmd/messages_text.go b/cmd/messages_text.go index 228bddf..9923f95 100644 --- a/cmd/messages_text.go +++ b/cmd/messages_text.go @@ -46,7 +46,7 @@ func newMessagesText(cfg *config.Config) *messagesText { cfg: cfg, } - mt.Box = ui.NewConfiguredBox(mt.Box, &cfg.Theme) + mt.Box = ui.ConfigureBox(mt.Box, &cfg.Theme) mt. SetDynamicColors(true). SetRegions(true). @@ -455,7 +455,7 @@ func (mt *messagesText) showUrlSelector(urls []string, attachments []discord.Att SetHighlightFullLine(true). ShowSecondaryText(false). SetDoneFunc(done) - list.Box = ui.NewConfiguredBox(list.Box, &mt.cfg.Theme) + list.Box = ui.ConfigureBox(list.Box, &mt.cfg.Theme) list. SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { diff --git a/cmd/state.go b/cmd/state.go index c44ad79..b8fe40d 100644 --- a/cmd/state.go +++ b/cmd/state.go @@ -12,7 +12,6 @@ import ( "github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver" "github.com/diamondburned/arikawa/v3/utils/ws" "github.com/diamondburned/ningen/v3" - "github.com/gdamore/tcell/v2" ) func openState(token string) error { @@ -76,7 +75,10 @@ func onReady(r *gateway.ReadyEvent) { root := app.guildsTree.GetRoot() root.ClearChildren() - dmNode := tview.NewTreeNode("Direct Messages").SetColor(tcell.GetColor(app.cfg.Theme.GuildsTree.PrivateChannelColor)) + style := app.cfg.Theme.GuildsTree.PrivateChannelStyle.Style + dmNode := tview.NewTreeNode("Direct Messages"). + SetTextStyle(style). + SetSelectedTextStyle(style.Reverse(true)) root.AddChild(dmNode) for _, folder := range r.UserSettings.GuildFolders { diff --git a/internal/config/config.toml b/internal/config/config.toml index b7993a7..f786c08 100644 --- a/internal/config/config.toml +++ b/internal/config/config.toml @@ -106,29 +106,28 @@ down = "Down" background_color = "default" [theme.title] -# The values can be one of the following: `"left"`, `"center"`, and `"right"`. +# `"left"`, `"center"`, or `"right"`. alignment = "left" -# style supports foreground and background. -style = { foreground = "default" } -active_style = { foreground = "green" } +normal_style = { foreground = "default", background = "default" } +active_style = { foreground = "green", background = "default" } [theme.border] enabled = true # [top, bottom, left, right] padding = [0, 0, 1, 1] -# The values can be one of the following: `"plain"`, `"round"`, `"thick"`, and `"double"`. +# `"plain"`, `"round"`, `"thick"`, or `"double"`. set = "plain" -style = { foreground = "default" } -active_style = { foreground = "green" } +normal_style = { foreground = "default", background = "default" } +active_style = { foreground = "green", background = "default" } [theme.guilds_tree] auto_expand_folders = true # Give tree-like shape graphics = true graphics_color = "default" -private_channel_color = "white" -guild_color = "white" -channel_color = "white" +private_channel_style = { foreground = "white", background = "default" } +guild_style = { foreground = "white", background = "default" } +channel_style = { foreground = "white", background = "default" } [theme.messages_text] # Set to false to show messages with usernames instead of nicknames @@ -138,11 +137,11 @@ show_user_colors = true reply_indicator = ">" forwarded_indicator = "<" -author_style = { foreground = "aqua" } -mention_style = { foreground = "blue" } -emoji_style = { foreground = "green" } -url_style = { foreground = "blue" } -attachment_style = { foreground = "yellow" } +author_style = { foreground = "aqua", background = "default" } +mention_style = { foreground = "blue", background = "default" } +emoji_style = { foreground = "green", background = "default" } +url_style = { foreground = "blue", background = "default" } +attachment_style = { foreground = "yellow", background = "default" } [theme.autocomplete] show_user_nicks = true diff --git a/internal/config/theme.go b/internal/config/theme.go index ded0442..d65947a 100644 --- a/internal/config/theme.go +++ b/internal/config/theme.go @@ -1,14 +1,25 @@ package config import ( + "errors" + "github.com/ayn2op/tview" "github.com/gdamore/tcell/v2" ) +var ( + errInvalidType = errors.New("invalid type") +) + type BorderSetWrapper struct{ tview.BorderSet } -func (bw *BorderSetWrapper) UnmarshalTOML(v any) error { - switch v.(string) { +func (bw *BorderSetWrapper) UnmarshalTOML(val any) error { + s, ok := val.(string) + if !ok { + return errInvalidType + } + + switch s { case "plain": bw.BorderSet = tview.BorderSetPlain() case "round": @@ -25,7 +36,12 @@ func (bw *BorderSetWrapper) UnmarshalTOML(v any) error { type AlignmentWrapper struct{ tview.Alignment } func (aw *AlignmentWrapper) UnmarshalTOML(v any) error { - switch v.(string) { + s, ok := v.(string) + if !ok { + return errInvalidType + } + + switch s { case "left": aw.Alignment = tview.AlignmentLeft case "center": @@ -46,16 +62,16 @@ func NewStyleWrapper(style tcell.Style) StyleWrapper { func (sw *StyleWrapper) UnmarshalTOML(v any) error { m, ok := v.(map[string]any) if !ok { - return nil + return errInvalidType } - for k, v := range m { - s, ok := v.(string) + for key, val := range m { + s, ok := val.(string) if !ok { continue } - switch k { + switch key { case "foreground": color := tcell.GetColor(s) sw.Style = sw.Foreground(color) @@ -69,29 +85,21 @@ func (sw *StyleWrapper) UnmarshalTOML(v any) error { } type ( - BorderTheme struct { - Enabled bool `toml:"enabled"` - Padding [4]int `toml:"padding"` - Set BorderSetWrapper `toml:"set"` - - Style StyleWrapper `toml:"style"` + ThemeStyle struct { + NormalStyle StyleWrapper `toml:"normal_style"` ActiveStyle StyleWrapper `toml:"active_style"` } TitleTheme struct { - Style StyleWrapper `toml:"style"` - ActiveStyle StyleWrapper `toml:"active_style"` - Alignment AlignmentWrapper `toml:"alignment"` + ThemeStyle + Alignment AlignmentWrapper `toml:"alignment"` } - Theme struct { - BackgroundColor string `toml:"background_color"` - - Title TitleTheme `toml:"title"` - Border BorderTheme `toml:"border"` - GuildsTree GuildsTreeTheme `toml:"guilds_tree"` - MessagesText MessagesTextTheme `toml:"messages_text"` - Autocomplete AutocompleteTheme `toml:"autocomplete"` + BorderTheme struct { + ThemeStyle + Enabled bool `toml:"enabled"` + Padding [4]int `toml:"padding"` + Set BorderSetWrapper `toml:"set"` } GuildsTreeTheme struct { @@ -100,9 +108,9 @@ type ( Graphics bool `toml:"graphics"` GraphicsColor string `toml:"graphics_color"` - PrivateChannelColor string `toml:"private_channel_color"` - GuildColor string `toml:"guild_color"` - ChannelColor string `toml:"channel_color"` + PrivateChannelStyle StyleWrapper `toml:"private_channel_style"` + GuildStyle StyleWrapper `toml:"guild_style"` + ChannelStyle StyleWrapper `toml:"channel_style"` } MessagesTextTheme struct { @@ -126,4 +134,15 @@ type ( MinWidth uint `toml:"min_width"` MaxHeight uint `toml:"max_height"` } + + Theme struct { + BackgroundColor string `toml:"background_color"` + + Title TitleTheme `toml:"title"` + Border BorderTheme `toml:"border"` + + GuildsTree GuildsTreeTheme `toml:"guilds_tree"` + MessagesText MessagesTextTheme `toml:"messages_text"` + Autocomplete AutocompleteTheme `toml:"autocomplete"` + } ) diff --git a/internal/ui/util.go b/internal/ui/util.go index 5aff8eb..0e01656 100644 --- a/internal/ui/util.go +++ b/internal/ui/util.go @@ -5,32 +5,36 @@ import ( "github.com/ayn2op/tview" ) -func NewConfiguredBox(box *tview.Box, cfg *config.Theme) *tview.Box { - b := cfg.Border - t := cfg.Title - p := b.Padding +// ConfigureBox configures the provided box according to the provided theme. +func ConfigureBox(box *tview.Box, cfg *config.Theme) *tview.Box { + border := cfg.Border + title := cfg.Title + normalBorderStyle, activeBorderStyle := border.NormalStyle.Style, border.ActiveStyle.Style + normalTitleStyle, activeTitleStyle := title.NormalStyle.Style, title.ActiveStyle.Style + p := border.Padding box. - SetBorderStyle(b.Style.Style). - SetBorderSet(b.Set.BorderSet). + SetBorderStyle(normalBorderStyle). + SetBorderSet(border.Set.BorderSet). SetBorderPadding(p[0], p[1], p[2], p[3]). - SetTitleStyle(t.Style.Style). - SetTitleAlignment(t.Alignment.Alignment). + SetTitleStyle(normalTitleStyle). + SetTitleAlignment(title.Alignment.Alignment). SetFocusFunc(func() { - box.SetBorderStyle(b.ActiveStyle.Style) - box.SetTitleStyle(t.ActiveStyle.Style) + box.SetBorderStyle(activeBorderStyle) + box.SetTitleStyle(activeTitleStyle) }). SetBlurFunc(func() { - box.SetBorderStyle(b.Style.Style) - box.SetTitleStyle(t.Style.Style) + box.SetBorderStyle(normalBorderStyle) + box.SetTitleStyle(normalTitleStyle) }) - if b.Enabled { + if border.Enabled { box.SetBorders(tview.BordersAll) } return box } +// Centered creates a new grid with provided primitive aligned in the center. func Centered(p tview.Primitive, width, height int) tview.Primitive { return tview.NewGrid(). SetColumns(0, width, 0).