DankMaterialShell icon indicating copy to clipboard operation
DankMaterialShell copied to clipboard

[THEMES] Matugen Theme Improvments - Ghostty, GTK, QT, etc.

Open bbedward opened this issue 3 months ago • 37 comments

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

bbedward avatar Aug 25 '25 02:08 bbedward

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.

Vantesh avatar Aug 25 '25 08:08 Vantesh

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

bbedward avatar Aug 25 '25 13:08 bbedward

I made a custom py script to get b16 colors for ghostty, and it seems to be pretty decent now, still needs work though

Image

but i'll await other opinions

bbedward avatar Aug 25 '25 15:08 bbedward

I made a custom py script to get b16 colors for ghostty, and it seems to be pretty decent now, still needs work though

Image but i'll await other opinions

This is nice, is it possible to integrate it into kitty?

Vantesh avatar Aug 25 '25 15:08 Vantesh

@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.

bbedward avatar Aug 25 '25 20:08 bbedward

Made more updates, I'm pretty happy with the terminal theme now.

Image

ghostty + kitty, was not happy with wallust output.

bbedward avatar Aug 26 '25 03:08 bbedward

Image Image

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.

Image

Vantesh avatar Aug 27 '25 10:08 Vantesh

@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 avatar Aug 27 '25 13:08 bbedward

@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

Image

To this

Image

Vantesh avatar Aug 27 '25 17:08 Vantesh

I added some ensure_contrast function and passed in the background color, seems good to me on all the backgrounds I tried now:

before:

Image

after:

Image

bbedward avatar Aug 27 '25 19:08 bbedward

Hi! Is it expected behavior that ghostty theme colors get applied only after closing all opened terminal instances and relaunching it?

dolence avatar Sep 24 '25 15:09 dolence

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+,

bbedward avatar Sep 24 '25 15:09 bbedward

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.

dolence avatar Sep 24 '25 16:09 dolence

There might be an ipc, I'll have to check it

bbedward avatar Sep 24 '25 16:09 bbedward

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

Vantesh avatar Sep 24 '25 16:09 Vantesh

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

This worked nicely!

Image

dolence avatar Sep 24 '25 17:09 dolence

Added it to the matugen-worker so it'll update on matugen runs

bbedward avatar Sep 24 '25 18:09 bbedward

@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 ?

Vantesh avatar Sep 25 '25 20:09 Vantesh

@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 ?

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.

bbedward avatar Sep 25 '25 20:09 bbedward

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.

bbedward avatar Sep 25 '25 20:09 bbedward

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.

Image

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

Vantesh avatar Sep 25 '25 22:09 Vantesh

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.

bbedward avatar Sep 25 '25 22:09 bbedward

Colloid theme aren't working anymore? Why? (i'm on latest git commit)

Image Image

linuxmobile avatar Oct 27 '25 16:10 linuxmobile

hello is there osc implementation planned for this? Is there any way to support other terminals ourself? I'm currently using foot

oihv avatar Nov 02 '25 07:11 oihv

Colloid theme aren't working anymore? Why? (i'm on latest git commit)

Image Image

matugen updated and broke a few tings

ryzendew avatar Nov 02 '25 10:11 ryzendew

Colloid theme aren't working anymore? Why? (i'm on latest git commit)

Image Image

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

bbedward avatar Nov 02 '25 13:11 bbedward

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

bbedward avatar Nov 02 '25 15:11 bbedward

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.

dolence avatar Nov 03 '25 18:11 dolence

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

bbedward avatar Nov 03 '25 18:11 bbedward

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!

oihv avatar Nov 04 '25 06:11 oihv