Skip to content

Commit 7b19fb6

Browse files
committed
feat: add transparent theme
The none theme paints no background and uses the terminal's ANSI palette, so the terminal's color scheme and transparency show through. The view no longer sets the terminal background for this theme, and the sidebar selection uses reverse video to stay readable with any palette.
1 parent cf369ec commit 7b19fb6

10 files changed

Lines changed: 161 additions & 4 deletions

File tree

‎config.example.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ cooldown = "2s"
4242

4343
[ui]
4444
# Available themes: "rose-pine", "ayu-dark", "dracula",
45-
# "gruvbox-dark", "solarized-light".
45+
# "gruvbox-dark", "solarized-light", "none". The "none" theme uses the
46+
# terminal's own background and color palette.
4647
theme = "rose-pine"
4748
unread_badges = true
4849
mention_badges = true

‎internal/config/loader.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func loadFromBytes(data []byte) (*Config, error) {
8888
cfg := defaultValues()
8989
if raw.UI.Theme != "" {
9090
switch raw.UI.Theme {
91-
case ThemeRosePine, ThemeAyuDark, ThemeDracula, ThemeGruvboxDark, ThemeSolarizedLight:
91+
case ThemeRosePine, ThemeAyuDark, ThemeDracula, ThemeGruvboxDark, ThemeSolarizedLight, ThemeNone:
9292
cfg.UI.Theme = raw.UI.Theme
9393
default:
9494
return nil, fmt.Errorf("config validation failed: unknown UI theme %q", raw.UI.Theme)

‎internal/config/loader_test.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,25 @@ channels = ["#go"]
221221
}
222222
}
223223

224+
func TestLoadAcceptsNoneTheme(t *testing.T) {
225+
cfg, err := loadFromBytes([]byte(`
226+
[ui]
227+
theme = "none"
228+
229+
[servers.libera]
230+
address = "irc.libera.chat"
231+
port = 6697
232+
nickname = "dexuser"
233+
channels = ["#go"]
234+
`))
235+
if err != nil {
236+
t.Fatalf("loadFromBytes() error = %v", err)
237+
}
238+
if got := cfg.UI.Theme; got != ThemeNone {
239+
t.Fatalf("UI theme = %q, want %q", got, ThemeNone)
240+
}
241+
}
242+
224243
func TestLoadDefaultsNotifications(t *testing.T) {
225244
cfg, err := loadFromBytes([]byte(`
226245
[servers.libera]

‎internal/config/types.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
ThemeDracula = "dracula"
1414
ThemeGruvboxDark = "gruvbox-dark"
1515
ThemeSolarizedLight = "solarized-light"
16+
ThemeNone = "none"
1617
)
1718

1819
// DesktopMode controls desktop notifications.

‎internal/ui/components/channels/channels.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,13 @@ func (m Model) updateContent() Model {
273273
}
274274

275275
if i == m.cursor {
276-
itemStyle = itemStyle.Background(m.theme.Colors.Sidebar.Selection)
276+
// Without a selection color, swap the terminal's own colors so
277+
// the selection stays readable with any palette.
278+
if _, ok := m.theme.Colors.Sidebar.Selection.(lipgloss.NoColor); ok {
279+
itemStyle = itemStyle.Reverse(true)
280+
} else {
281+
itemStyle = itemStyle.Background(m.theme.Colors.Sidebar.Selection)
282+
}
277283
}
278284

279285
itemStyle = itemStyle.Width(m.viewport.Width())

‎internal/ui/components/channels/channels_test.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package channels
22

33
import (
44
"fmt"
5+
"regexp"
56
"strings"
67
"testing"
78

@@ -362,3 +363,13 @@ func newScrollableChannelsModel() Model {
362363
})
363364
return m.SetSize(25, 10)
364365
}
366+
367+
func TestNoneThemeSelectionReversesTerminalColors(t *testing.T) {
368+
m := New(styles.NoneTheme(), []*config.Server{{Name: "libera", Channels: []string{"#go"}}})
369+
m = m.SetSize(30, 10)
370+
371+
reverse := regexp.MustCompile(`\x1b\[(?:\d+;)*7[;m]`)
372+
if view := m.View(30, 10); !reverse.MatchString(view) {
373+
t.Fatalf("expected selected row to use reverse video, got %q", view)
374+
}
375+
}

‎internal/ui/styles/colorschemes.go‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,93 @@ func SolarizedLightTheme() Theme {
427427

428428
return theme
429429
}
430+
431+
// NoneTheme paints no background and uses the terminal's ANSI palette, so the
432+
// terminal's own color scheme and transparency show through.
433+
func NoneTheme() Theme {
434+
theme := Theme{}
435+
436+
theme.Colors.Base.Background = lipgloss.NoColor{}
437+
theme.Colors.Base.Foreground = lipgloss.NoColor{}
438+
theme.Colors.Base.Surface = lipgloss.NoColor{}
439+
theme.Colors.Base.Border = lipgloss.Color("8")
440+
theme.Colors.Base.Accent = lipgloss.Color("4")
441+
theme.Colors.Base.Error = lipgloss.Color("1")
442+
theme.Colors.Base.Dimmed = lipgloss.Color("8")
443+
theme.Colors.Base.Subtle = lipgloss.Color("8")
444+
theme.Colors.Base.OnAccent = lipgloss.Color("0")
445+
theme.Colors.Base.Divider = lipgloss.Color("8")
446+
447+
theme.Colors.Sidebar.Server = lipgloss.Color("4")
448+
theme.Colors.Sidebar.Unread = lipgloss.Color("6")
449+
theme.Colors.Sidebar.Notification = lipgloss.Color("3")
450+
theme.Colors.Sidebar.Mention = lipgloss.Color("1")
451+
theme.Colors.Sidebar.Selection = lipgloss.NoColor{}
452+
453+
theme.Colors.Chat.Nickname = lipgloss.Color("4")
454+
theme.Colors.Chat.InactiveNickname = lipgloss.Color("8")
455+
theme.Colors.Chat.Mention = lipgloss.Color("1")
456+
theme.Colors.Chat.Separator = lipgloss.Color("8")
457+
theme.Colors.Chat.Self = lipgloss.Color("2")
458+
theme.Colors.Chat.Connected = lipgloss.Color("2")
459+
theme.Colors.Chat.Disconnected = lipgloss.Color("1")
460+
theme.Colors.Chat.UserEvents = lipgloss.Color("8")
461+
theme.Colors.Chat.ServerMessage = lipgloss.Color("3")
462+
463+
theme.Colors.Palette.Border = lipgloss.Color("4")
464+
theme.Colors.Palette.Highlight = lipgloss.Color("4")
465+
466+
theme.Colors.Nicknames = []Color{
467+
lipgloss.Color("2"),
468+
lipgloss.Color("3"),
469+
lipgloss.Color("4"),
470+
lipgloss.Color("5"),
471+
lipgloss.Color("6"),
472+
lipgloss.Color("10"),
473+
lipgloss.Color("11"),
474+
lipgloss.Color("12"),
475+
lipgloss.Color("13"),
476+
lipgloss.Color("14"),
477+
}
478+
479+
theme.Styles.App = lipgloss.NewStyle().
480+
Background(theme.Colors.Base.Background).
481+
Foreground(theme.Colors.Base.Foreground)
482+
483+
theme.Styles.Sidebar = lipgloss.NewStyle().
484+
Background(theme.Colors.Base.Background).
485+
Foreground(theme.Colors.Base.Foreground).
486+
BorderStyle(lipgloss.HiddenBorder()).
487+
BorderBackground(theme.Colors.Base.Background)
488+
489+
theme.Styles.UnreadItem = lipgloss.NewStyle().
490+
Background(theme.Colors.Base.Background).
491+
Foreground(theme.Colors.Sidebar.Unread)
492+
493+
theme.Styles.NotifiedItem = lipgloss.NewStyle().
494+
Background(theme.Colors.Base.Background).
495+
Foreground(theme.Colors.Sidebar.Notification).
496+
Bold(true)
497+
498+
theme.Styles.MentionedItem = lipgloss.NewStyle().
499+
Background(theme.Colors.Base.Background).
500+
Foreground(theme.Colors.Sidebar.Mention).
501+
Bold(true)
502+
503+
theme.Styles.ServerItem = lipgloss.NewStyle().
504+
Background(theme.Colors.Base.Background).
505+
Foreground(theme.Colors.Sidebar.Server).
506+
Bold(true)
507+
508+
theme.Styles.ChatArea = lipgloss.NewStyle().
509+
Background(theme.Colors.Base.Background).
510+
Foreground(theme.Colors.Base.Foreground).
511+
BorderBackground(theme.Colors.Base.Background).
512+
Border(lipgloss.HiddenBorder())
513+
514+
theme.Styles.InputField = lipgloss.NewStyle().
515+
Background(theme.Colors.Base.Surface).
516+
Foreground(theme.Colors.Base.Foreground)
517+
518+
return theme
519+
}

‎internal/ui/styles/theme.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,10 @@ func RightDividerBorder() lipgloss.Border {
8181
border.Right = lipgloss.NormalBorder().Right
8282
return border
8383
}
84+
85+
// HasBackground reports whether the theme paints its own background. Without
86+
// one, the terminal's default background shows through.
87+
func (t Theme) HasBackground() bool {
88+
_, isNoColor := t.Colors.Base.Background.(lipgloss.NoColor)
89+
return t.Colors.Base.Background != nil && !isNoColor
90+
}

‎internal/ui/ui.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ func New(cfg *config.Config) *Model {
9898
theme = styles.GruvboxDarkTheme()
9999
case config.ThemeSolarizedLight:
100100
theme = styles.SolarizedLightTheme()
101+
case config.ThemeNone:
102+
theme = styles.NoneTheme()
101103
}
102104
m := Model{
103105
config: cfg,
@@ -402,7 +404,11 @@ func (m *Model) View() tea.View {
402404
view.AltScreen = true
403405
view.ReportFocus = true
404406
view.MouseMode = tea.MouseModeCellMotion
405-
view.BackgroundColor = m.theme.Colors.Base.Background
407+
// Setting a background color changes the terminal's own background, so
408+
// leave it unset when the theme should follow the terminal.
409+
if m.theme.HasBackground() {
410+
view.BackgroundColor = m.theme.Colors.Base.Background
411+
}
406412
return view
407413
}
408414

‎internal/ui/ui_test.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,7 @@ func TestNewUsesConfiguredTheme(t *testing.T) {
13571357
{name: "Dracula", configured: config.ThemeDracula, want: styles.DraculaTheme()},
13581358
{name: "Gruvbox Dark", configured: config.ThemeGruvboxDark, want: styles.GruvboxDarkTheme()},
13591359
{name: "Solarized Light", configured: config.ThemeSolarizedLight, want: styles.SolarizedLightTheme()},
1360+
{name: "None", configured: config.ThemeNone, want: styles.NoneTheme()},
13601361
}
13611362

13621363
for _, tt := range tests {
@@ -1371,6 +1372,21 @@ func TestNewUsesConfiguredTheme(t *testing.T) {
13711372
}
13721373
}
13731374

1375+
func TestViewBackgroundFollowsTheme(t *testing.T) {
1376+
t.Setenv("XDG_DATA_HOME", t.TempDir())
1377+
1378+
m := newActivityTestModel()
1379+
m.activeBuffer = makeBufferKey("libera", "#go")
1380+
if got := m.View().BackgroundColor; got == nil {
1381+
t.Fatal("themed view background = nil, want theme background")
1382+
}
1383+
1384+
m.theme = styles.NoneTheme()
1385+
if got := m.View().BackgroundColor; got != nil {
1386+
t.Fatalf("none theme view background = %#v, want nil", got)
1387+
}
1388+
}
1389+
13741390
func TestConfiguredHistoryLoadOnlyLoadsReadState(t *testing.T) {
13751391
t.Setenv("XDG_DATA_HOME", t.TempDir())
13761392

0 commit comments

Comments
 (0)