Skip to content

Support transparent backgrounds in themes - #97

Open
brettbash wants to merge 1 commit into
gammons:mainfrom
brettbash:feat/transparent-backgrounds
Open

brettbash wants to merge 1 commit into
gammons:mainfrom
brettbash:feat/transparent-backgrounds

Conversation

@brettbash

@brettbash brettbash commented Jul 13, 2026

Copy link
Copy Markdown

Summary

  • Theme colors now accept "transparent", "none", or "default" so terminal emulators with background opacity (Ghostty, kitty, alacritty, etc.) can show the wallpaper through slk's UI instead of painting opaque panels.
  • Transparent values resolve to a nil color. lipgloss already skips styling for nil, but slk has a few hand-rolled color conversions that would otherwise render literal black (since NoColor.RGBA() reports opaque black). Those are guarded:
    • message-pane background fills emit the ANSI default-background reset (SGR 49) and foregrounds emit SGR 39
    • derived tints (compose insert background, selection rows) stay transparent when mixed against a transparent background
    • Block Kit attachment color bars fall back to a neutral gray

Usage

[theme]
background = "transparent"

Compatibility

Fully backwards compatible — existing hex/ANSI values parse exactly as before, and no built-in theme uses the new keywords. Covered by new tests in styles_test.go and tint_test.go; full suite passes with -race.

@gammons gammons left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall

Solid, well-scoped change. The core insight — that nil (not lipgloss.NoColor{}) is the correct sentinel because NoColor.RGBA() reports opaque black — is correct and clearly documented. Backwards compatibility is preserved (guarded if overrides.X != "" calls, no built-in theme uses the keywords), and the tests cover case-insensitivity plus both nil directions in mixColors.

I verified the PR's central claim that every hand-rolled color conversion is guarded by grepping all .RGBA() call sites: the only theme-color consumers are tint.go, render.go, and blockkit/color.go (the halfblock.go one operates on image pixels, not theme colors), and all three are now nil-guarded. Coverage is complete. Build and the styles/messages/blockkit suites pass locally.

Issues worth addressing

1. (Design) Selection highlight and compose-insert background silently vanish on transparent themes

When Background is transparent, mixColors returns nil for any derived tint:

  • SelectionTintColor (tint.go:39,44) → the selected-message row gets no background → the "which row is selected" affordance disappears.
  • ComposeInsertBG (styles.go:382) → insert-mode compose box loses its background cue.

This may be an acceptable tradeoff, but it's a real UX regression for exactly the users this feature targets. Consider whether these derived indicators should fall back to a solid color (e.g. Accent) rather than going transparent, so selection stays visible. At minimum this should be a conscious, documented decision. This is the one item I'd want an answer on before merge.

2. (Low) nil sentinel collision in SelectionTintColor

selectionBgFocused/selectionBgUnfocused use nil to mean "not computed yet" (tint.go:38,43). With a transparent Background, mixColors legitimately returns nil, so the cache never populates and mixColors re-runs on every call. Harmless today (the bg == nil branch returns early), but it defeats the caching and is a latent footgun. A separate computed bool flag would be cleaner.

3. (Low) "default" as a synonym for transparent is ambiguous

transparent/none clearly signal "no color," but a user reading background = "default" in TOML could reasonably read it as "the theme's default color" rather than "the terminal's default background." Consider dropping "default" or documenting it explicitly.

4. (Low, docs) wiki/Configuration.md isn't updated

The theme section documents color values but doesn't mention the new transparent/none/default keywords, so the feature is undiscoverable outside the PR body. Worth a one-line addition. (There's no color validation in config.go, so nothing to update there.)

5. (Nit) Magic constant in colorString

The #333333 fallback (blockkit/color.go:50) duplicates the default Border value. A named const (e.g. subduedAttachmentBar) would document intent.

Verdict

Approvable. None of the above are blockers, but #1 is worth a quick answer before merge — it's the difference between "transparent works" and "transparent works but you can't see what row you've selected."

@gammons

gammons commented Jul 23, 2026

Copy link
Copy Markdown
Owner

hi @brettbash see my comments. the first issue is really the one that should be addressed before this can be merged.

@brettbash
brettbash force-pushed the feat/transparent-backgrounds branch from 26938c2 to 02860d7 Compare August 14, 2026 11:53
Theme colors now accept "transparent", "none", or "default", letting
terminal emulators with background opacity (Ghostty, kitty, etc.) show
the wallpaper through slk's panels.

Transparent values resolve to a nil color, which lipgloss renders as
"no color". The hand-rolled color conversions are guarded so nothing
paints literal black for nil: message-pane background fills emit the
ANSI default-background reset (SGR 49), foregrounds emit SGR 39,
derived tints (compose insert bg, selection rows) stay transparent
when mixed against a transparent background, and attachment color
bars fall back to a neutral gray.

Fully backwards compatible: existing hex/ANSI theme values are
unchanged, and no built-in theme uses the new keys.
@brettbash
brettbash force-pushed the feat/transparent-backgrounds branch from 02860d7 to 7d46f75 Compare August 14, 2026 11:55
@gammons

gammons commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Sorry this sat so long — it's the oldest PR in the queue and that's on me. CI had also never been allowed to run (first-time contributor); I've approved it, and the lint failure you'll see is not your fault: it's an old golangci-lint typechecking the go1.27 stdlib (could not import math/rand/v2), fixed on main by 6d39fe5. Your test job passes, and I confirmed the branch rebases onto main with go build, go vet and go test ./internal/ui/... -race all green.

The core mechanism is right, and you got the hard part right. Using nil rather than lipgloss.NoColor{} is the correct call, and I verified why it matters: NoColor{}.RGBA() reports opaque black, which would poison every hand-rolled blending path. parseColor returns a bare nil rather than a typed nil pointer, so there's no interface-nil trap. And your guard coverage is complete — I checked every non-test .RGBA() consumer of theme colors (tint.go:58-59, render.go:515,533, blockkit/color.go:47) and all three are guarded. BgANSI()'s cache survives a nil bg correctly too.

But the feature doesn't actually work as documented, and that's what's blocking. I applied background = "transparent" across all 59 built-in themes and measured:

outcome count
SidebarBackground still opaque 57/59
RailBackground still opaque 59/59
Surface still opaque 59/59
fully transparent UI 0/59

styles.go:344-360 reads sidebar/rail from colors.SidebarBackground — the theme — never from an override, and config.Theme (internal/config/config.go:180-191) has no sidebar_background or rail_background fields at all. 57 themes set both explicitly (themes.go:32,35), so those are permanently opaque and unreachable from user config. Even setting every key a user can set (background + surface + surface_dark), only 2/59 go fully transparent — ANSI Dark and ANSI Light, the only two that don't hardcode sidebar/rail. The documented one-liner gives you a transparent message pane bolted to an opaque sidebar and rail.

I also confirmed my earlier concern about selection, plus two more:

SelectionTint(focused)   = <nil>   -> selected row has NO background
SelectionTint(unfocused) = <nil>
ComposeInsertBG          = <nil>
SelectionForeground      = <nil>   -> selected text in terminal-default fg on #4A9EFF
SearchHighlightFg        = <nil>   -> search matches in terminal-default fg on #E0A030

The last two are readability regressions, not just aesthetics — on a light-fg terminal that's white on orange.

What I'd like:

  1. Make transparency reachable. Either add sidebar_background / rail_background / sidebar_text / sidebar_text_muted to config.Theme, or treat background = "transparent" as cascading to SidebarBackground / RailBackground / Surface / SurfaceDark unless explicitly overridden. Without this it's a no-op for 96% of themes.
  2. Keep selection visible — fall back to a solid Accent/Primary when Background is nil rather than returning nil from mixColors for SelectionTintColor and ComposeInsertBG.
  3. Fix the fg fallthroughs at styles.go:376 and styles.go:387 — fall back to TextPrimary, not nil.
  4. Add a computed bool or sentinel to break the nil cache collision at tint.go:36-46. Right now selectionBgFocused stays nil after computing, so mixColors re-runs every frame — cheap today, but it's on the scroll path we optimize hard elsewhere.
  5. Add tests asserting the actual ANSI payload: bgANSIFor(nil) == "\x1b[49m", fgANSIFor(nil) == "\x1b[39m", and colorString(nil). Your existing tests are well-targeted (TestMixColors_Transparent* would genuinely nil-panic without the guards) but nothing covers the bytes we actually emit, which is the most breakable part.
  6. Document the keywords in wiki/Configuration.md, including which keys must be set for full transparency.
  7. Drop "default" as a keyword — it's ambiguous against "the theme's default color".
  8. Name the #333333 const at blockkit/color.go:50.

To be clear on the product question: I want this feature. 59 themes plus drop-in customs means theming is first-class here, it's opt-in, it's zero-cost for existing users, and terminal transparency is a reasonable thing to want. It just needs to actually work on more than 2 themes before it ships.

@gammons gammons added the changes requested Blocking issues found in review label Sep 3, 2026
@gammons gammons closed this Sep 3, 2026
@gammons gammons reopened this Sep 3, 2026
@gammons

gammons commented Sep 3, 2026

Copy link
Copy Markdown
Owner

#171 has landed and I've re-run CI here — the lint failure is gone and this is green now. That was the stale golangci-lint/go1.27 issue, not anything you did.

Note that #171 also enabled gofmt as an enforced lint check, so please run gofmt -w over your changes when you push the next revision.

My review above still stands — that's what's needed to move this forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes requested Blocking issues found in review in progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants