├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── PKGBUILD ├── README.md ├── assets └── demo.gif └── src ├── main.rs ├── spicetify.rs └── wal.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.14" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.7" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.4" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "clap" 56 | version = "4.5.9" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" 59 | dependencies = [ 60 | "clap_builder", 61 | ] 62 | 63 | [[package]] 64 | name = "clap_builder" 65 | version = "4.5.9" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" 68 | dependencies = [ 69 | "anstream", 70 | "anstyle", 71 | "clap_lex", 72 | "strsim", 73 | ] 74 | 75 | [[package]] 76 | name = "clap_lex" 77 | version = "0.7.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 80 | 81 | [[package]] 82 | name = "colorchoice" 83 | version = "1.0.1" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 86 | 87 | [[package]] 88 | name = "is_terminal_polyfill" 89 | version = "1.70.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 92 | 93 | [[package]] 94 | name = "pywal-spicetify" 95 | version = "0.1.0" 96 | dependencies = [ 97 | "clap", 98 | ] 99 | 100 | [[package]] 101 | name = "strsim" 102 | version = "0.11.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 105 | 106 | [[package]] 107 | name = "utf8parse" 108 | version = "0.2.2" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 111 | 112 | [[package]] 113 | name = "windows-sys" 114 | version = "0.52.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 117 | dependencies = [ 118 | "windows-targets", 119 | ] 120 | 121 | [[package]] 122 | name = "windows-targets" 123 | version = "0.52.6" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 126 | dependencies = [ 127 | "windows_aarch64_gnullvm", 128 | "windows_aarch64_msvc", 129 | "windows_i686_gnu", 130 | "windows_i686_gnullvm", 131 | "windows_i686_msvc", 132 | "windows_x86_64_gnu", 133 | "windows_x86_64_gnullvm", 134 | "windows_x86_64_msvc", 135 | ] 136 | 137 | [[package]] 138 | name = "windows_aarch64_gnullvm" 139 | version = "0.52.6" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 142 | 143 | [[package]] 144 | name = "windows_aarch64_msvc" 145 | version = "0.52.6" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 148 | 149 | [[package]] 150 | name = "windows_i686_gnu" 151 | version = "0.52.6" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 154 | 155 | [[package]] 156 | name = "windows_i686_gnullvm" 157 | version = "0.52.6" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 160 | 161 | [[package]] 162 | name = "windows_i686_msvc" 163 | version = "0.52.6" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 166 | 167 | [[package]] 168 | name = "windows_x86_64_gnu" 169 | version = "0.52.6" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 172 | 173 | [[package]] 174 | name = "windows_x86_64_gnullvm" 175 | version = "0.52.6" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 178 | 179 | [[package]] 180 | name = "windows_x86_64_msvc" 181 | version = "0.52.6" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 184 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pywal-spicetify" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = "4.5.9" 8 | 9 | [profile.release] 10 | debug = false 11 | opt-level = 3 12 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: jhideki 2 | pkgname='pywal-spicetify' 3 | pkgver=0.1.1 4 | pkgrel=4 5 | pkgdesc="Apply wal colors to spicetify" 6 | arch=('x86_64') 7 | url="https://github.com/jhideki/pywal-spicetify" 8 | license=('MIT') 9 | depends=('python-pywal16' 'spicetify-cli') 10 | makedepends=('cargo' 'git') 11 | source=("${pkgname}::git+https://github.com/jhideki/pywal-spicetify#tag=${pkgver}") 12 | sha256sums=('SKIP') 13 | options=('strip' '!debug') 14 | 15 | build() { 16 | cd "$srcdir/$pkgname" 17 | cargo build --release --bin $pkgname 18 | } 19 | 20 | package() { 21 | cd "$pkgname" 22 | install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" 23 | strip --strip-all "$pkgdir/usr/bin/$pkgname" 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pywal-spicetify 2 | A simple cli tool for applying wal generated colors to spicetify. I built this purely for personal use.

3 | I haven't done any testing but it works on my machine (i use arch btw)...

4 | Built in rust 5 | ## Demo 6 | `pywal-spicetify` is called inside my waypaper config. 7 | ```bash 8 | post_command = wal -i $wallpaper && pywal-spicetify text && killall -SIGUSR2 waybar 9 | ``` 10 | 11 | 12 | https://github.com/user-attachments/assets/a39338c5-d996-465d-bd07-25e409ef2aec 13 | 14 | 15 | ## Dependencies 16 | - `pywal-spicetify` requires a fork of pywal called `pywal-16-colors` Here is the [repo](https://github.com/eylles/pywal16). Support for default `pywal` may be added later. (I created an issue for this but I probably won't ever get to it) 17 | - Obviously you will need `spicetify' installed with at least one theme. 18 | 19 | ## Installation 20 | To install rom the AUR run: 21 | ```bash 22 | yay -S pywal-spicetify 23 | ``` 24 | From Source 25 | ```bash 26 | git clone https://github.com/jhideki/pywal-spicetify 27 | cd pywal-spicetify 28 | cargo build --release 29 | cp target/release/bin/pywal-spicetify 30 | export PATH=$PATH: 31 | ``` 32 | ## Usage 33 | ```bash 34 | Usage: pywal-spicetify [OPTIONS] 35 | 36 | Arguments: 37 | 38 | 39 | Options: 40 | -r, --reset 41 | -h, --help Print help 42 | -V, --version Print version 43 | ``` 44 | E.g. to apply wal colors to the Dribblish theme run:, 45 | ```bash 46 | pywal-spicetify Dribbblish 47 | ``` 48 | `-r` or `--reset` will delete `colors-spicetify.ini` and remove the configuration from `.config/spicetify/Themes//colors.ini` 49 | ## Example Usage 50 | pywal-spicetify is meant to be called whenever you change wallpapers. Here is how it is set up with `waypaper` and the **text** theme
51 | ```bash 52 | [Settings] 53 | language = en 54 | folder = ~/dotfiles/wallpapers 55 | wallpaper = ~/dotfiles/wallpapers/wp5089612-scenery-anime-wallpapers.jpg 56 | post_command = wal -i $wallpaper && pywal-spicetify text && killall -SIGUSR2 waybar 57 | ``` 58 | ## Configuration 59 | Colors can be configured from `.confiig/wal/templates/colors-spicetify.ini`
60 | `colors-spicetify.ini` will be auto-generated upon running `pywal-spicetify` or the first time. 61 | ```bash 62 | accent = {color0.strip} 63 | accent-active = {color2.strip} 64 | accent-inactive = {color3.strip} 65 | banner = {color4.strip} 66 | border-active = {foreground.strip} 67 | border-inactive = {foreground.strip} 68 | header = {foreground.strip} 69 | highlight = {color6.strip} 70 | main = {background.strip} 71 | notification = {color7.strip} 72 | notification-error = {color8.strip} 73 | subtext = {cursor.strip} 74 | text = {cursor.strip} 75 | ``` 76 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhideki/pywal-spicetify/5bd186e260acf34dedd0e5b74b0a5f18b99bd533/assets/demo.gif -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod spicetify; 2 | mod wal; 3 | 4 | use clap::{Arg, Command}; 5 | use std::env; 6 | 7 | use spicetify::Spicetify; 8 | use wal::Wal; 9 | 10 | fn main() { 11 | let matches = Command::new("pywal-spicetify") 12 | .version("0.1") 13 | .about("A simple cli tool for setting spicetify colors from wal") 14 | .arg(Arg::new("theme").required(true)) 15 | .arg( 16 | Arg::new("reset") 17 | .short('r') 18 | .long("reset") 19 | .action(clap::ArgAction::SetTrue), 20 | ) 21 | .arg_required_else_help(true) 22 | .get_matches(); 23 | 24 | let theme: String = matches 25 | .get_one::("theme") 26 | .expect("theme required") 27 | .to_string(); 28 | 29 | println!("You selected the theme: {theme}"); 30 | 31 | //Only linux support 32 | let home = match env::home_dir() { 33 | Some(path) => path, 34 | None => panic!("unable to locate home directory!"), 35 | }; 36 | 37 | let wal = Wal::new(home.clone()); 38 | let spicetify = Spicetify::new(home.clone(), &theme); 39 | 40 | if matches.get_flag("reset") { 41 | println!("Resetting configs..."); 42 | wal.reset(); 43 | spicetify.write_config(None); 44 | spicetify.reload(); 45 | } else { 46 | wal.set_config(); 47 | let wal_config = wal.get_config(); 48 | 49 | spicetify.write_config(Some(wal_config)); 50 | spicetify.reload(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/spicetify.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::fs::{File, OpenOptions}; 3 | use std::io::{BufRead, BufReader, Write}; 4 | use std::path::PathBuf; 5 | use std::process; 6 | pub struct Spicetify { 7 | config_path: PathBuf, 8 | theme: String, 9 | } 10 | impl Spicetify { 11 | pub fn new(home: PathBuf, theme: &str) -> Self { 12 | let mut config_path = home; 13 | config_path.push(format!(".config/spicetify/Themes/{theme}/color.ini")); 14 | if let Err(e) = fs::metadata(&config_path) { 15 | panic!("Error reading file {} {}", config_path.display(), e); 16 | } 17 | Self { 18 | config_path, 19 | theme: String::from(theme), 20 | } 21 | } 22 | 23 | pub fn reload(&self) { 24 | process::Command::new("spicetify") 25 | .arg("config") 26 | .arg("current_theme") 27 | .arg(&self.theme) 28 | .status() 29 | .unwrap(); 30 | process::Command::new("spicetify") 31 | .arg("config") 32 | .arg("color_scheme") 33 | .arg("pywal") 34 | .status() 35 | .unwrap(); 36 | match process::Command::new("spicetify").arg("apply").output() { 37 | Ok(stdout) => { 38 | println!("Running spicetify..."); 39 | if let Ok(output) = String::from_utf8(stdout.stdout) { 40 | println!("{output}"); 41 | } 42 | } 43 | Err(e) => panic!("Error running spicetify apply {}", e), 44 | } 45 | } 46 | 47 | pub fn write_config(&self, wal_config: Option) { 48 | let file = match File::open(&self.config_path) { 49 | Ok(file) => file, 50 | Err(e) => panic!("Invalid path: {e}"), 51 | }; 52 | 53 | let reader = BufReader::new(file); 54 | 55 | let lines: Vec = match reader.lines().collect() { 56 | Ok(lines) => lines, 57 | Err(e) => panic!("Error reading lines! {}", e), 58 | }; 59 | 60 | let mut buf: Vec = Vec::new(); 61 | let mut i = 0; 62 | while i < lines.len() { 63 | //Remove exisitng config 64 | if lines[i].contains("pywal") { 65 | buf.pop(); //pop the last \n 66 | i += 14; 67 | continue; 68 | } 69 | buf.push(lines[i].clone()); 70 | i += 1; 71 | } 72 | let mut writer = match OpenOptions::new() 73 | .write(true) 74 | .truncate(true) 75 | .open(&self.config_path) 76 | { 77 | Ok(w) => w, 78 | Err(e) => panic!("Error opening file! {}", e), 79 | }; 80 | 81 | let mut content = buf.join("\n"); 82 | if let Some(wal_config) = wal_config { 83 | content.push_str("\n\n"); 84 | content.push_str("[pywal]"); 85 | content.push_str("\n"); 86 | content.push_str(&wal_config); 87 | } 88 | 89 | let _ = writer.write_all(content.as_bytes()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/wal.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, OpenOptions}; 2 | use std::io::{BufReader, Read, Write}; 3 | use std::path::PathBuf; 4 | use std::process; 5 | 6 | pub struct Wal { 7 | config_path: PathBuf, 8 | cache_path: PathBuf, 9 | } 10 | impl Wal { 11 | pub fn new(home: PathBuf) -> Self { 12 | let mut config_path = home.clone(); 13 | let mut cache_path = home.clone(); 14 | config_path.push(".config/wal/templates/colors-spicetify.ini"); 15 | cache_path.push(".cache/wal/colors-spicetify.ini"); 16 | Wal { 17 | config_path, 18 | cache_path, 19 | } 20 | } 21 | 22 | pub fn reload(&self) { 23 | let _ = process::Command::new("wal") 24 | .arg("-n") 25 | .output() 26 | .expect("Failed run wal"); 27 | } 28 | 29 | pub fn reset(&self) { 30 | println!("Resetting..."); 31 | let files = [&self.config_path, &self.cache_path]; 32 | let mut existing_files = files.into_iter().filter(|&path| path.exists()).peekable(); 33 | let do_files_exist = existing_files.peek().is_some(); 34 | 35 | if !do_files_exist { 36 | println!( 37 | "Files {} and {} have been removed", 38 | self.config_path.display(), 39 | self.cache_path.display() 40 | ); 41 | return; 42 | } 43 | 44 | for path in existing_files { 45 | println!("Removing file {}", path.display()); 46 | if let Err(error) = std::fs::remove_file(path) { 47 | println!("Failed to remove config: {error}"); 48 | } 49 | } 50 | 51 | self.reload(); 52 | } 53 | 54 | pub fn set_config(&self) { 55 | let path = &self.config_path; 56 | if !path.exists() { 57 | let mut file = match OpenOptions::new().create(true).write(true).open(&path) { 58 | Ok(f) => f, 59 | Err(e) => panic!("Error opening .config/wal {}", e), 60 | }; 61 | println!( 62 | "Generating colors-spicetify.ini file in {}", 63 | &path.display() 64 | ); 65 | let content = r#"accent = {color0.strip} 66 | accent-active = {color2.strip} 67 | accent-inactive = {color3.strip} 68 | banner = {color4.strip} 69 | border-active = {foreground.strip} 70 | border-inactive = {foreground.strip} 71 | header = {foreground.strip} 72 | highlight = {color6.strip} 73 | main = {background.strip} 74 | notification = {color7.strip} 75 | notification-error = {color8.strip} 76 | subtext = {cursor.strip} 77 | text = {cursor.strip}"#; 78 | let _ = file.write_all(content.as_bytes()); 79 | } 80 | self.reload(); 81 | } 82 | 83 | pub fn get_config(&self) -> String { 84 | let path = &self.cache_path; 85 | let file = match File::open(&path) { 86 | Ok(f) => f, 87 | Err(e) => panic!("Error opening colors-spicetify.ini! {}", e), 88 | }; 89 | 90 | println!("wal config path: {}", path.display()); 91 | 92 | let mut reader = BufReader::new(file); 93 | let mut wal_config = String::new(); 94 | let _ = reader.read_to_string(&mut wal_config); 95 | wal_config 96 | } 97 | } 98 | 99 | #[cfg(target_family = "unix")] // to stop this test from running on windows 100 | #[test] 101 | fn removes_both_files_in_any_combination() { 102 | let wal = Wal::new(std::env::home_dir().unwrap()); 103 | let files = [&wal.config_path, &wal.cache_path]; 104 | let paths_and_names = [ 105 | (None, ""), 106 | (Some(&wal.cache_path), "cache"), 107 | (Some(&wal.config_path), "config"), 108 | ]; 109 | 110 | //Ensure that reset deleted files properly 111 | wal.reset(); 112 | for (path, name) in &paths_and_names[1..] { 113 | println!("Testing {} {}", path.unwrap().display(), name); 114 | assert!( 115 | path.is_some_and(|x| !x.exists()), 116 | "Had some trouble deleting config files{}", 117 | if path.is_some() { 118 | format!(" after deleting .{name} file") 119 | } else { 120 | String::new() 121 | } 122 | ); 123 | } 124 | for (path, name) in paths_and_names { 125 | //This will create a cache file as well (needs pywal-16-colors for wal -w option) 126 | wal.set_config(); 127 | 128 | if let Some(path) = path { 129 | std::fs::remove_file(path).unwrap(); 130 | } 131 | wal.reset(); 132 | let mut existing_files = files.into_iter().filter(|&path| path.exists()); 133 | let has_deleted_files = existing_files.next().is_none(); 134 | assert!( 135 | has_deleted_files, 136 | "Had some trouble deleting config files{}", 137 | if path.is_some() { 138 | format!(" after deleting .{name} file") 139 | } else { 140 | String::new() 141 | } 142 | ); 143 | } 144 | } 145 | --------------------------------------------------------------------------------