├── .ignore ├── .gitignore ├── test-configs ├── imports │ └── test-theme └── config.yml ├── Cargo.toml ├── CHANGELOG.md ├── install.sh ├── README.md ├── examples ├── i3.md └── i3-wallpaper.md ├── assets └── config.yml ├── src ├── config.rs ├── utils.rs ├── updates.rs ├── main.rs └── block.rs └── Cargo.lock /.ignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /config.yml 3 | -------------------------------------------------------------------------------- /test-configs/imports/test-theme: -------------------------------------------------------------------------------- 1 | # This is imported file for theme 2 | 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "themer" 3 | description = "Update theme variables in your config files with single command" 4 | version = "1.3.1" 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = { version = "3.2.12", features = ["derive"] } 11 | log = "0.4.17" 12 | simplelog = "0.12.0" 13 | colored = "2.0.0" 14 | serde = { version = "1.0.139", features = ["derive"] } 15 | serde_yaml = "0.8.26" 16 | regex = "1.6.0" 17 | lazy_static = "1.4.0" 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.1 2 | - [X] Optimized `set` subcommand speed: writing to file only once when using multiple blocks mode 3 | 4 | # 1.3.0 5 | - [X] Multiple themer blocks inside single file 6 | - [X] `comment_end` variable to support comments like `/* comment */` 7 | 8 | # 1.2.1 9 | - [X] Do not exit after an error when applying changes 10 | 11 | # 1.2.0 12 | - [X] Cover everything with tests 13 | - [X] Getting custom block vars 14 | - [X] Var formatting 15 | - [X] Add Documentation 16 | - [X] `reload` variable to run any command after configs update 17 | - [X] Aliasing 18 | - [X] `only` as the opposite to `ignore` 19 | - [X] Imports 20 | - [X] Expand variables inside import statement for dynamic files 21 | - [X] Split `engine.rs` into `mod` to avoid large file with all codebase 22 | -------------------------------------------------------------------------------- /test-configs/config.yml: -------------------------------------------------------------------------------- 1 | themes: 2 | theme: 3 | background: "#000000" 4 | foreground: "#ffffff" 5 | 6 | # Paths are set to / only because Themer does not really write anything to files 7 | # in thest environment 8 | files: 9 | basic: 10 | path: "/" 11 | comment: ";" 12 | format: "set my_ as \"\"" 13 | 14 | custom: 15 | path: "/" 16 | comment: "#" 17 | format: "set as " 18 | custom: | 19 | # This is just a comment 20 | # This is colors for my theme : 21 | 22 | set foreground as 23 | 24 | imports: 25 | path: "/" 26 | comment: "#" 27 | custom: ">" 28 | 29 | ignore: 30 | path: "/" 31 | ignore: ["foreground"] 32 | 33 | only: 34 | path: "/" 35 | only: ["foreground"] 36 | 37 | aliases: 38 | path: "/" 39 | aliases: 40 | bg: background 41 | fg: foreground 42 | 43 | tags: 44 | path: "/" 45 | comment: "//" 46 | blocks: 47 | one: 48 | custom: "content inside first block" 49 | two: 50 | format: "$ " 51 | ignore: ["foreground"] 52 | custom: | 53 | theme = 54 | 55 | closing: 56 | path: "/" 57 | comment: "/*" 58 | closing_comment: "*/" -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | BLUE='\033[0;34m' 2 | GREEN='\033[0;32m' 3 | PURPLE='\033[0;35m' 4 | NC='\033[0m' # No Color 5 | 6 | TAG="v1.3.1" 7 | 8 | function download_config() { 9 | mkdir -p ~/.config/themer 10 | 11 | CONFIG=~/.config/themer/config.yml 12 | if [ ! -f "$CONFIG" ]; then 13 | echo 14 | echo "Themer config is not found in $CONFIG" 15 | while true; do 16 | echo -e -n "Would you like to create default one? $PURPLE[Y/N]$NC " 17 | read yn 18 | case $yn in 19 | [Yy]* ) 20 | echo -e "${BLUE}Downloading default config to $CONFIG... $NC" 21 | wget https://github.com/uwumouse/themer/releases/download/$TAG/config.yml -q --show-progress -O $CONFIG && \ 22 | echo -e "Default config is dowloaded to: ${BLUE}$CONFIG$NC" 23 | break;; 24 | [Nn]* ) break;; 25 | * ) echo -e "Please answer \"${BLUE}Y$NC\" or \"${BLUE}N$NC\".";; 26 | esac 27 | done 28 | fi 29 | } 30 | 31 | function print_success() { 32 | echo 33 | echo -e "${GREEN}Themer successfully installed!" 34 | } 35 | 36 | echo "This script will install Themer into /usr/bin and create a directory inside of ~/.config" 37 | echo -e -n "Press $PURPLE[ENTER]$NC to continue installation: " 38 | read 39 | 40 | echo -e "${BLUE}Downloading Themer binary... $NC" 41 | 42 | sudo wget https://github.com/uwumouse/themer/releases/download/$TAG/themer -q --show-progress -O /usr/bin/themer && \ 43 | sudo chmod +x /usr/bin/themer && \ 44 | download_config && print_success 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/61386270/188315348-5b3979f5-1f18-4ee6-ad3f-0bbca8bdba63.png) 2 | 3 | # About 4 | Themer allows you to update themes for your desktop environment with just one command by swapping blocks of code that define color theme (and more!) variables inside configuration files. 5 | 6 | # Documentation 7 | You can read documentation at [Wiki](https://github.com/uwumouse/themer/wiki) page to get started. 8 | 9 | # Installation 10 | To install latest version of Themer, execute this command inside of your terminal 11 | ```bash 12 | bash <(curl https://github.com/uwumouse/themer/releases/latest/download/install.sh -L -s) 13 | ``` 14 | Script will prompt you before installation will begin. 15 | 16 | Any other version can be installed with similar command by swapping `` with the desired version prefixed by `v` 17 | ```bash 18 | bash <(curl https://github.com/uwumouse/themer/releases/download//install.sh -L -s) 19 | ``` 20 | 21 | # Examples 22 | You can go to [examples](./examples) directory to see some examples to follow. 23 | 24 | ## What Themer can do? 25 | - [X] Automatically set/swap color scheme variables (practically any variables) defined in your configuration file 26 | - [X] Custom format: define your own code to be injected when you swap a theme. 27 | - [X] Ignoring variables you don't need for each file 28 | - [X] Specify command to reload your environment automatically 29 | - [X] Aliasing vars for some custom names 30 | - [X] Import files inside custom block 31 | -------------------------------------------------------------------------------- /examples/i3.md: -------------------------------------------------------------------------------- 1 | # Change colors inside i3 config 2 | Assuming you have read project's Wiki, the most obvious thing to do is to define Themer block inside your i3's config. 3 | Find a place where colors are defined, and wrap it with comments that themer will recognize, and you should get something like this: 4 | ``` 5 | # i3 config goes here... 6 | 7 | # THEMER 8 | set $background #1a1b26 9 | set $background_alt #565f89 10 | set $foreground #c0caf5 11 | set $foreground_alt #f7768e 12 | # THEMER_END 13 | 14 | # i3 config continues here... 15 | ``` 16 | Don't think about these variables inside the block, it's fine to leave just 2 lines of comments: `# THEMER` and `# THEMER_END` 17 | 18 | # Themer config 19 | Now we need to register i3 config inside Themer's config. By default it's located in `~/.config/themer/config.yml` 20 | If you haven't already, define some themes: 21 | ```yaml 22 | themes: 23 | # Just a dummy themes, put any variables you want, not only colors! 24 | dark: 25 | background: #000000 26 | foreground: #ffffff 27 | light: 28 | background: #ffffff 29 | foreground: #000000 30 | ``` 31 | 32 | Then, define an i3 config file: 33 | ```yaml 34 | files: 35 | i3: 36 | # Themer can expand tildes! 37 | path: "~/.config/i3/config" 38 | comment: "#" # you can skip this one, because hash (#) is the default comment for Themer 39 | format: "set $ " 40 | ``` 41 | 42 | # Bonus 43 | You can set reload command to automatically update your i3 environment after you set a theme: 44 | ```yaml 45 | # Put any command that can be executed via shell 46 | reload: "i3 reload" 47 | 48 | themes: 49 | # ... 50 | files: 51 | # ... 52 | ``` 53 | 54 | # Testing 55 | You're good to go with this. You can check how themer recognizes your file by running this command: 56 | ```bash 57 | $ themer files --check 58 | > Listed configuration files: 59 | > 60 | > ok i3 (~/.config/i3/config) 61 | ``` 62 | 63 | # Setting a theme 64 | To set a theme, run this command: 65 | ```bash 66 | $ themer set 67 | ``` 68 | -------------------------------------------------------------------------------- /assets/config.yml: -------------------------------------------------------------------------------- 1 | # You can find more info on how to use this configuration file in Themer's wiki 2 | 3 | # Uncomment to specify shell command that will run after `themer set` 4 | # reload: "i3 restart" 5 | 6 | # place your themes' variables here 7 | themes: 8 | theme_name: 9 | var1: value 10 | var2: value 11 | 12 | # list files you want to be managed by Themer 13 | files: 14 | file_name: 15 | path: "/path/to/file" # required 16 | # List of theme's variables that should be ignored 17 | ignore: [] 18 | # Keep only needed variables (have more priority than `ignore` 19 | only: [] 20 | # Rename variables, if needed 21 | aliases: 22 | # The syntax is: `new_name: old_name` 23 | foo: var1 24 | bar: var2 25 | # This tells Themer which character(s) is considered a single line comment 26 | comment: "#" # default value 27 | closing_comment: "" # May be needed for files like .css, so you have something like /* THEMER */ comments 28 | # You may need to change this since different configs support different ways of assigning variables 29 | format: " = " # default 30 | custom: | 31 | # This block will override default Themer's block 32 | # Also you can place variables in here 33 | # A signle varialbe 34 | theme_name = "" 35 | the_black_color = ""; 36 | # You can import any files you need to embed here. 37 | # Imports are dynamic - specify any variable inside import path! 38 | /themer-embed.txt> 39 | # All variables at once (basically, it places the default Themer block in your custom code) 40 | 41 | mutliple_codeblocks: 42 | path: "/path/to/file" 43 | comment: "\"" 44 | closing_comment: "" 45 | 46 | # Blocks represent a code block with a tag inside your config file 47 | # Instead of: 48 | # # THEMER 49 | # content here.. 50 | # # THEMER_END 51 | # 52 | # Each Themer block comment will be suffixed with a block name like this: 53 | # # THEMER:blockname 54 | # content here.. 55 | # # THEMER_END:blockname 56 | blocks: 57 | # For this block your Themer block should look like this 58 | # (notice using a double qoute as the comment line sequece): 59 | 60 | # " THEMER:vars 61 | # " THEMER_END:vars 62 | vars: 63 | # Here you can set any of the known values except `path`, `comment` & `closing_comment` 64 | # since these values already set for the current file 65 | format: "let = " 66 | 67 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::BTreeMap; 3 | 4 | pub type ThemeVars = BTreeMap; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 7 | pub struct BlockConfig { 8 | pub path: String, 9 | #[serde(default = "default_comment")] 10 | pub comment: String, 11 | pub closing_comment: Option, 12 | 13 | #[serde(skip)] 14 | pub tag: Option, 15 | 16 | #[serde(flatten)] 17 | pub block: BlockOptions, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize, Clone)] 21 | pub struct TaggedConfig { 22 | pub path: String, 23 | #[serde(default = "default_comment")] 24 | pub comment: String, 25 | pub closing_comment: Option, 26 | pub blocks: BTreeMap, 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 30 | pub struct BlockOptions { 31 | #[serde(default)] 32 | pub only: Vec, 33 | #[serde(default)] 34 | pub ignore: Vec, 35 | 36 | pub aliases: Option, 37 | 38 | #[serde(default = "default_format")] 39 | pub format: String, 40 | pub custom: Option, 41 | } 42 | 43 | fn default_comment() -> String { 44 | "#".to_owned() 45 | } 46 | fn default_format() -> String { 47 | " = ".to_owned() 48 | } 49 | 50 | #[derive(Debug, Serialize, Deserialize, Clone)] 51 | #[serde(untagged)] 52 | pub enum FileConfig { 53 | Multi(TaggedConfig), 54 | Single(BlockConfig), 55 | } 56 | 57 | impl FileConfig { 58 | pub fn get_path(&self) -> String { 59 | match self { 60 | FileConfig::Single(v) => v.path.clone(), 61 | FileConfig::Multi(v) => v.path.clone(), 62 | } 63 | } 64 | pub fn flatten(&self) -> Vec { 65 | match self { 66 | FileConfig::Single(v) => vec![v.clone()], 67 | FileConfig::Multi(mutli) => mutli 68 | .clone() 69 | .blocks 70 | .into_iter() 71 | .map(|(tag, block)| BlockConfig { 72 | tag: Some(tag), 73 | path: mutli.path.clone(), 74 | comment: mutli.comment.clone(), 75 | closing_comment: mutli.closing_comment.clone(), 76 | block, 77 | }) 78 | .collect(), 79 | } 80 | } 81 | } 82 | 83 | #[derive(Debug, Serialize, Deserialize)] 84 | pub struct Config { 85 | pub themes: BTreeMap, 86 | pub files: BTreeMap, 87 | pub reload: Option, 88 | } 89 | -------------------------------------------------------------------------------- /examples/i3-wallpaper.md: -------------------------------------------------------------------------------- 1 | # Dynamic wallpaper 2 | This example shows how to update wallpaper every time you change theme. 3 | 4 | > Note: this example is based on `i3.md` example, so take a look there to understand what's happening. 5 | 6 | > Note 2: in this example I show configuration specifically for i3 wallpaper, but you can use this with any config file such as sway or something else. 7 | 8 | # Setup 9 | If you don't have any Themer blocks inside your i3, you can can just wrap code where you set your wallpaper with Themer block and skip to the next section. 10 | If you've set some block that defines other things inside your config, you should tag your config. 11 | > Note: Tags allows themer to manage your config in multiple places, so you can separate logic. 12 | 13 | So if you had some block that defines colors, you should do this: 14 | ```diff 15 | - # THEMER 16 | - # THEMER_END 17 | + # THEMER:colors 18 | + # THEMER_END:colors 19 | ``` 20 | > Note: `colors` is abritrary single-word tag name 21 | 22 | Now let's add another block for changing wallpaper. 23 | ```diff 24 | + # THEMER:wallpaper 25 | + # THEMER_END:wallpaper 26 | ``` 27 | 28 | # Themer config 29 | Now we need to patch your config a little bit (this one is based on config from `i3.md` example): 30 | ```yaml 31 | # ~./config/themer/config.yml 32 | files: 33 | i3: 34 | # Themer can expand tildes! 35 | path: "~/.config/i3/config" 36 | comment: "#" # you can skip this one, because hash (#) is the default comment for Themer 37 | # Here we define blocks inside our file 38 | blocks: 39 | colors: 40 | format: "set $ " 41 | wallpaper: 42 | # Here we define a code that will be inserted inside `THEMER:wallpaper` block 43 | # I use feh to set background, you can use any other tool 44 | # Also note .png part, this is a dynamic varialbe that contains name of the current theme 45 | custom: | 46 | exec_always feh --bg-scale /home/user/wallpapers/.png 47 | ``` 48 | 49 | # Images 50 | To match expression insde `wallpaper.custom`, we should create files inside `/home/user/wallpapers/` that 51 | match names of themes defined inside Themer's `config.yml`. Let's assume you have 2 themes: `dark` and `light`. 52 | 53 | So you need to have these files: 54 | ```bash 55 | $ ls /home/user/wallpapers 56 | > dark.png 57 | > light.png 58 | ``` 59 | 60 | # Checking 61 | You can additionally check wether Themer recognizes blocks inside config file: 62 | ```bash 63 | $ themer files --check 64 | > Listed configuration files: 65 | > 66 | > i3 (~/.config/i3/config) [Multiple blocks]: 67 | > ok colors 68 | > ok wallpaper 69 | ``` 70 | 71 | # Result 72 | Now, when you run `themer set ` you'll also get automatically updated wallpaper that matches your color scheme. 73 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::block::BlockGenerator; 2 | use crate::config::BlockConfig; 3 | use crate::config::Config; 4 | use crate::config::FileConfig; 5 | use crate::config::TaggedConfig; 6 | use crate::updates::UpdatesGenerator; 7 | use colored::Colorize; 8 | use std::collections::BTreeMap; 9 | use std::env; 10 | 11 | pub fn expand_tilde(p: &String) -> String { 12 | let mut new = p.clone(); 13 | 14 | if new.starts_with("~") { 15 | new = new.replacen("~", &env::var("HOME").unwrap_or(String::new()), 1); 16 | } 17 | 18 | new 19 | } 20 | 21 | pub fn list_files(config: Config, check: bool) { 22 | println!("{}", "Listed configuration files:\n".purple()); 23 | 24 | let vars = BTreeMap::new(); 25 | let block_gen = BlockGenerator::new( 26 | String::new(), 27 | &vars, 28 | FileConfig::Single(BlockConfig::default()), 29 | ); 30 | let mut updates = UpdatesGenerator::new(block_gen); 31 | 32 | config 33 | .files 34 | .into_iter() 35 | .for_each(|(name, config)| match config { 36 | FileConfig::Multi(multi) => { 37 | list_mutli(name, multi, &mut updates, check); 38 | } 39 | FileConfig::Single(single) => { 40 | updates.block_generator.config = single; 41 | println!("{}", list_block(name, &mut updates, check)); 42 | } 43 | }); 44 | } 45 | 46 | fn list_mutli(name: String, multi: TaggedConfig, updates: &mut UpdatesGenerator, check: bool) { 47 | println!("{} ({}) [Multiple blocks]: ", name.blue(), multi.path); 48 | 49 | multi.blocks.iter().for_each(|(tag, config)| { 50 | let config = BlockConfig { 51 | tag: Some(tag.to_string()), 52 | path: multi.path.clone(), 53 | comment: multi.comment.clone(), 54 | closing_comment: multi.closing_comment.clone(), 55 | block: config.clone(), 56 | }; 57 | 58 | updates.block_generator.config = config; 59 | let out = list_block(tag.clone(), updates, check); 60 | 61 | println!(" {out}"); 62 | }); 63 | } 64 | 65 | fn list_block(name: String, updates: &mut UpdatesGenerator, check: bool) -> String { 66 | // Do not show path if it's a tagged block 67 | let display_path = if updates.block_generator.config.tag.is_some() { 68 | String::new() 69 | } else { 70 | let mut dp = String::from("("); 71 | dp.push_str(&updates.block_generator.config.path); 72 | dp.push(')'); 73 | 74 | dp 75 | }; 76 | 77 | if check { 78 | let mut err: Option<&'static str> = None; 79 | 80 | if let Ok(c) = updates.read_file(&updates.block_generator.config.path) { 81 | if updates.validate_block(&c).is_err() { 82 | err = Some("No valid block found"); 83 | } 84 | } else { 85 | err = Some("Failed to read file"); 86 | } 87 | 88 | let mut status = "ok".green(); 89 | let mut err_msg = String::new(); 90 | 91 | if let Some(e) = err { 92 | status = "err".red(); 93 | err_msg = format!("[{}]", e.to_string().red()); 94 | } 95 | 96 | return format!("{status} {} {} {err_msg}", name, display_path); 97 | } 98 | format!("- {} {}", name, display_path) 99 | } 100 | -------------------------------------------------------------------------------- /src/updates.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | block::BlockGenerator, 3 | config::{BlockConfig, Config, FileConfig}, 4 | utils::expand_tilde, 5 | }; 6 | use colored::Colorize; 7 | use std::{fs, process::exit}; 8 | 9 | pub fn run(theme_name: String, config: &Config) { 10 | let vars = match config.themes.get(&theme_name) { 11 | Some(t) => t.clone(), 12 | None => { 13 | log::error!("Theme `{theme_name}` is not listed in configuration file."); 14 | println!( 15 | " {} Try to list avaliable themes with `themer themes`", 16 | "?".blue() 17 | ); 18 | exit(1); 19 | } 20 | }; 21 | 22 | let block_gen = BlockGenerator::new( 23 | theme_name, 24 | &vars, 25 | FileConfig::Single(BlockConfig::default()), 26 | ); 27 | let mut update_gen = UpdatesGenerator::new(block_gen); 28 | 29 | for (_, conf) in &config.files { 30 | let update = update_gen.generate(&conf); 31 | write_results(update, &conf); 32 | } 33 | } 34 | 35 | fn write_results(results: Result, conf: &FileConfig) { 36 | let path = &conf.get_path(); 37 | match results { 38 | Ok(s) => fs::write(expand_tilde(&path), s.as_bytes()).unwrap(), 39 | Err(e) => match e { 40 | UpdatesError::InvalidBlock(message) => { 41 | log::error!("{message}") 42 | } 43 | UpdatesError::UnableToRead => log::error!("Failed to read file {path}"), 44 | }, 45 | } 46 | } 47 | 48 | #[derive(Debug)] 49 | pub enum UpdatesError { 50 | UnableToRead, 51 | InvalidBlock(String), 52 | } 53 | 54 | pub struct UpdatesGenerator { 55 | pub block_generator: BlockGenerator, 56 | } 57 | 58 | impl UpdatesGenerator { 59 | pub fn new(gen: BlockGenerator) -> Self { 60 | Self { 61 | block_generator: gen, 62 | } 63 | } 64 | 65 | pub fn read_file(&self, path: &String) -> Result { 66 | match fs::read_to_string(expand_tilde(&path)) { 67 | Ok(f) => Ok(f), 68 | Err(_) => { 69 | return Err(UpdatesError::UnableToRead); 70 | } 71 | } 72 | } 73 | 74 | pub fn validate_block(&self, contents: &String) -> Result<(), UpdatesError> { 75 | if !self.block_generator.get_re().is_match(contents) { 76 | let msg = format!( 77 | "No Themer block with tag '{}'", 78 | self.block_generator 79 | .config 80 | .tag 81 | .clone() 82 | .unwrap_or(String::from("No Tag")) 83 | ); 84 | return Err(UpdatesError::InvalidBlock(msg)); 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | pub fn generate(&mut self, config: &FileConfig) -> Result { 91 | let mut contents = self.read_file(&config.get_path())?; 92 | 93 | for block in config.flatten() { 94 | contents = self.update_block(&mut contents, &block)?; 95 | } 96 | 97 | Ok(contents) 98 | } 99 | 100 | fn update_block( 101 | &mut self, 102 | contents: &mut String, 103 | config: &BlockConfig, 104 | ) -> Result { 105 | self.block_generator.config = config.clone(); 106 | self.validate_block(&contents)?; 107 | 108 | let mut update = self.block_generator.generate(); 109 | // Replacing dollar sign to avoid Regex issues 110 | update = self.block_generator.wrap(&update).replace("$", "$$"); 111 | 112 | Ok(self 113 | .block_generator 114 | .get_re() 115 | .replacen(&contents, 1, update) 116 | .to_string()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod block; 2 | mod config; 3 | mod updates; 4 | mod utils; 5 | 6 | use clap::{Parser, Subcommand}; 7 | use colored::Colorize; 8 | use config::Config; 9 | use log; 10 | use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; 11 | use std::process::Command; 12 | use std::{fs, process::exit}; 13 | use utils::expand_tilde; 14 | 15 | #[derive(Parser, Debug)] 16 | #[clap(author, version, about, long_about = None)] 17 | struct Args { 18 | /// Path to the config file 19 | #[clap( 20 | global = true, 21 | short, 22 | long, 23 | default_value = "~/.config/themer/config.yml" 24 | )] 25 | config: String, 26 | 27 | #[clap(subcommand)] 28 | command: Option, 29 | } 30 | 31 | #[derive(Subcommand, Debug)] 32 | enum Commands { 33 | /// List avaliable themes in config file 34 | Themes, 35 | /// List avaliable files in config file 36 | Files { 37 | /// Check if config files are valid to be process by Themer 38 | #[clap(parse(from_flag), long)] 39 | check: bool, 40 | }, 41 | /// Set new theme for all of your configuration files 42 | Set { 43 | /// Theme name to set 44 | #[clap(required = true, value_parser)] 45 | theme: String, 46 | }, 47 | } 48 | 49 | fn setup_logger() { 50 | #[cfg(debug_assertions)] 51 | let level = LevelFilter::Debug; 52 | #[cfg(not(debug_assertions))] 53 | let level = LevelFilter::Info; 54 | 55 | let log_conf = ConfigBuilder::new() 56 | .set_time_level(LevelFilter::Off) 57 | .build(); 58 | TermLogger::init(level, log_conf, TerminalMode::Mixed, ColorChoice::Auto).unwrap(); 59 | } 60 | 61 | fn main() { 62 | setup_logger(); 63 | 64 | let args = Args::parse(); 65 | 66 | let config = match fs::read_to_string(expand_tilde(&args.config)) { 67 | Ok(c) => c, 68 | Err(_) => { 69 | log::error!( 70 | "Failed to read Themer configuration file in '{}'", 71 | args.config 72 | ); 73 | exit(1); 74 | } 75 | }; 76 | 77 | let config: Config = match serde_yaml::from_str(&config) { 78 | Ok(c) => c, 79 | Err(e) => { 80 | log::error!("Failed to parse configuration file:\n\t{e}"); 81 | exit(1) 82 | } 83 | }; 84 | log::debug!("{config:#?}"); 85 | 86 | let command = args.command.unwrap_or(Commands::Themes); 87 | 88 | match command { 89 | Commands::Themes => { 90 | println!("{}", "Avaliable themes:".purple()); 91 | config 92 | .themes 93 | .into_iter() 94 | .for_each(|x| println!(" - {}", x.0)); 95 | } 96 | Commands::Files { check } => { 97 | utils::list_files(config, check); 98 | } 99 | Commands::Set { theme } => { 100 | updates::run(theme, &config); 101 | if let Some(reload_cmd) = config.reload { 102 | println!("{}", "Running reload command...".blue()); 103 | 104 | let mut cmd = Command::new("sh"); 105 | match cmd.args(["-c", &reload_cmd]).output() { 106 | Ok(output) => { 107 | if output.status.success() { 108 | println!("{}", "Environment succsessfully reloaded!".green()); 109 | } else { 110 | log::error!("Unsuccessfull outcome of reload command:"); 111 | println!("\t{}", output.status); 112 | } 113 | } 114 | Err(_) => log::error!("Failed to run reload command"), 115 | } 116 | } else { 117 | println!( 118 | "{}\n {} To see updates, you may need to reload your environment.", 119 | "Theme succsessfully updated".green(), 120 | "?".blue() 121 | ); 122 | } 123 | } 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 19 | dependencies = [ 20 | "hermit-abi", 21 | "libc", 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "autocfg" 27 | version = "1.1.0" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 30 | 31 | [[package]] 32 | name = "bitflags" 33 | version = "1.3.2" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 36 | 37 | [[package]] 38 | name = "cfg-if" 39 | version = "1.0.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 42 | 43 | [[package]] 44 | name = "clap" 45 | version = "3.2.12" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d" 48 | dependencies = [ 49 | "atty", 50 | "bitflags", 51 | "clap_derive", 52 | "clap_lex", 53 | "indexmap", 54 | "once_cell", 55 | "strsim", 56 | "termcolor", 57 | "textwrap", 58 | ] 59 | 60 | [[package]] 61 | name = "clap_derive" 62 | version = "3.2.7" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" 65 | dependencies = [ 66 | "heck", 67 | "proc-macro-error", 68 | "proc-macro2", 69 | "quote", 70 | "syn", 71 | ] 72 | 73 | [[package]] 74 | name = "clap_lex" 75 | version = "0.2.4" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 78 | dependencies = [ 79 | "os_str_bytes", 80 | ] 81 | 82 | [[package]] 83 | name = "colored" 84 | version = "2.0.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 87 | dependencies = [ 88 | "atty", 89 | "lazy_static", 90 | "winapi", 91 | ] 92 | 93 | [[package]] 94 | name = "hashbrown" 95 | version = "0.12.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 98 | 99 | [[package]] 100 | name = "heck" 101 | version = "0.4.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 104 | 105 | [[package]] 106 | name = "hermit-abi" 107 | version = "0.1.19" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 110 | dependencies = [ 111 | "libc", 112 | ] 113 | 114 | [[package]] 115 | name = "indexmap" 116 | version = "1.9.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 119 | dependencies = [ 120 | "autocfg", 121 | "hashbrown", 122 | ] 123 | 124 | [[package]] 125 | name = "itoa" 126 | version = "1.0.2" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 129 | 130 | [[package]] 131 | name = "lazy_static" 132 | version = "1.4.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 135 | 136 | [[package]] 137 | name = "libc" 138 | version = "0.2.126" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 141 | 142 | [[package]] 143 | name = "linked-hash-map" 144 | version = "0.5.6" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 147 | 148 | [[package]] 149 | name = "log" 150 | version = "0.4.17" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 153 | dependencies = [ 154 | "cfg-if", 155 | ] 156 | 157 | [[package]] 158 | name = "memchr" 159 | version = "2.5.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 162 | 163 | [[package]] 164 | name = "num_threads" 165 | version = "0.1.6" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 168 | dependencies = [ 169 | "libc", 170 | ] 171 | 172 | [[package]] 173 | name = "once_cell" 174 | version = "1.13.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 177 | 178 | [[package]] 179 | name = "os_str_bytes" 180 | version = "6.2.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" 183 | 184 | [[package]] 185 | name = "proc-macro-error" 186 | version = "1.0.4" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 189 | dependencies = [ 190 | "proc-macro-error-attr", 191 | "proc-macro2", 192 | "quote", 193 | "syn", 194 | "version_check", 195 | ] 196 | 197 | [[package]] 198 | name = "proc-macro-error-attr" 199 | version = "1.0.4" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 202 | dependencies = [ 203 | "proc-macro2", 204 | "quote", 205 | "version_check", 206 | ] 207 | 208 | [[package]] 209 | name = "proc-macro2" 210 | version = "1.0.40" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 213 | dependencies = [ 214 | "unicode-ident", 215 | ] 216 | 217 | [[package]] 218 | name = "quote" 219 | version = "1.0.20" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 222 | dependencies = [ 223 | "proc-macro2", 224 | ] 225 | 226 | [[package]] 227 | name = "regex" 228 | version = "1.6.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 231 | dependencies = [ 232 | "aho-corasick", 233 | "memchr", 234 | "regex-syntax", 235 | ] 236 | 237 | [[package]] 238 | name = "regex-syntax" 239 | version = "0.6.27" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 242 | 243 | [[package]] 244 | name = "ryu" 245 | version = "1.0.10" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 248 | 249 | [[package]] 250 | name = "serde" 251 | version = "1.0.139" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" 254 | dependencies = [ 255 | "serde_derive", 256 | ] 257 | 258 | [[package]] 259 | name = "serde_derive" 260 | version = "1.0.139" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" 263 | dependencies = [ 264 | "proc-macro2", 265 | "quote", 266 | "syn", 267 | ] 268 | 269 | [[package]] 270 | name = "serde_yaml" 271 | version = "0.8.26" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" 274 | dependencies = [ 275 | "indexmap", 276 | "ryu", 277 | "serde", 278 | "yaml-rust", 279 | ] 280 | 281 | [[package]] 282 | name = "simplelog" 283 | version = "0.12.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "48dfff04aade74dd495b007c831cd6f4e0cee19c344dd9dc0884c0289b70a786" 286 | dependencies = [ 287 | "log", 288 | "termcolor", 289 | "time", 290 | ] 291 | 292 | [[package]] 293 | name = "strsim" 294 | version = "0.10.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 297 | 298 | [[package]] 299 | name = "syn" 300 | version = "1.0.98" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 303 | dependencies = [ 304 | "proc-macro2", 305 | "quote", 306 | "unicode-ident", 307 | ] 308 | 309 | [[package]] 310 | name = "termcolor" 311 | version = "1.1.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 314 | dependencies = [ 315 | "winapi-util", 316 | ] 317 | 318 | [[package]] 319 | name = "textwrap" 320 | version = "0.15.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 323 | 324 | [[package]] 325 | name = "themer" 326 | version = "1.3.1" 327 | dependencies = [ 328 | "clap", 329 | "colored", 330 | "lazy_static", 331 | "log", 332 | "regex", 333 | "serde", 334 | "serde_yaml", 335 | "simplelog", 336 | ] 337 | 338 | [[package]] 339 | name = "time" 340 | version = "0.3.11" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" 343 | dependencies = [ 344 | "itoa", 345 | "libc", 346 | "num_threads", 347 | "time-macros", 348 | ] 349 | 350 | [[package]] 351 | name = "time-macros" 352 | version = "0.2.4" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" 355 | 356 | [[package]] 357 | name = "unicode-ident" 358 | version = "1.0.2" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" 361 | 362 | [[package]] 363 | name = "version_check" 364 | version = "0.9.4" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 367 | 368 | [[package]] 369 | name = "winapi" 370 | version = "0.3.9" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 373 | dependencies = [ 374 | "winapi-i686-pc-windows-gnu", 375 | "winapi-x86_64-pc-windows-gnu", 376 | ] 377 | 378 | [[package]] 379 | name = "winapi-i686-pc-windows-gnu" 380 | version = "0.4.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 383 | 384 | [[package]] 385 | name = "winapi-util" 386 | version = "0.1.5" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 389 | dependencies = [ 390 | "winapi", 391 | ] 392 | 393 | [[package]] 394 | name = "winapi-x86_64-pc-windows-gnu" 395 | version = "0.4.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 398 | 399 | [[package]] 400 | name = "yaml-rust" 401 | version = "0.4.5" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 404 | dependencies = [ 405 | "linked-hash-map", 406 | ] 407 | -------------------------------------------------------------------------------- /src/block.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{BlockConfig, FileConfig, ThemeVars}, 3 | utils::expand_tilde, 4 | }; 5 | use colored::Colorize; 6 | use lazy_static::lazy_static; 7 | use regex::{Regex, RegexBuilder}; 8 | use std::{ 9 | collections::{hash_set::IntoIter, HashSet}, 10 | fs, 11 | path::PathBuf, 12 | process::exit, 13 | }; 14 | 15 | pub struct BlockGenerator { 16 | vars: ThemeVars, 17 | theme_name: String, 18 | pub config: BlockConfig, 19 | } 20 | 21 | impl BlockGenerator { 22 | pub fn new(theme_name: String, vars: &ThemeVars, config: FileConfig) -> Self { 23 | match config { 24 | FileConfig::Single(c) => Self { 25 | theme_name, 26 | vars: Self::apply_aliases(vars, &c.block.aliases), 27 | config: c, 28 | }, 29 | FileConfig::Multi(_) => { 30 | log::error!("Tried to create block generator from MultiBlock file config"); 31 | exit(1); 32 | } 33 | } 34 | } 35 | 36 | fn apply_aliases(vars: &ThemeVars, aliases: &Option) -> ThemeVars { 37 | let mut theme = vars.clone(); 38 | 39 | if let Some(aliases) = aliases { 40 | for (new_key, old_key) in aliases { 41 | if theme.contains_key(old_key) { 42 | // Remove old key and add new one to ThemeVars 43 | let val = theme.get(old_key).unwrap().clone(); 44 | theme.remove(old_key); 45 | theme.insert(new_key.to_owned(), val.to_owned()); 46 | } else { 47 | log::warn!("Failed to alias {new_key}: {old_key} does not exist"); 48 | } 49 | } 50 | } 51 | theme 52 | } 53 | 54 | pub fn get_re(&self) -> Regex { 55 | let (start, end) = self.get_tags(); 56 | 57 | let regex_str = format!( 58 | "{0} {start}[ \t]*{1}\n.*{0} {end}[ \t]*{1}", 59 | regex::escape(&self.config.comment), 60 | regex::escape(&self.config.closing_comment.clone().unwrap_or_default()) 61 | ); 62 | log::debug!("Generated regex: {}", regex_str); 63 | 64 | RegexBuilder::new(®ex_str) 65 | .dot_matches_new_line(true) 66 | .build() 67 | .unwrap() 68 | } 69 | 70 | pub fn generate(&self) -> String { 71 | match &self.config.block.custom { 72 | Some(custom) => self.custom_block(custom.to_owned(), 0), 73 | None => self.default_block(), 74 | } 75 | } 76 | 77 | pub fn get_tags(&self) -> (String, String) { 78 | let mut block_name = String::from("THEMER"); 79 | if let Some(tag) = &self.config.tag { 80 | block_name.push(':'); 81 | block_name.push_str(tag); 82 | } 83 | let mut end_name = block_name.clone(); 84 | // 5 is the length of the word "THEMeR", after which we should put "_END" so it becomes 85 | // "THEMER_END" 86 | end_name.insert_str(6, "_END"); 87 | 88 | (block_name, end_name) 89 | } 90 | /// Wraps contents with appropriate comments that will identify Themer block 91 | pub fn wrap(&self, contents: &String) -> String { 92 | let (start, end) = self.get_tags(); 93 | 94 | let mut closing = self.config.closing_comment.clone().unwrap_or_default(); 95 | 96 | // Separate block tag and closing comment with space 97 | if !closing.is_empty() { 98 | closing.insert(0, ' '); 99 | } 100 | 101 | format!( 102 | "{0} {start}{1}\n{contents}\n{0} {end}{1}", 103 | &self.config.comment, closing 104 | ) 105 | } 106 | 107 | fn default_block(&self) -> String { 108 | let mut block = String::new(); 109 | 110 | let mut filter_closure: Option bool>> = None; 111 | 112 | // `only` has more "power" than `ignore`, so here we decide how to filter variables 113 | if !self.config.block.only.is_empty() { 114 | filter_closure = Some(Box::new(|x| self.config.block.only.contains(&x.0))); 115 | } else if !self.config.block.ignore.is_empty() { 116 | filter_closure = Some(Box::new(|x| !self.config.block.ignore.contains(&x.0))); 117 | } 118 | 119 | // Filters variables if needed, otherwise leaving everything as it was 120 | let vars = self 121 | .vars 122 | .clone() 123 | .into_iter() 124 | .filter(filter_closure.unwrap_or(Box::new(|_| true))); 125 | 126 | for (key, val) in vars { 127 | block.push_str( 128 | &self 129 | .config 130 | .block 131 | .format 132 | .clone() 133 | .replace("", &key) 134 | .replace("", &val), 135 | ); 136 | block.push('\n'); 137 | } 138 | 139 | block.trim_end().to_owned() 140 | } 141 | 142 | pub fn custom_block(&self, mut input: String, depth: u8) -> String { 143 | input = self.expand_vars(input); 144 | input = self.resolve_imports(input, depth); 145 | 146 | return input.trim_end().to_owned(); 147 | } 148 | 149 | /// Turns one-word variables into actual values 150 | fn expand_vars(&self, mut input: String) -> String { 151 | for var in Self::extract_vars(&input) { 152 | match var.as_str() { 153 | "" => input = input.replace("", &self.default_block()), 154 | "" => input = input.replace("", &self.theme_name), 155 | var => { 156 | let var_name = var.replace("<", "").replace(">", ""); 157 | 158 | if let Some(v) = self.vars.get(&var_name) { 159 | input = input.replace(var, v); 160 | } else { 161 | log::warn!( 162 | "Custom block for file `{}`: variable {var} cannot be found.", 163 | self.config.path 164 | ); 165 | } 166 | } 167 | }; 168 | } 169 | 170 | input 171 | } 172 | 173 | fn resolve_imports(&self, mut input: String, import_depth: u8) -> String { 174 | for import in Self::extract_imports(&input) { 175 | if import_depth == 1 { 176 | log::error!("Maximum import depth exceeded (tried to import <{import}>)"); 177 | println!( 178 | " {} Probably, you've tried to a file from already imported file", 179 | "?".blue() 180 | ); 181 | exit(1); 182 | } 183 | 184 | let path = match import.split(" ").nth(1) { 185 | Some(v) => { 186 | log::debug!("Importing {v:#?}"); 187 | PathBuf::from(expand_tilde(&v.to_string())) 188 | } 189 | None => { 190 | log::error!("`{import}` is not valid."); 191 | log::info!("Import path should consist of import keyword and a path separated by whitespace."); 192 | continue; 193 | } 194 | }; 195 | 196 | let import_contents = match fs::read_to_string(path) { 197 | Ok(v) => v, 198 | Err(e) => { 199 | log::error!("Failed to resolve import `{import}`: {e}"); 200 | exit(1); 201 | } 202 | }; 203 | 204 | let expanded = self.custom_block(import_contents, import_depth + 1); 205 | input = input.replace(&format!("<{import}>"), &expanded); 206 | } 207 | 208 | input 209 | } 210 | 211 | /// A generic function to retrive unique substrings from string with Regex 212 | fn find_with_re(contents: &String, re: &Regex) -> IntoIter { 213 | re.find_iter(contents) 214 | .map(|x| x.as_str().to_string()) 215 | .collect::>() 216 | .into_iter() 217 | } 218 | /// Finds unique variables inside contents 219 | fn extract_vars(contents: &String) -> Vec { 220 | lazy_static! { 221 | // Matches only single word tokens: no variables inside variables 222 | static ref RE: Regex = Regex::new("<\\S+[^<>]>").unwrap(); 223 | } 224 | 225 | Self::find_with_re(contents, &RE).collect() 226 | } 227 | 228 | /// Finds unique imports inside contents 229 | fn extract_imports(contents: &String) -> Vec { 230 | lazy_static! { 231 | // Matches only single word tokens: no variables inside variables 232 | static ref RE: Regex = Regex::new("").unwrap(); 233 | } 234 | 235 | Self::find_with_re(contents, &RE) 236 | .map(|x| x.replace("<", "").replace(">", "")) 237 | .collect() 238 | } 239 | } 240 | 241 | #[cfg(test)] 242 | mod tests { 243 | use super::BlockGenerator; 244 | use crate::config::{Config, FileConfig, ThemeVars}; 245 | use std::fs; 246 | 247 | fn load_config(file: &'static str) -> (ThemeVars, FileConfig) { 248 | let conf: Config = serde_yaml::from_str( 249 | &fs::read_to_string(format!("./test-configs/config.yml")).unwrap(), 250 | ) 251 | .unwrap(); 252 | 253 | ( 254 | conf.themes.get("theme").unwrap().to_owned(), 255 | conf.files.get(file).unwrap().to_owned(), 256 | ) 257 | } 258 | 259 | #[test] 260 | fn valid_themer_block() { 261 | let (theme, conf) = load_config("basic"); 262 | let gen = BlockGenerator::new("theme".to_string(), &theme, conf); 263 | 264 | assert_eq!( 265 | gen.generate(), 266 | "set my_background as \"#000000\"\nset my_foreground as \"#ffffff\"" 267 | ) 268 | } 269 | 270 | #[test] 271 | fn valid_custom_block() { 272 | let (theme, conf) = load_config("custom"); 273 | 274 | let res = BlockGenerator::new("theme".to_string(), &theme, conf).generate(); 275 | 276 | let expected = format!( 277 | r#"# This is just a comment 278 | # This is colors for my theme theme: 279 | set background as #000000 280 | set foreground as #ffffff 281 | set foreground as {}"#, 282 | theme.get("foreground").unwrap() 283 | ); 284 | 285 | assert_eq!(res, expected); 286 | } 287 | 288 | #[test] 289 | fn valid_wrapper() { 290 | let (theme, conf) = load_config("custom"); 291 | 292 | let gen = BlockGenerator::new("theme".to_string(), &theme, conf); 293 | let s = String::from("some string \n on newline"); 294 | let res = gen.wrap(&s); 295 | 296 | assert_eq!(res, format!("# THEMER\n{s}\n# THEMER_END")); 297 | } 298 | 299 | #[test] 300 | fn imports() { 301 | let (theme, conf) = load_config("imports"); 302 | 303 | let res = BlockGenerator::new("theme".to_string(), &theme, conf).generate(); 304 | 305 | assert_eq!( 306 | res, 307 | "# This is imported file for theme theme\nbackground = #000000\nforeground = #ffffff" 308 | ) 309 | } 310 | 311 | #[test] 312 | fn ignore() { 313 | let (theme, conf) = load_config("ignore"); 314 | let res = BlockGenerator::new("theme".to_string(), &theme, conf).generate(); 315 | 316 | assert_eq!(res, "background = #000000"); 317 | } 318 | 319 | #[test] 320 | fn only() { 321 | let (theme, conf) = load_config("only"); 322 | let res = BlockGenerator::new("theme".to_string(), &theme, conf).generate(); 323 | 324 | assert_eq!(res, "foreground = #ffffff"); 325 | } 326 | 327 | #[test] 328 | fn aliases() { 329 | let (theme, conf) = load_config("aliases"); 330 | let res = BlockGenerator::new("theme".to_string(), &theme, conf).generate(); 331 | 332 | assert_eq!(res, "bg = #000000\nfg = #ffffff"); 333 | } 334 | 335 | #[test] 336 | fn closing_comment() { 337 | let (theme, conf) = load_config("closing"); 338 | let blk = BlockGenerator::new("theme".to_string(), &theme, conf); 339 | 340 | assert_eq!( 341 | blk.wrap(&blk.generate()), 342 | r#"/* THEMER */ 343 | background = #000000 344 | foreground = #ffffff 345 | /* THEMER_END */"# 346 | ); 347 | } 348 | 349 | #[test] 350 | fn tags() { 351 | let (theme, conf) = load_config("tags"); 352 | let blocks = conf.flatten(); 353 | println!("{blocks:#?}"); 354 | let one = blocks[0].clone(); 355 | let two = blocks[1].clone(); 356 | 357 | let mut blk = BlockGenerator::new("theme".to_string(), &theme, FileConfig::Single(one)); 358 | let out = blk.wrap(&blk.generate()); 359 | assert_eq!( 360 | r#"// THEMER:one 361 | content inside first block 362 | // THEMER_END:one"#, 363 | out 364 | ); 365 | 366 | blk.config = two; 367 | let out = blk.wrap(&blk.generate()); 368 | assert_eq!( 369 | r#"// THEMER:two 370 | theme = theme 371 | $background #000000 372 | // THEMER_END:two"#, 373 | out 374 | ); 375 | } 376 | } 377 | --------------------------------------------------------------------------------