├── Makefile ├── README.md ├── chameleon.py ├── config.yaml └── demo.gif /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | PREFIX = ~/.local 3 | CONFIG = ~/.config 4 | PIPVER = `command -v pip3 || command -v pip` 5 | install: 6 | mkdir -p $(DESTDIR)$(PREFIX)/bin 7 | cp -f chameleon.py $(DESTDIR)$(PREFIX)/bin/chameleon.py 8 | @echo "chameleon.py has been installed to $(DESTDIR)$(PREFIX)/bin/chameleon.py" 9 | mkdir -p $(DESTDIR)$(CONFIG)/chameleon 10 | cp -f config.yaml $(DESTDIR)$(CONFIG)/chameleon/config.yaml 11 | @echo "config file for chameleon.py created in $(DESTDIR)$(PREFIX)/config/config.yaml" 12 | @echo "installing $(PIPVER) dependencies" 13 | @$(PIPVER) install --user whichcraft || echo "dependencies couldn't be installed install pip and rerun" 14 | uninstall: 15 | rm -f $(DESTDIR)$(PREFIX)/bin/chameleon.py 16 | @echo "removed $(DESTDIR)$(PREFIX)/bin/chameleon.py" 17 | rm -rf $(DESTDIR)$(CONFIG)/chameleon 18 | @echo "removed $(DESTDIR)$(CONFIG)/chameleon" 19 | $(PIPVER) uninstall whichcraft 20 | .PHONY: install uninstall pipversion 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chameleon 2 | 3 | ![alt-text](/demo.gif) 4 | 5 | 6 | This script acts as an extension to wal, by taking the generated colors and theming anything that can be themed, all in one script. 7 | 8 | If the script detects you have certain programs on your system, it will try to generate themes for them. 9 | 10 | The current programs are ones that I use, but feel free to add more and send a PR! 11 | 12 | # Examples 13 | 14 | 15 | 16 | ![alt-text](https://i.imgur.com/araXbD4.jpg) 17 | 18 | 19 | Programs that use GTK themes like Thunar and Baobab should just pick up the theme, assuming you have selected the `oomox-xresources-reverse` theme in `lxappearance`. Spotify, Discord, Firefox, and gnuplot are shown here rocking their custom generated themes. 20 | 21 | Programs that use Qt themes can also be configured to take themes from GTK, meaning we can theme them as well! The programs must be launched with the `--style gtk2` flag, and you must install and configure [qt5-styleplugins](https://www.archlinux.org/packages/community/x86_64/qt5-styleplugins/). 22 | 23 | ## Installation 24 | 25 | ```bash 26 | git clone https://github.com/GideonWolfe/Chameleon/ 27 | cd Chameleon 28 | make install 29 | ``` 30 | 31 | ## Usage 32 | 33 | * `chameleon -i [path to picture] [options for wal]` 34 | * `chameleon -t [wal theme] [options for wal]` 35 | 36 | ## Configuration 37 | 38 | Configuration of `chameleon` is done through the file `$HOME/.config/chameleon/config.yaml` 39 | 40 | Here, one can specify options specific to a single program, or even specify custom commands to be run every time you apply a theme. 41 | 42 | Most programs will have a `path` attribute which may or may not be necessary depending on your setup. For example, if you were using a cloned, local version of `wal-discord`, one 43 | might want to specify a specific path where `chameleon` can find this specific executable. 44 | 45 | If the `path` attribute is not given for a program, it is assumed that the program is located in your `$PATH` and will be run as a standalone command. 46 | 47 | ## Programs supported 48 | * [oomox](https://github.com/themix-project/oomox) for GTK and Spotify 49 | 50 | * [Zathura-Pywal](https://github.com/GideonWolfe/Zathura-Pywal) 51 | 52 | * [Gnuplot-Pywal](https://github.com/GideonWolfe/Gnuplot-Pywal) 53 | 54 | * [pyWalNeopixels](https://github.com/Paul-Houser/pyWalNeopixels) 55 | 56 | * [slickgreeter-pywal](https://github.com/Paul-Houser/slickgreeter-pywal) 57 | 58 | * [StartTree](https://github.com/Paul-Houser/StartTree): Dynamic browser startpage 59 | 60 | * [ckb-next](https://github.com/ckb-next/ckb-next) for corsair keyboards 61 | 62 | * [razer-cli](https://github.com/LoLei/razer-cli) for razer devices 63 | 64 | * [telegram-palette-gen](https://github.com/matgua/telegram-palette-gen) for telegram-desktop 65 | 66 | * [spicetify](https://github.com/khanhas/spicetify-cli): CSS Customization engine for spotify 67 | 68 | * [Pywalfox](https://github.com/Frewacom/Pywalfox) to theme FireFox and DuckDuckGo on the fly 69 | 70 | * [pywal-discord](https://github.com/FilipLitwora/pywal-discord) to theme Discord 71 | 72 | * [wal-discord](https://github.com/guglicap/wal-discord) to theme Discord 73 | 74 | ## Planned support 75 | 76 | ## Notes 77 | To get the most complete theme possible, check out my [dotfiles](https://github.com/GideonWolfe/dots). Here you can find the configurations to get these colors on many other programs, such as rofi, polybar, firefox, and more. Since they update automatically, there was no need to include them in this script. 78 | 79 | To apply icon themes, you need one of the icon sets supported by oomox. Change the icons section of the config file to look for the folder your desired icons are in, and change the command to the appropriate variant. 80 | 81 | * [gnome-color-icons](https://aur.archlinux.org/packages/gnome-colors-icon-theme/) 82 | * [archdroid icons](https://aur.archlinux.org/packages/archdroid-icon-theme/) 83 | * [Materia icons](https://aur.archlinux.org/packages/materia-theme-git/) 84 | 85 | To use the new icon themes added to oomox, you have to create a executable file in ```$HOME/.local/bin``` with a similar naming schema like the example command used for gnome colors icon theme in ```config.yaml```. The content of the executable should be something like: 86 | ``` 87 | #!/bin/bash 88 | cd /opt/oomox 89 | exec ./plugins/{path to change_color.sh file for your respective icon theme under plugins} "$@" 90 | ``` 91 | With the above executable created, all you have to do is to give it execute permission by using ```chmod +x``` and use the name of the executable as the command for your icon theme in the ```config.yaml``` file. **Note: Make sure ```$HOME/.local/bin``` is added to your PATH.** 92 | 93 | ## Upgrade from v1 to v2 94 | 95 | Simply delete the old `chameleon` executable at `/usr/local/bin/chameleon`. Now use `chameleon.py` which should be symlinked to `$HOME/.local/bin/chameleon.py`. 96 | 97 | `$HOME/.local/bin/` must be on your `$PATH` 98 | -------------------------------------------------------------------------------- /chameleon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import yaml 5 | import os 6 | import subprocess 7 | from os.path import expanduser 8 | from shutil import copyfile 9 | from whichcraft import which 10 | from shutil import which 11 | 12 | # _ _ _ _ _ _ _ _ 13 | # | | | | |_(_) (_) |_(_) ___ ___ 14 | # | | | | __| | | | __| |/ _ \/ __| 15 | # | |_| | |_| | | | |_| | __/\__ \ 16 | # \___/ \__|_|_|_|\__|_|\___||___/ 17 | 18 | class bcolors: 19 | HEADER = '\033[95m' 20 | OKBLUE = '\033[94m' 21 | OKGREEN = '\033[92m' 22 | WARNING = '\033[93m' 23 | FAIL = '\033[91m' 24 | ENDC = '\033[0m' 25 | BOLD = '\033[1m' 26 | UNDERLINE = '\033[4m' 27 | 28 | def print_status(status, program): 29 | if(status == 0): 30 | print(bcolors.OKGREEN + "⚡"+bcolors.ENDC+bcolors.OKBLUE+" Themed "+program + bcolors.ENDC) 31 | elif(status == 1): 32 | print(bcolors.FAIL + "X"+bcolors.ENDC+bcolors.WARNING+" Failed to theme "+program + bcolors.ENDC) 33 | elif(status == 2): 34 | print(bcolors.FAIL + "X"+bcolors.ENDC+bcolors.WARNING+" User hook "+program + " failed"+bcolors.ENDC) 35 | elif(status == 3): 36 | print(bcolors.OKGREEN + "⚡"+bcolors.ENDC+bcolors.OKBLUE+" User hook "+program + " succeeded"+bcolors.ENDC) 37 | 38 | def is_tool(name): 39 | """Check whether `name` is on PATH and marked as executable.""" 40 | return which(name) is not None 41 | 42 | # ____ __ _ 43 | # / ___|___ _ __ / _(_) __ _ 44 | # | | / _ \| '_ \| |_| |/ _` | 45 | # | |__| (_) | | | | _| | (_| | 46 | # \____\___/|_| |_|_| |_|\__, | 47 | # |___/ 48 | 49 | # get home directory 50 | home = expanduser("~") 51 | 52 | # get config path 53 | config_dir = home + '/.config/chameleon' 54 | config_path = home + '/.config/chameleon/config.yaml' 55 | 56 | # Parse command line arguments 57 | def parse_args(): 58 | parser = argparse.ArgumentParser(description='Chameleon Arguments', usage='%(prog)s -i/t [image/theme] [arguments for wal]') 59 | parser.add_argument('--theme', '-t', metavar='theme', type=str, nargs='?', help='a color scheme name to use as a theme') 60 | parser.add_argument('--image', '-i', metavar='image', type=str, nargs='?', help='an image file to use as a theme') 61 | args = parser.parse_known_args() 62 | return args 63 | 64 | # Parse user config file 65 | def parse_yaml(): 66 | with open(config_path, mode='r') as file: 67 | file_dict = yaml.full_load(file) 68 | file.close() 69 | return file_dict 70 | 71 | # Print keys from a dictionary 72 | def print_keys(dictionary): 73 | for key in dictionary: 74 | print(key) 75 | if isinstance(dictionary[key], dict): 76 | print_keys(dictionary[key]) 77 | 78 | 79 | # _____ _ _ 80 | # |_ _| |__ ___ _ __ ___ (_)_ __ __ _ 81 | # | | | '_ \ / _ \ '_ ` _ \| | '_ \ / _` | 82 | # | | | | | | __/ | | | | | | | | | (_| | 83 | # |_| |_| |_|\___|_| |_| |_|_|_| |_|\__, | 84 | # |___/ 85 | 86 | # Detects and runs hooks set by user 87 | def user_hooks(config): 88 | # if the user has defined hooks 89 | if("hooks" in config): 90 | # iterate through the hooks 91 | for value in config["hooks"].items(): 92 | # If the user has a simple command to run 93 | if(type(value[1]) == str): 94 | # print("single command found") 95 | try: 96 | arglist = value[1].split(' ') 97 | p = subprocess.Popen(arglist) 98 | p.wait() 99 | except: 100 | print_status(2, value[0]) 101 | return 102 | # User has specified options for the hook 103 | elif(type(value[1]) == dict): 104 | path = value[1].get('directory', './') 105 | arglist = value[1].get('command').split(' ') 106 | try: 107 | p = subprocess.Popen(arglist, cwd=path) 108 | p.wait() 109 | except: 110 | print_status(2, value[0]) 111 | return 112 | print_status(3, value[0]) 113 | 114 | def call_wal(args, walargs): 115 | # if we are calling wal on an image 116 | if(args.image): 117 | try: 118 | imagepath = os.path.abspath(args.image) 119 | commandlist = ["wal", "-i", imagepath] 120 | commandlist.extend(walargs) 121 | p = subprocess.Popen(commandlist) 122 | p.wait() 123 | except: 124 | print_status(1, "pywal") 125 | return 126 | # if we are using a prebuilt or custom colorscheme 127 | elif(args.theme): 128 | try: 129 | commandlist = ["wal", "--theme", args.theme] 130 | commandlist.extend(walargs) 131 | p = subprocess.Popen(commandlist) 132 | p.wait() 133 | except: 134 | print_status(1, "pywal") 135 | return 136 | print_status(0, "pywal") 137 | 138 | 139 | 140 | def call_slickpywal(config): 141 | # Check to see if the user defined a custom path 142 | if("slickpywal" in config): 143 | try: 144 | p = subprocess.Popen(["slick-pywal"], cwd=config["slickpywal"]["path"]) 145 | p.wait() 146 | except: 147 | print_status(1, "SlickGreeter Pywal") 148 | return 149 | # Check to see if it exists somewhere in the path 150 | elif(is_tool("slick-pywal")): 151 | try: 152 | p = subprocess.Popen(["slick-pywal"]) 153 | p.wait() 154 | except: 155 | print_status(1, "SlickGreeter Pywal") 156 | return 157 | else: 158 | return 159 | print_status(0, "SlickGreeter Pywal") 160 | return 161 | 162 | def call_pywalneopixels(config): 163 | # Check to see if the user defined a custom path 164 | if("pywalneopixels" in config): 165 | try: 166 | commandstring = config["pywalneopixels"]["path"]+"startLEDs" 167 | os.system(commandstring) 168 | except: 169 | print_status(1, "Pywal NeoPixel") 170 | # Check to see if it exists somewhere in the path 171 | elif(is_tool("startLEDS")): 172 | try: 173 | os.system("startLEDS") 174 | except: 175 | print_status(1, "Pywal NeoPixel") 176 | return 177 | # it is not detected it all 178 | else: 179 | return 180 | print_status(0, "Pywal NeoPixel") 181 | 182 | 183 | def call_wal_discord(config): 184 | # Check to see if the user defined a custom path 185 | if("waldiscord" in config): 186 | try: 187 | m = subprocess.Popen(["wal-discord", "-t"], cwd=config["waldiscord"]["path"]) 188 | m.wait() 189 | except: 190 | print_status(1, "Discord") 191 | return 192 | print_status(0, "Discord") 193 | # Check to see if it exists somewhere in the path 194 | elif(is_tool("wal-discord")): 195 | try: 196 | n = subprocess.Popen(["wal-discord", "-t"]) 197 | n.wait() 198 | except: 199 | print_status(1, "Discord") 200 | return 201 | print_status(0, "Discord") 202 | else: 203 | return 204 | 205 | def call_pywal_discord(config): 206 | # Check to see if the user defined a custom path 207 | if("pywaldiscord" in config): 208 | try: 209 | m = subprocess.Popen(["pywal-discord"], cwd=config["pywaldiscord"]["path"]) 210 | m.wait() 211 | except: 212 | print_status(1, "Discord") 213 | return 214 | print_status(0, "Discord") 215 | # Check to see if it exists somewhere in the path 216 | elif(is_tool("pywal-discord")): 217 | try: 218 | n = subprocess.Popen(["pywal-discord"]) 219 | n.wait() 220 | except: 221 | print_status(1, "Discord") 222 | return 223 | print_status(0, "Discord") 224 | else: 225 | return 226 | 227 | def call_xmenu(config): 228 | # Check to see if the user defined a custom path 229 | if("xmenu" in config): 230 | try: 231 | # make xmenu 232 | null = open("/dev/null") 233 | m = subprocess.Popen(["make"], cwd=config["xmenu"]["path"], stdout=subprocess.DEVNULL) 234 | m.wait() 235 | retval = m.returncode 236 | null.close() 237 | # if making failed 238 | if(retval != 0): 239 | print_status(1, "Xmenu") 240 | return 241 | # Install the new files 242 | i = subprocess.Popen(["sudo", "make", "install"], cwd=config["xmenu"]["path"], stdout=subprocess.DEVNULL) 243 | i.wait() 244 | retval = m.returncode 245 | # if installation failed 246 | if(retval != 0): 247 | print_status(1, "Xmenu") 248 | return 249 | # If we found a config but something went wrong 250 | except: 251 | print_status(1, "Xmenu") 252 | return 253 | print_status(0, "Xmenu") 254 | # no config for xmenu, just return 255 | else: 256 | return 257 | 258 | def call_cordless(config): 259 | if("cordless" in config): 260 | # the full path to the cordless theme template 261 | templatepath = config['cordless']['path'] 262 | try: 263 | with open(home+"/.config/cordless/theme.json", "w") as theme: 264 | commandstring = "go run "+templatepath 265 | commandstring = commandstring.split(' ') 266 | g = subprocess.Popen(commandstring, stdout=theme) 267 | g.wait() 268 | except: 269 | print_status(1, "cordless") 270 | return 271 | print_status(0, "cordless") 272 | 273 | def call_razercli(config): 274 | if("razercli" in config): 275 | try: 276 | p = subprocess.Popen([config['razercli']['path']+"razer-cli", '-a']) 277 | p.wait() 278 | except: 279 | print_status(1, "Razer Devices") 280 | return 281 | elif(is_tool("razer-cli")): 282 | try: 283 | p = subprocess.Popen(["razer-cli", "-a"]) 284 | p.wait() 285 | except: 286 | print_status(1, "Razer Devices") 287 | return 288 | else: 289 | return 290 | print_status(0, "Razer Devices") 291 | 292 | def call_spicetify(config): 293 | if("spicetify" in config): 294 | try: 295 | null = open("/dev/null") 296 | path = config['spicetify']['path'] 297 | p = subprocess.Popen([path+"spicetify", 'update'], stdout=null) 298 | p.wait() 299 | null.close() 300 | except: 301 | print_status(1, "Spicetify") 302 | return 303 | elif(is_tool("spicetify")): 304 | try: 305 | null = open("/dev/null") 306 | p = subprocess.Popen(["spicetify", "apply"], stdout=null) 307 | p.wait() 308 | null.close() 309 | except: 310 | print_status(1, "Spicetify") 311 | return 312 | else: 313 | return 314 | print_status(0, "Spicetify") 315 | 316 | def call_tellegrampallettegen(config): 317 | if("telegrampalletegen" in config): 318 | print("telegram was found in config") 319 | try: 320 | path = config['telegrampalletegen']['path'] 321 | p = subprocess.Popen([path+"telegram-pallete-gen", '--wal']) 322 | p.wait() 323 | except: 324 | print_status(1, "Telegram Pallete") 325 | return 326 | elif(is_tool("telegram-palette-gen")): 327 | print("telegram was found to be tool") 328 | try: 329 | p = subprocess.Popen(["telegram-pallete-gen", "--wal"]) 330 | p.wait() 331 | except: 332 | print_status(1, "Telegram Pallete") 333 | return 334 | else: 335 | return 336 | print_status(0, "Telegram Pallete") 337 | 338 | def call_oomoxicons(config): 339 | if("oomoxicons" in config): 340 | try: 341 | command = config['oomoxicons']['command'] 342 | themepath = config['oomoxicons']['themepath'] 343 | fullcommand = command+" "+themepath+" > /dev/null" 344 | os.system(fullcommand) 345 | except: 346 | print_status(1, "Oomox Icons") 347 | return 348 | print_status(0, "Oomox Icons") 349 | 350 | def call_oomoxgtk(config): 351 | if("oomoxgtk" in config): 352 | try: 353 | themepath = config['oomoxgtk']['themepath'] 354 | fullcommand = "oomox-cli"+" "+themepath+" > /dev/null" 355 | os.system(fullcommand) 356 | except: 357 | print_status(1, "Oomox GTK") 358 | return 359 | print_status(0, "Oomox GTK") 360 | 361 | # Spicetify is preferred 362 | def call_oomoxspotify(config): 363 | if("oomoxspotify" in config): 364 | if(config['oomoxspotify']['enabled'] == "True"): 365 | try: 366 | spotifypath = config['oomoxspotify']['spotifypath'] 367 | fullcommand = "oomoxify-cli"+" "+home+"/.cache/wal/colors-oomox"+" -s "+spotifypath 368 | os.system(fullcommand) 369 | except: 370 | print_status(1, "Oomox Spotify") 371 | return 372 | print_status(0, "Oomox Spofify") 373 | else: 374 | return 375 | else: 376 | return 377 | 378 | def call_pywalfox(config): 379 | if("pywalfox" in config): 380 | try: 381 | path = config['pywalfox']['path'] 382 | p = subprocess.Popen([path+"pywalfox", 'update']) 383 | p.wait() 384 | except: 385 | print_status(1, "Pywalfox") 386 | return 387 | elif(is_tool("pywalfox")): 388 | try: 389 | p = subprocess.Popen(["pywalfox", "update"]) 390 | p.wait() 391 | except: 392 | print_status(1, "Pywalfox") 393 | return 394 | print_status(0, "Pywalfox") 395 | 396 | def call_gnuplot_pywal(config): 397 | if("gnuplotpywal" in config): 398 | try: 399 | path = config['gnuplotpywal']['path'] 400 | file = open(home+"/.gnuplot", "w+") 401 | p = subprocess.Popen([path+"gengnuplotconfig"], stdout=file) 402 | p.wait() 403 | file.close() 404 | except: 405 | print_status(1, "Gnuplot") 406 | return 407 | elif(is_tool("gengnuplotconfig")): 408 | try: 409 | file = open(home+"/.gnuplot", "w+") 410 | p = subprocess.Popen(["gengnuplotconfig"], stdout=file) 411 | p.wait() 412 | file.close() 413 | except: 414 | print_status(1, "Gnuplot") 415 | return 416 | print_status(0, "Gnuplot") 417 | 418 | def call_starttree(config): 419 | if("starttree" in config): 420 | try: 421 | path = config['starttree']['path'] 422 | p = subprocess.Popen([path+"generate.py"]) 423 | p.wait() 424 | except: 425 | print_status(1, "StartTree") 426 | return 427 | elif(is_tool("starttree.py")): 428 | try: 429 | p = subprocess.Popen(["starttree.py"], stdout=subprocess.DEVNULL) 430 | p.wait() 431 | except: 432 | print_status(1, "StartTree") 433 | return 434 | else: 435 | return 436 | print_status(0, "StartTree") 437 | 438 | def theme(config, args, walargs): 439 | call_wal(args, walargs) 440 | call_slickpywal(config) 441 | call_pywalneopixels(config) 442 | call_wal_discord(config) 443 | call_xmenu(config) 444 | call_cordless(config) 445 | call_razercli(config) 446 | call_spicetify(config) 447 | call_tellegrampallettegen(config) 448 | call_oomoxicons(config) 449 | call_oomoxgtk(config) 450 | call_oomoxspotify(config) 451 | call_pywalfox(config) 452 | call_gnuplot_pywal(config) 453 | call_starttree(config) 454 | user_hooks(config) 455 | 456 | def main(): 457 | config = parse_yaml() 458 | args, walargs = parse_args() 459 | theme(config, args, walargs) 460 | 461 | if __name__ == '__main__': 462 | main() 463 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | cordless: 2 | # This is a GO program that generates a cordless colorscheme. 3 | # The color values have been turned into pywal variables. 4 | # This program is run, and the produced 5 | # theme.json is put into $HOME/.config/cordless/ 6 | path: /home/gideon/.cache/wal/cordless.go 7 | 8 | oomoxicons: 9 | # This changes depending on your iconset 10 | command: "oomox-archdroid-icons-cli" 11 | # Path to the desired theme you want to apply 12 | themepath: "/opt/oomox/scripted_colors/xresources/xresources-reverse" 13 | 14 | oomoxgtk: 15 | # Path to the desired theme you want to apply 16 | themepath: "/opt/oomox/scripted_colors/xresources/xresources-reverse" 17 | 18 | # oomoxspotify: 19 | # spotifypath: "/opt/spotify/Apps" 20 | # enabled: "False" 21 | 22 | # slickpywal: 23 | # path: 24 | 25 | # pywalneopixels: 26 | # path: 27 | 28 | # waldiscord: 29 | # path: 30 | 31 | # razer-cli: 32 | # path: 33 | 34 | # telegrampalletegen: 35 | # path: 36 | 37 | # spicetify: 38 | # path: 39 | 40 | # pywalfox: 41 | # path: 42 | 43 | xmenu: 44 | path: "/home/gideon/Programs/xmenu" 45 | 46 | starttree: 47 | path: "/home/Projects/StartTree/" 48 | 49 | # Custom commands to be run when chameleon is run. 50 | # Use at your own risk 51 | hooks: 52 | echosuccess: "echo 'success, theme applied'" 53 | startTree: 54 | command: "docker-compose -f docker-compose.yaml restart StartTree" 55 | directory: "/home/gideon/Projects/StartTree/docker/" 56 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GideonWolfe/Chameleon/125687cc842758329d95940f1cd7557a006d1d79/demo.gif --------------------------------------------------------------------------------