[THEMES] Matugen Theme Improvments - Ghostty, GTK, QT, etc.
Too many issues about colors not being right, etc. So I just want to make a meta issue.
- [x] Ghostty+kitty Improvements
- Done Created a custom py script (standard lib only) that maps some matugen colors into 16 colors, looks good to me.
- Ghostty can be themed with a 16-color palette, plus some extras for cursor color and background colors. Some are easy enough to theme, but in general it's tough to figure out the material mapping from matugen since it's not really designed for such use cases.
- Consider exploring different schems matugen has and seeing if 16 different contrasty colors can be gotten that way for light and dark mode ( scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot)
- Ghostty can also be styled with CSS, which may be the way - but need a template that honors the existing config values like its window transparency settings and things, they may be exposed as scss vars.
- [ ] GTK3 Improvements
- Seems pretty decent but some users report issues
- Figure out if theres a feasible way to make it independent of a specific theme, right now it depends on Colloid specifically.
- [ ] QT Improvements
- Seems pretty decent to me but requires the qt6ct-kde fork, in general it's kinda ugly but might just be QT
Suggestions:
Terminal: Use Wallust for a 16-color palette.
GTK: Instead of Colloid, consider adw-gtk3, which works well with the default Matugen template.
Alternative approach: Skip full theming and provide a script exposing $is_light. Users could then integrate this into their own configs or Matugen setups, which many already have.
Suggestions:
Terminal: Use Wallust for a 16-color palette.
I'd prefer not to require an extra dependency, but for just Ghostty maybe it's fine to use it if its available over matugen. Pretty sure matugen just cant do it
GTK: Instead of Colloid, consider adw-gtk3, which works well with the default Matugen template.
I was unsuccessful just dumping the default template into ~/.config/gtk-3.0/gtk.css or the like. GTK4 worked better, but that's basically what it does now (gtk4 not dependent on any specific theme)
Alternative approach: Skip full theming and provide a script exposing $is_light. Users could then integrate this into their own configs or Matugen setups, which many already have.
The full theming generates the colors but shouldnt interfere unless explicitly configured by the user, so it creates ~/.config/gtk-3.0/dank-colors.css - but they wont get symlink'd to gtk.css unless pressing the button in settings. Ditto for the others like Ghostty, just require some user config to use the generated colors.
the current mode can be gotten with IPC though, qs -c dms ipc call theme getMode
I made a custom py script to get b16 colors for ghostty, and it seems to be pretty decent now, still needs work though
but i'll await other opinions
I made a custom py script to get b16 colors for ghostty, and it seems to be pretty decent now, still needs work though
but i'll await other opinions
This is nice, is it possible to integrate it into kitty?
@Vantesh added kitty and updated readme that its supported
echo "include dank-theme.conf" >> ~/.config/kitty/kitty.conf
seems to be a perfect mapping of the ghostty logic, so any tweaks to the b16 logic would work for both.
Made more updates, I'm pretty happy with the terminal theme now.
ghostty + kitty, was not happy with wallust output.
Currently the permissions and username are unreadable (eza command) on light mode when generating theme with dank16. It doesnt change those colors on any scheme - I think it's something to do with LS_COLORS
wallust applies the colors.
@Vantesh I tweaked the yellow and green pallettes of d16 see if that works better.
There are some guidelines for ansi palettes like - you have a black/red/blue/green/cyan/magenta - lighter and darker variations of each. I deviate from it in the cyan/blue/magenta palettes to keep colors closer to the primary theme, but prefer to keep the red/green/yellow|orange distinct since, it makes error/warn/debug logs distinct which I like - if the log output uses ansi colors.
@bbedward its fine now, but some wallpapers have unreadable colors on light mode. Maybe you can add some tweaks that makes sure text has enough WCAG contrast ratio against background (nudges brightness until readable).
Example of a modified script, Forgive me for butchering your script :)
#!/usr/bin/env python3
import colorsys
import sys
import os
import json
from pathlib import Path
def hex_to_rgb(hex_color):
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16)/255.0 for i in (0, 2, 4))
def rgb_to_hex(r, g, b):
r = max(0, min(1, r))
g = max(0, min(1, g))
b = max(0, min(1, b))
return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
def blend_hex(hex_a, hex_b, ratio: float):
ar, ag, ab = hex_to_rgb(hex_a)
br, bg, bb = hex_to_rgb(hex_b)
r = ar * ratio + br * (1 - ratio)
g = ag * ratio + bg * (1 - ratio)
b = ab * ratio + bb * (1 - ratio)
return rgb_to_hex(r, g, b)
def generate_palette(base_color, is_light=False, honor_primary=None):
r, g, b = hex_to_rgb(base_color)
h, s, v = colorsys.rgb_to_hsv(r, g, b)
palette = []
if is_light:
palette.append("#f8f8f8")
else:
palette.append("#1a1a1a")
def luminance(hex_color):
rr, gg, bb = hex_to_rgb(hex_color)
def channel(c):
return c/12.92 if c <= 0.03928 else ((c+0.055)/1.055) ** 2.4
return 0.2126 * channel(rr) + 0.7152 * channel(gg) + 0.0722 * channel(bb)
def contrast_ratio(hex_a, hex_b):
la = luminance(hex_a)
lb = luminance(hex_b)
hi = max(la, lb)
lo = min(la, lb)
return (hi + 0.05) / (lo + 0.05)
def ensure_contrast(hex_color, bg_hex, min_ratio=4.5):
"""Adjust value (v) in HSV to reach at least min_ratio against bg_hex.
Keeps hue and saturation, only nudges brightness. Returns hex string."""
cr = contrast_ratio(hex_color, bg_hex)
if cr >= min_ratio:
return hex_color
# try darkening then lightening, small steps
r0, g0, b0 = hex_to_rgb(hex_color)
hh, ss, vv = colorsys.rgb_to_hsv(r0, g0, b0)
# try up to 20 steps
for step in range(1, 21):
# darken
nv = max(0.0, vv - step * 0.03)
cand = rgb_to_hex(*colorsys.hsv_to_rgb(hh, ss, nv))
if contrast_ratio(cand, bg_hex) >= min_ratio:
return cand
# lighten
nv = min(1.0, vv + step * 0.03)
cand = rgb_to_hex(*colorsys.hsv_to_rgb(hh, ss, nv))
if contrast_ratio(cand, bg_hex) >= min_ratio:
return cand
# fallback: return original if no sufficient contrast found
return hex_color
# catppuccin colors, idk i love them :)
cat = {'red_light': '#d20f39', 'green_light': '#40a02b', 'yellow_light': '#df8e1d', 'red_dark': '#f38ba8', 'green_dark': '#8bd5a1', 'yellow_dark': '#dba63a'}
blend_ratio = 0.6 if is_light else 0.5
dark_yellow_blend = 0.35 if not is_light else blend_ratio
dark_green_blend = 0.75 if not is_light else blend_ratio
red_h = 0.0
if is_light:
gen_red = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.75, 0.85))
palette.append(ensure_contrast(blend_hex(cat['red_light'], gen_red, blend_ratio), "#f8f8f8"))
else:
gen_red = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.6, 0.8))
palette.append(ensure_contrast(blend_hex(cat['red_dark'], gen_red, blend_ratio), "#1a1a1a"))
green_h = 0.33
if is_light:
gen_green = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.8, 0.65), v * 0.9))
palette.append(ensure_contrast(blend_hex(cat['green_light'], gen_green, blend_ratio), "#f8f8f8"))
else:
gen_green = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.65, 0.5), v * 0.9))
palette.append(ensure_contrast(blend_hex(cat['green_dark'], gen_green, dark_green_blend), "#1a1a1a"))
yellow_h = 0.16
if is_light:
gen_yellow = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.7, 0.55), v * 1.2))
palette.append(ensure_contrast(blend_hex(cat['yellow_light'], gen_yellow, blend_ratio), "#f8f8f8"))
else:
gen_yellow = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.5, 0.45), v * 1.4))
palette.append(ensure_contrast(blend_hex(cat['yellow_dark'], gen_yellow, dark_yellow_blend), "#1a1a1a"))
if is_light:
palette.append(ensure_contrast(rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.9, 0.7), v * 1.1)), "#f8f8f8"))
else:
palette.append(ensure_contrast(rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.8, 0.6), min(v * 1.6, 1.0))), "#1a1a1a"))
mag_h = h - 0.03 if h >= 0.03 else h + 0.97
if honor_primary:
hr, hg, hb = hex_to_rgb(honor_primary)
hh, hs, hv = colorsys.rgb_to_hsv(hr, hg, hb)
if is_light:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(hh, max(hs * 0.9, 0.7), hv * 0.85)))
else:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(hh, hs * 0.8, hv * 0.75)))
elif is_light:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.75, 0.6), v * 0.9)))
else:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.7, 0.6), v * 0.85)))
cyan_h = h + 0.08
if honor_primary:
if is_light:
palette.append(honor_primary)
else:
palette.append(honor_primary)
elif is_light:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(cyan_h, max(s * 0.8, 0.65), v * 1.05)))
else:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(cyan_h, max(s * 0.6, 0.5), min(v * 1.25, 0.85))))
if is_light:
palette.append("#2e2e2e")
palette.append("#4a4a4a")
else:
palette.append("#abb2bf")
palette.append("#5c6370")
if is_light:
gen_red2 = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.6, 0.9))
palette.append(blend_hex(cat['red_light'], gen_red2, blend_ratio))
gen_green2 = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.7, 0.6), v * 1.25))
palette.append(blend_hex(cat['green_light'], gen_green2, blend_ratio))
gen_yellow2 = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.6, 0.5), v * 1.35))
palette.append(blend_hex(cat['yellow_light'], gen_yellow2, blend_ratio))
if honor_primary:
hr, hg, hb = hex_to_rgb(honor_primary)
hh, hs, hv = colorsys.rgb_to_hsv(hr, hg, hb)
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(hh, min(hs * 1.1, 1.0), min(hv * 1.2, 1.0))))
else:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.8, 0.7), min(v * 1.3, 1.0))))
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.9, 0.75), min(v * 1.25, 1.0))))
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(cyan_h, max(s * 0.75, 0.65), min(v * 1.25, 1.0))))
else:
gen_red2 = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.45, min(1.0, 0.9)))
palette.append(blend_hex(cat['red_dark'], gen_red2, blend_ratio))
gen_green2 = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.5, 0.4), min(v * 1.5, 0.9)))
palette.append(blend_hex(cat['green_dark'], gen_green2, blend_ratio))
gen_yellow2 = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.4, 0.35), min(v * 1.6, 0.95)))
palette.append(blend_hex(cat['yellow_dark'], gen_yellow2, blend_ratio))
if honor_primary:
hr, hg, hb = hex_to_rgb(honor_primary)
hh, hs, hv = colorsys.rgb_to_hsv(hr, hg, hb)
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(hh, min(hs * 1.2, 1.0), min(hv * 1.1, 1.0))))
else:
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.6, 0.5), min(v * 1.5, 0.9))))
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.7, 0.6), min(v * 1.3, 0.9))))
palette.append(rgb_to_hex(*colorsys.hsv_to_rgb(h + 0.02 if h + 0.02 <= 1.0 else h + 0.02 - 1.0, max(s * 0.6, 0.5), min(v * 1.2, 0.85))))
if is_light:
palette.append("#1a1a1a")
else:
palette.append("#ffffff")
return palette
def read_wal_colors(path: Path):
try:
with path.open() as f:
data = json.load(f)
primary = data.get('primary')
primary_container = data.get('primary_container')
return primary, primary_container
except Exception:
return None, None
def main():
args = sys.argv[1:]
is_light = '--light' in args
honor_primary = None
if '--honor-primary' in args:
try:
i = args.index('--honor-primary')
honor_primary = args[i+1]
if not honor_primary.startswith('#'):
honor_primary = '#' + honor_primary
except Exception:
honor_primary = None
base = None
for a in args:
if not a.startswith('-'):
base = a
break
if base:
if not base.startswith('#'):
base = '#' + base
else:
cache = Path(os.path.expanduser('~/.cache/wal/colors.json'))
p, pc = read_wal_colors(cache)
if pc:
base = pc if pc.startswith('#') else ('#' + pc)
elif p:
base = p if p.startswith('#') else ('#' + p)
else:
print('Error: no base color provided and wal cache not found', file=sys.stderr)
sys.exit(1)
if not honor_primary:
cache = Path(os.path.expanduser('~/.cache/wal/colors.json'))
p, pc = read_wal_colors(cache)
if p:
honor_primary = p if p.startswith('#') else ('#' + p)
colors = generate_palette(base, is_light=is_light, honor_primary=honor_primary)
kitty_colors = [(f"color{i}", colors[i]) for i in range(min(16, len(colors)))]
for name, color in kitty_colors:
print(f"{name} {color}")
if __name__ == '__main__':
main()
// colors.json ( Generated by matugen)
{
"primary": "#c3cd7c",
"primary_container": "#434b05"
}
This goes from something like this
To this
I added some ensure_contrast function and passed in the background color, seems good to me on all the backgrounds I tried now:
before:
after:
Hi! Is it expected behavior that ghostty theme colors get applied only after closing all opened terminal instances and relaunching it?
Hi! Is it expected behavior that ghostty theme colors get applied only after closing all opened terminal instances and relaunching it?
Ghostty doesn't auto reload config, but you can manually reload with Ctrl+shift+,
Is there a way to make it autoreload after wallpaper switching? I have my wallpapers set to change every 30 min. Also, thank you very much for everyone involved in this project. After so much tinkering with waybar and never get the exact result I was expecting, this shell turned out to be all I wanted and more.
There might be an ipc, I'll have to check it
Ghostty doesn't auto reload config, but you can manually reload with Ctrl+shift+,
pkill -USR2 -x ghostty This works
Only on the lastest version though
Ghostty doesn't auto reload config, but you can manually reload with Ctrl+shift+,
pkill -USR2 -x ghosttyThis worksOnly on the lastest version though
This worked nicely!
Added it to the matugen-worker so it'll update on matugen runs
@bbedward Is matugen being run twice, once from theme.qml and again from matugen-worker.sh? if so, is it possible to move the matugen runs in matugen-worker.sh and theme.qml only runs the worker ?
@bbedward Is matugen being run twice, once from
theme.qmland again frommatugen-worker.sh? if so, is it possible to move the matugen runs inmatugen-worker.shandtheme.qmlonly runs the worker ?
It runs it twice only when using auto theme (from wallpaper) One is generating the internal colors, then the script is generating all the system themes.
It could be done once if the worker echo'd the json back at the end -I'll see how much that delays things.
This one a6a41d4de182a139a77856f95bff72b3feed783b moves matugen exclusively to the worker - it writes a desired state, and a fileview watches it for changes and loads them when it changes and theme is dynamic. Which, is kinda working like custom themes do now.
I have tried generating the colors with matugen and storing them in $statedir/dms-colors.json and it works. Instead of using the printf "%s" "$JSON" >"$STATE_DIR/dms-colors.json" logic, try the template below.
{
"colors": {
"dark": {<* for name, value in colors *>
"{{name}}": "{{value.default.hex}}"<* if not loop.last *>,<* endif *><* endfor *>
},
"light": {<* for name, value in colors *>
"{{name}}": "{{value.light.hex}}"<* if not loop.last *>,<* endif *><* endfor *>
}
}
}
Add it as dank.json in matugen, then in have matugen generate it to $statedir/dms-colors.json.
Now matugen can only be called once and generates both the shell and the rest of the themes
https://github.com/user-attachments/assets/1e2e2117-431d-4e65-b2c8-f2f48e3e0b10
The only problem i have found so far is that on monochrome scheme, some colors are unreadable
That is a far better solution, just did it - thanks!
Re: monochrome, for that it would mainly need some special handling for primaryContainer, to get contrast with the primary color.
Colloid theme aren't working anymore? Why? (i'm on latest git commit)
hello is there osc implementation planned for this? Is there any way to support other terminals ourself? I'm currently using foot
Colloid theme aren't working anymore? Why? (i'm on latest git commit)
![]()
matugen updated and broke a few tings
Colloid theme aren't working anymore? Why? (i'm on latest git commit)
![]()
Colloid guidance was replaced with adwaita and adw-gtk3, it's just way easier
hello is there osc implementation planned for this? Is there any way to support other terminals ourself? I'm currently using foot
We just need templates for other terminals
hello is there osc implementation planned for this? Is there any way to support other terminals ourself? I'm currently using foot
I added foot and alacritty https://danklinux.com/docs/dankmaterialshell/application-themes#foot
After an update on my system that updated both ghostty and dms, terminal isn't changing colors dynamically anymore. I can see the .config/ghostty/config-dankcolors file changing when wallpaper changes. It's probably a ghostty 1.2.3 thing, right? I'm running ghostty as a systemd service, if it matters.
After an update on my system that updated both ghostty and dms, terminal isn't changing colors dynamically anymore. I can see the .config/ghostty/config-dankcolors file changing when wallpaper changes. It's probably a ghostty 1.2.3 thing, right? I'm running ghostty as a systemd service, if it matters.
What's the dms version ? it was updated for matugen 3, and a different color algo that moved to dms-cli
hello is there osc implementation planned for this? Is there any way to support other terminals ourself? I'm currently using foot
I added foot and alacritty https://danklinux.com/docs/dankmaterialshell/application-themes#foot
thanks a bunch!
but i'll await other opinions

