├── .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 |
--------------------------------------------------------------------------------