feat(core): use PathExt to unify env var resolution

This new implementation allows for expanding any environment variable so
it is not limited to just `~`, `$HOME`, `$Env:USERPROFILE` and
`$Env:KOMOREBI_CONFIG_HOME`.

It expands the follwing formats:
- CMD: `%variable%`
- PowerShell: `$Env:variable`
- Bash: `$variable`

I searched throughout the code base for path and migrate any code that
might need to PathExt::replace_env.

It is possible that I might have missed a few places due to my
unfamiliarity with the code base, so if you find any, please let me
know.

Most of the paths that needed this trait, are in:

- Clap arguments, and that was handled by #[value_parse] attribute and a
  helper function.
- SocketMessage and that was handled by custom deserialization with the
  help of serde_with crate
This commit is contained in:
amrbashir
2025-03-25 11:13:48 +02:00
committed by LGUG2Z
parent 5a0196ac9d
commit 9f8e4b9dca
11 changed files with 558 additions and 278 deletions

234
Cargo.lock generated
View File

@@ -59,7 +59,7 @@ dependencies = [
"accesskit_consumer",
"hashbrown 0.15.2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
]
@@ -295,19 +295,21 @@ checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
[[package]]
name = "arboard"
version = "3.4.1"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4"
checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70"
dependencies = [
"clipboard-win",
"core-graphics 0.23.2",
"image 0.25.6",
"log",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation 0.2.2",
"objc2 0.6.0",
"objc2-app-kit 0.3.0",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.0",
"parking_lot",
"windows-sys 0.48.0",
"percent-encoding",
"windows-sys 0.59.0",
"x11rb",
]
@@ -842,9 +844,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.17"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"jobserver",
"libc",
@@ -1231,9 +1233,9 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.4.5"
version = "3.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
dependencies = [
"nix",
"windows-sys 0.59.0",
@@ -1245,6 +1247,41 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.100",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.100",
]
[[package]]
name = "deflate"
version = "0.8.6"
@@ -1257,11 +1294,12 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
@@ -1424,7 +1462,7 @@ dependencies = [
"js-sys",
"log",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"parking_lot",
"percent-encoding",
@@ -1647,9 +1685,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys 0.59.0",
@@ -1701,7 +1739,7 @@ dependencies = [
"bit_field",
"half",
"lebe",
"miniz_oxide 0.8.5",
"miniz_oxide 0.8.7",
"rayon-core",
"smallvec",
"zune-inflate",
@@ -1766,7 +1804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [
"crc32fast",
"miniz_oxide 0.8.5",
"miniz_oxide 0.8.7",
]
[[package]]
@@ -2148,7 +2186,7 @@ dependencies = [
"glutin_wgl_sys",
"libloading",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"once_cell",
"raw-window-handle",
@@ -2249,7 +2287,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http",
"indexmap 2.8.0",
"indexmap 2.9.0",
"slab",
"tokio",
"tokio-util",
@@ -2594,6 +2632,12 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
@@ -2696,16 +2740,18 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
"serde",
]
[[package]]
@@ -2888,6 +2934,7 @@ dependencies = [
"schemars",
"serde",
"serde_json_lenient",
"serde_with",
"serde_yaml 0.9.34+deprecated",
"shadow-rs",
"strum 0.27.1",
@@ -3097,7 +3144,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.0",
"libc",
"redox_syscall 0.5.10",
"redox_syscall 0.5.11",
]
[[package]]
@@ -3315,11 +3362,13 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess2"
version = "2.0.5"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41"
checksum = "f54028747dfea8e8bf00d3c2d4e83cf023c1accfd5d436335456e9864940cb85"
dependencies = [
"mime",
"phf 0.11.3",
"phf_shared 0.11.3",
"unicase",
]
@@ -3359,9 +3408,9 @@ dependencies = [
[[package]]
name = "miniz_oxide"
version = "0.8.5"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430"
dependencies = [
"adler2",
"simd-adler32",
@@ -3411,7 +3460,7 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"hexf-parse",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"rustc-hash",
"spirv",
@@ -3836,6 +3885,18 @@ dependencies = [
"objc2-quartz-core",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2-core-graphics",
"objc2-foundation 0.3.0",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.2.2"
@@ -3872,6 +3933,28 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.2.2"
@@ -3923,6 +4006,18 @@ checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2-core-foundation",
]
[[package]]
@@ -3933,7 +4028,7 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
]
@@ -4198,7 +4293,7 @@ dependencies = [
"cfg-if 1.0.0",
"libc",
"petgraph",
"redox_syscall 0.5.10",
"redox_syscall 0.5.11",
"smallvec",
"thread-id",
"windows-targets 0.52.6",
@@ -4232,7 +4327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap 2.8.0",
"indexmap 2.9.0",
]
[[package]]
@@ -4250,6 +4345,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared 0.11.3",
]
@@ -4293,6 +4389,20 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"proc-macro2",
"quote",
"syn 2.0.100",
"unicase",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@@ -4309,6 +4419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher 1.0.1",
"unicase",
]
[[package]]
@@ -4367,7 +4478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
dependencies = [
"base64",
"indexmap 2.8.0",
"indexmap 2.9.0",
"quick-xml 0.32.0",
"serde",
"time",
@@ -4395,7 +4506,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.5",
"miniz_oxide 0.8.7",
]
[[package]]
@@ -4745,9 +4856,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.10"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
dependencies = [
"bitflags 2.9.0",
]
@@ -5179,6 +5290,37 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.9.0",
"schemars",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "serde_yaml"
version = "0.8.26"
@@ -5197,7 +5339,7 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"itoa",
"ryu",
"serde",
@@ -5335,9 +5477,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "smithay-client-toolkit"
@@ -5806,9 +5948,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.44.1"
version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
@@ -5888,7 +6030,7 @@ version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6505,7 +6647,7 @@ dependencies = [
"bitflags 2.9.0",
"cfg_aliases",
"document-features",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"naga",
"once_cell",
@@ -6521,9 +6663,9 @@ dependencies = [
[[package]]
name = "wgpu-hal"
version = "24.0.2"
version = "24.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4317a17171dc20e6577bf606796794580accae0716a69edbc7388c86a3ec9f23"
checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259"
dependencies = [
"android_system_properties",
"arrayvec",
@@ -7259,7 +7401,7 @@ dependencies = [
"memmap2",
"ndk",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"objc2-ui-kit",
"orbclient",

View File

@@ -49,6 +49,7 @@ use komorebi_client::Colour;
use komorebi_client::KomorebiTheme;
use komorebi_client::MonitorNotification;
use komorebi_client::NotificationEvent;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
@@ -500,13 +501,16 @@ impl Komobar {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);

View File

@@ -16,6 +16,8 @@ use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use image::RgbaImage;
use komorebi_client::replace_env_in_path;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use std::collections::HashMap;
@@ -65,6 +67,7 @@ struct Opts {
fonts: bool,
/// Path to a JSON or YAML configuration file
#[clap(short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>,
/// Write an example komorebi.bar.json to disk
#[clap(long)]
@@ -159,13 +162,15 @@ fn main() -> color_eyre::Result<()> {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
},
);
@@ -174,7 +179,7 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
println!(
"Example komorebi.bar.json file written to {}",
home_dir.as_path().display()
home_dir.display()
);
std::process::exit(0);
@@ -182,16 +187,11 @@ fn main() -> color_eyre::Result<()> {
let default_config_path = home_dir.join("komorebi.bar.json");
let config_path = opts.config.map_or_else(
|| {
if !default_config_path.is_file() {
None
} else {
Some(default_config_path.clone())
}
},
Option::from,
);
let config_path = opts.config.or_else(|| {
default_config_path
.is_file()
.then_some(default_config_path.clone())
});
let mut config = match config_path {
None => {
@@ -201,17 +201,14 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(&default_config_path, komorebi_bar_json)?;
tracing::info!(
"created example configuration file: {}",
default_config_path.as_path().display()
default_config_path.display()
);
KomobarConfig::read(&default_config_path)?
}
Some(ref config) => {
if !opts.aliases {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
tracing::info!("found configuration file: {}", config.display());
}
KomobarConfig::read(config)?
@@ -311,10 +308,7 @@ fn main() -> color_eyre::Result<()> {
hotwatch.watch(config_path, move |event| match event.kind {
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
Ok(updated) => {
tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
tracing::info!("configuration file updated: {}", config_path_cl.display());
if let Err(error) = tx_config.send(updated) {
tracing::error!("could not send configuration update to gui: {error}")

View File

@@ -12,7 +12,7 @@ pub use komorebi::config_generation::MatchingRule;
pub use komorebi::config_generation::MatchingStrategy;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
pub use komorebi::core::replace_env_in_path;
pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement;

View File

@@ -48,6 +48,7 @@ windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.55"
serde_with = { version = "3.12", features = ["schemars_0_8"] }
[build-dependencies]
shadow-rs = { workspace = true }

View File

@@ -1,12 +1,10 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use clap::ValueEnum;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
@@ -28,7 +26,10 @@ pub use default_layout::DefaultLayout;
pub use direction::Direction;
pub use layout::Layout;
pub use operation_direction::OperationDirection;
pub use pathext::replace_env_in_path;
pub use pathext::resolve_option_hashmap_usize_path;
pub use pathext::PathExt;
pub use pathext::ResolvedPathBuf;
pub use rect::Rect;
pub mod animation;
@@ -44,6 +45,8 @@ pub mod operation_direction;
pub mod pathext;
pub mod rect;
// serde_as must be before derive
#[serde_with::serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "type", content = "content")]
@@ -105,7 +108,7 @@ pub enum SocketMessage {
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(DefaultLayout),
CycleLayout(CycleDirection),
ChangeLayoutCustom(PathBuf),
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
FlipLayout(Axis),
ToggleWorkspaceWindowContainerBehaviour,
ToggleWorkspaceFloatOverride,
@@ -123,8 +126,8 @@ pub enum SocketMessage {
RetileWithResizeDimensions,
QuickSave,
QuickLoad,
Save(PathBuf),
Load(PathBuf),
Save(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
Load(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
CycleFocusMonitor(CycleDirection),
CycleFocusWorkspace(CycleDirection),
CycleFocusEmptyWorkspace(CycleDirection),
@@ -147,19 +150,24 @@ pub enum SocketMessage {
WorkspaceName(usize, usize, String),
WorkspaceLayout(usize, usize, DefaultLayout),
NamedWorkspaceLayout(String, DefaultLayout),
WorkspaceLayoutCustom(usize, usize, PathBuf),
NamedWorkspaceLayoutCustom(String, PathBuf),
WorkspaceLayoutCustom(usize, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
NamedWorkspaceLayoutCustom(String, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
WorkspaceLayoutRule(usize, usize, usize, DefaultLayout),
NamedWorkspaceLayoutRule(String, usize, DefaultLayout),
WorkspaceLayoutCustomRule(usize, usize, usize, PathBuf),
NamedWorkspaceLayoutCustomRule(String, usize, PathBuf),
WorkspaceLayoutCustomRule(
usize,
usize,
usize,
#[serde_as(as = "ResolvedPathBuf")] PathBuf,
),
NamedWorkspaceLayoutCustomRule(String, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
ClearWorkspaceLayoutRules(usize, usize),
ClearNamedWorkspaceLayoutRules(String),
ToggleWorkspaceLayer,
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
ReloadStaticConfiguration(PathBuf),
ReplaceConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
ReloadStaticConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
@@ -458,45 +466,28 @@ impl Sizing {
}
}
pub fn resolve_home_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let mut resolved_path = PathBuf::new();
let mut resolved = false;
for c in path.as_ref().components() {
match c {
std::path::Component::Normal(c)
if (c == "~" || c == "$Env:USERPROFILE" || c == "$HOME") && !resolved =>
{
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
#[cfg(test)]
mod tests {
use super::*;
resolved_path.extend(home.components());
resolved = true;
}
#[test]
fn deserializes() {
// Set a variable for testing
std::env::set_var("VAR", "VALUE");
std::path::Component::Normal(c) if (c == "$Env:KOMOREBI_CONFIG_HOME") && !resolved => {
let komorebi_config_home =
PathBuf::from(std::env::var("KOMOREBI_CONFIG_HOME").ok().ok_or_else(|| {
anyhow!("there is no KOMOREBI_CONFIG_HOME environment variable set")
})?);
let json = r#"{"type":"WorkspaceLayoutCustomRule","content":[0,0,0,"/path/%VAR%/d"]}"#;
let message: SocketMessage = serde_json::from_str(json).unwrap();
resolved_path.extend(komorebi_config_home.components());
resolved = true;
}
let SocketMessage::WorkspaceLayoutCustomRule(
_workspace_index,
_workspace_number,
_monitor_index,
path,
) = message
else {
panic!("Expected WorkspaceLayoutCustomRule");
};
_ => resolved_path.push(c),
}
assert_eq!(path, PathBuf::from("/path/VALUE/d"));
}
let parent = resolved_path
.parent()
.ok_or_else(|| anyhow!("cannot parse parent directory"))?;
Ok(if parent.is_dir() {
let file = resolved_path
.components()
.next_back()
.ok_or_else(|| anyhow!("cannot parse filename"))?;
dunce::canonicalize(parent)?.join(file)
} else {
resolved_path
})
}

View File

@@ -1,48 +1,192 @@
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
/// Path extension trait
pub trait PathExt {
/// Resolve environment variables components in a path.
///
/// Resolves the follwing formats:
/// - CMD: `%variable%`
/// - PowerShell: `$Env:variable`
/// - Bash: `$variable`.
fn replace_env(&self) -> PathBuf;
}
impl PathExt for PathBuf {
/// Blanket implementation for all types that can be converted to a `Path`.
impl<P: AsRef<Path>> PathExt for P {
fn replace_env(&self) -> PathBuf {
let mut result = PathBuf::new();
let mut out = PathBuf::new();
for component in self.components() {
match component {
Component::Normal(segment) => {
// Check if it starts with `$` or `$Env:`
if let Some(stripped_segment) = segment.to_string_lossy().strip_prefix('$') {
let var_name = if let Some(env_name) = stripped_segment.strip_prefix("Env:")
{
// Extract the variable name after `$Env:`
env_name
} else if stripped_segment == "HOME" {
// Special case for `$HOME`
"USERPROFILE"
} else {
// Extract the variable name after `$`
stripped_segment
};
if let Ok(value) = env::var(var_name) {
result.push(&value); // Replace with the value
} else {
result.push(segment); // Keep as-is if variable is not found
}
} else {
result.push(segment); // Keep as-is if not an environment variable
for c in self.as_ref().components() {
match c {
Component::Normal(mut c) => {
// Special case for ~ and $HOME, replace with $Env:USERPROFILE
if c == OsStr::new("~") || c.eq_ignore_ascii_case("$HOME") {
c = OsStr::new("$Env:USERPROFILE");
}
let bytes = c.as_encoded_bytes();
// %LOCALAPPDATA%
let var = if bytes[0] == b'%' && bytes[bytes.len() - 1] == b'%' {
Some(&bytes[1..bytes.len() - 1])
} else {
// prefix length is 5 for $Env: and 1 for $
// so we take the minimum of 5 and the length of the bytes
let prefix = &bytes[..5.min(bytes.len())];
let prefix = unsafe { OsStr::from_encoded_bytes_unchecked(prefix) };
// $Env:LOCALAPPDATA
if prefix.eq_ignore_ascii_case("$Env:") {
Some(&bytes[5..])
} else if bytes[0] == b'$' {
// $LOCALAPPDATA
Some(&bytes[1..])
} else {
// not a variable
None
}
};
// if component is a variable, get the value from the environment
if let Some(var) = var {
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
if let Some(value) = env::var_os(var) {
out.push(value);
continue;
}
}
// if not a variable, or a value couldn't be obtained from environemnt
// then push the component as is
out.push(c);
}
_ => {
// Add other components (e.g., root, parent) as-is
result.push(component.as_os_str());
}
// other components are pushed as is
_ => out.push(c),
}
}
result
out
}
}
/// Replace environment variables in a path. This is a wrapper around
/// [`PathExt::replace_env`] to be used in Clap arguments parsing.
pub fn replace_env_in_path(input: &str) -> Result<PathBuf, std::convert::Infallible> {
Ok(input.replace_env())
}
/// A wrapper around [`PathBuf`] that has a custom [Deserialize] implementation
/// that uses [`PathExt::replace_env`] to resolve environment variables
#[derive(Clone, Debug)]
pub struct ResolvedPathBuf(PathBuf);
impl ResolvedPathBuf {
/// Create a new [`ResolvedPathBuf`] from a [`PathBuf`]
pub fn new(path: PathBuf) -> Self {
Self(path.replace_env())
}
}
impl From<ResolvedPathBuf> for PathBuf {
fn from(path: ResolvedPathBuf) -> Self {
path.0
}
}
impl serde_with::SerializeAs<PathBuf> for ResolvedPathBuf {
fn serialize_as<S>(path: &PathBuf, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
path.serialize(serializer)
}
}
impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
fn deserialize_as<D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
Ok(path.replace_env())
}
}
#[cfg(feature = "schemars")]
impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
fn schema_name() -> String {
"PathBuf".to_owned()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
<PathBuf as schemars::JsonSchema>::json_schema(gen)
}
}
/// Custom deserializer for [`Option<HashMap<usize, PathBuf>>`] that uses
/// [`PathExt::replace_env`] to resolve environment variables in the paths.
///
/// This is used in `WorkspaceConfig` struct because we can't use
/// #[serde_with::serde_as] as it doesn't handle [`Option<HashMap<usize, ResolvedPathBuf>>`]
/// quite well and generated compiler errors that can't be fixed because of Rust's orphan rule.
pub fn resolve_option_hashmap_usize_path<'de, D>(
deserializer: D,
) -> Result<Option<HashMap<usize, PathBuf>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let map = Option::<HashMap<usize, PathBuf>>::deserialize(deserializer)?;
Ok(map.map(|map| map.into_iter().map(|(k, v)| (k, v.replace_env())).collect()))
}
#[cfg(test)]
mod tests {
use super::*;
// helper functions
fn expected<P: AsRef<Path>>(p: P) -> PathBuf {
// Ensure that the path is using the correct path separator for the OS.
p.as_ref().components().collect::<PathBuf>()
}
fn resolve<P: AsRef<Path>>(p: P) -> PathBuf {
p.replace_env()
}
#[test]
fn resolves_env_vars() {
// Set a variable for testing
std::env::set_var("VAR", "VALUE");
// %VAR% format
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
// $env:VAR format
assert_eq!(resolve("/path/$env:VAR/d"), expected("/path/VALUE/d"));
// $VAR format
assert_eq!(resolve("/path/$VAR/d"), expected("/path/VALUE/d"));
// non-existent variable
assert_eq!(resolve("/path/%ASD%/to/d"), expected("/path/%ASD%/to/d"));
assert_eq!(
resolve("/path/$env:ASD/to/d"),
expected("/path/$env:ASD/to/d")
);
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
// Set a $env:USERPROFILE variable for testing
std::env::set_var("USERPROFILE", "C:\\Users\\user");
// ~ and $HOME should be replaced with $Env:USERPROFILE
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
assert_eq!(resolve("$HOME"), expected("C:\\Users\\user"));
}
}

View File

@@ -188,15 +188,16 @@ lazy_static! {
Arc::new(Mutex::new(HidingBehaviour::Cloak));
pub static ref HOME_DIR: PathBuf = {
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
);
}
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
})
};
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");

View File

@@ -22,6 +22,7 @@ use crossbeam_utils::Backoff;
use komorebi::animation::AnimationEngine;
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
use komorebi::replace_env_in_path;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
@@ -176,6 +177,7 @@ struct Opts {
tcp_port: Option<usize>,
/// Path to a static configuration JSON file
#[clap(short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>,
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
#[clap(long)]

View File

@@ -19,7 +19,6 @@ use crate::core::config_generation::ApplicationConfigurationGenerator;
use crate::core::config_generation::ApplicationOptions;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::resolve_home_path;
use crate::core::AnimationStyle;
use crate::core::BorderImplementation;
use crate::core::BorderStyle;
@@ -39,6 +38,7 @@ use crate::current_virtual_desktop;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
use crate::resolve_option_hashmap_usize_path;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
@@ -61,6 +61,7 @@ use crate::Axis;
use crate::CrossBoundaryBehaviour;
use crate::FloatingLayerBehaviour;
use crate::PredefinedAspectRatio;
use crate::ResolvedPathBuf;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
@@ -162,10 +163,12 @@ pub struct ThemeOptions {
pub bar_accent: Option<komorebi_themes::Base16Value>,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Wallpaper {
/// Path to the wallpaper image file
#[serde_as(as = "ResolvedPathBuf")]
pub path: PathBuf,
/// Generate and apply Base16 theme for this wallpaper (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
@@ -175,6 +178,8 @@ pub struct Wallpaper {
pub theme_options: Option<ThemeOptions>,
}
// serde_as must be before derive
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WorkspaceConfig {
@@ -185,12 +190,14 @@ pub struct WorkspaceConfig {
pub layout: Option<DefaultLayout>,
/// END OF LIFE FEATURE: Custom Layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<ResolvedPathBuf>")]
pub custom_layout: Option<PathBuf>,
/// Layout rules in the format of threshold => layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
/// END OF LIFE FEATURE: Custom layout rules (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "resolve_option_hashmap_usize_path", default)]
pub custom_layout_rules: Option<HashMap<usize, PathBuf>>,
/// Container padding (default: global)
#[serde(skip_serializing_if = "Option::is_none")]
@@ -369,16 +376,18 @@ impl From<&Monitor> for MonitorConfig {
}
}
#[serde_with::serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum AppSpecificConfigurationPath {
/// A single applications.json file
Single(PathBuf),
Single(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
/// Multiple applications.json files
Multiple(Vec<PathBuf>),
Multiple(#[serde_as(as = "Vec<ResolvedPathBuf>")] Vec<PathBuf>),
}
#[serde_with::serde_as]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.json` static configuration file reference for `v0.1.36`
@@ -519,6 +528,7 @@ pub struct StaticConfig {
/// Komorebi status bar configuration files for multiple instances on different monitors
// this option is a little special because it is only consumed by komorebic
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<Vec<ResolvedPathBuf>>")]
pub bar_configurations: Option<Vec<PathBuf>>,
/// HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1171,44 +1181,7 @@ impl StaticConfig {
pub fn read(path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
if let Some(path) = &mut value.app_specific_configuration_path {
match path {
AppSpecificConfigurationPath::Single(path) => {
*path = resolve_home_path(&*path)?;
}
AppSpecificConfigurationPath::Multiple(paths) => {
for path in paths {
*path = resolve_home_path(&*path)?;
}
}
}
}
if let Some(monitors) = &mut value.monitors {
for m in monitors {
for w in &mut m.workspaces {
if let Some(path) = &mut w.custom_layout {
*path = resolve_home_path(&*path)?;
}
if let Some(map) = &mut w.custom_layout_rules {
for path in map.values_mut() {
*path = resolve_home_path(&*path)?;
}
}
}
}
}
if let Some(bar_configurations) = &mut value.bar_configurations {
for path in bar_configurations {
*path = resolve_home_path(&*path)?;
}
}
Ok(value)
serde_json::from_str(&content).map_err(Into::into)
}
#[allow(clippy::too_many_lines)]
@@ -1773,7 +1746,6 @@ fn handle_asc_file(
Some(ext) => match ext.to_string_lossy().to_string().as_str() {
"yaml" => {
tracing::info!("loading applications.yaml from: {}", path.display());
let path = resolve_home_path(path)?;
let content = std::fs::read_to_string(path)?;
let asc = ApplicationConfigurationGenerator::load(&content)?;
@@ -1822,8 +1794,7 @@ fn handle_asc_file(
}
"json" => {
tracing::info!("loading applications.json from: {}", path.display());
let path = resolve_home_path(path)?;
let mut asc = ApplicationSpecificConfiguration::load(&path)?;
let mut asc = ApplicationSpecificConfiguration::load(path)?;
for entry in asc.values_mut() {
match entry {
@@ -1885,7 +1856,10 @@ fn handle_asc_file(
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::StaticConfig;
use crate::WorkspaceConfig;
#[test]
fn backwards_compat() {
@@ -1914,4 +1888,40 @@ mod tests {
StaticConfig::read_raw(&config).unwrap();
}
}
#[test]
fn deserialize_custom_layout_rules() {
// set an environment variable for testing
std::env::set_var("VAR", "VALUE");
let config = r#"
{
"name": "Test",
"custom_layout_rules": {
"1": "path/to/dir",
"2": "path/to/%VAR%"
}
}
"#;
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
let custom_layout_rules = config.custom_layout_rules.unwrap();
assert_eq!(
custom_layout_rules.get(&1).unwrap(),
&PathBuf::from("path/to/dir")
);
assert_eq!(
custom_layout_rules.get(&2).unwrap(),
&PathBuf::from("path/to/VALUE")
);
let config = r#"
{
"name": "Test",
}
"#;
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
assert_eq!(config.custom_layout_rules, None);
}
}

View File

@@ -2,12 +2,13 @@
#![allow(clippy::missing_errors_doc, clippy::doc_markdown)]
use chrono::Utc;
use komorebi_client::replace_env_in_path;
use komorebi_client::PathExt;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
@@ -22,7 +23,6 @@ use color_eyre::eyre::bail;
use color_eyre::Result;
use dirs::data_local_dir;
use fs_tail::TailedFile;
use komorebi_client::resolve_home_path;
use komorebi_client::send_message;
use komorebi_client::send_query;
use komorebi_client::AppSpecificConfigurationPath;
@@ -64,8 +64,7 @@ lazy_static! {
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
if home.as_path().is_dir() {
HAS_CUSTOM_CONFIG_HOME.store(true, Ordering::SeqCst);
home
@@ -88,12 +87,12 @@ lazy_static! {
.join(".config")
},
|home_path| {
let whkd_config_home = PathBuf::from(&home_path);
let whkd_config_home = home_path.replace_env();
assert!(
whkd_config_home.as_path().is_dir(),
whkd_config_home.is_dir(),
"$Env:WHKD_CONFIG_HOME is set to '{}', which is not a valid directory",
whkd_config_home.to_string_lossy()
home_path
);
whkd_config_home
@@ -299,6 +298,7 @@ pub struct WorkspaceCustomLayout {
workspace: usize,
/// JSON or YAML file from which the custom layout definition should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
@@ -308,6 +308,7 @@ pub struct NamedWorkspaceCustomLayout {
workspace: String,
/// JSON or YAML file from which the custom layout definition should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
@@ -350,6 +351,7 @@ pub struct WorkspaceCustomLayoutRule {
at_container_count: usize,
/// JSON or YAML file from which the custom layout definition should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
@@ -362,6 +364,7 @@ pub struct NamedWorkspaceCustomLayoutRule {
at_container_count: usize,
/// JSON or YAML file from which the custom layout definition should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
@@ -770,6 +773,7 @@ struct Start {
ffm: bool,
/// Path to a static configuration JSON file
#[clap(short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>,
/// Wait for 'komorebic complete-configuration' to be sent before processing events
#[clap(short, long)]
@@ -832,18 +836,21 @@ struct Kill {
#[derive(Parser)]
struct SaveResize {
/// File to which the resize layout dimensions should be saved
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
#[derive(Parser)]
struct LoadResize {
/// File from which the resize layout dimensions should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
#[derive(Parser)]
struct LoadCustomLayout {
/// JSON or YAML file from which the custom layout definition should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
@@ -874,28 +881,34 @@ struct UnsubscribePipe {
#[derive(Parser)]
struct AhkAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
/// Optional YAML file of overrides to apply over the first file
#[clap(value_parser = replace_env_in_path)]
override_path: Option<PathBuf>,
}
#[derive(Parser)]
struct PwshAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
/// Optional YAML file of overrides to apply over the first file
#[clap(value_parser = replace_env_in_path)]
override_path: Option<PathBuf>,
}
#[derive(Parser)]
struct FormatAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
#[derive(Parser)]
struct ConvertAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
@@ -909,6 +922,7 @@ struct AltFocusHack {
struct EnableAutostart {
/// Path to a static configuration JSON file
#[clap(action, short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>,
/// Enable komorebi's custom focus-follows-mouse implementation
#[clap(hide = true)]
@@ -932,12 +946,14 @@ struct EnableAutostart {
struct Check {
/// Path to a static configuration JSON file
#[clap(action, short, long)]
#[clap(value_parser = replace_env_in_path)]
komorebi_config: Option<PathBuf>,
}
#[derive(Parser)]
struct ReplaceConfiguration {
/// Static configuration JSON file from which the configuration should be loaded
#[clap(value_parser = replace_env_in_path)]
path: PathBuf,
}
@@ -1677,7 +1693,7 @@ fn main() -> Result<()> {
println!("Application specific configuration file path has not been set. Try running 'komorebic fetch-asc'\n");
}
Some(AppSpecificConfigurationPath::Single(path)) => {
if !Path::exists(Path::new(&path)) {
if !path.exists() {
println!("Application specific configuration file path '{}' does not exist. Try running 'komorebic fetch-asc'\n", path.display());
}
}
@@ -1690,8 +1706,7 @@ fn main() -> Result<()> {
// errors
let _ = serde_json::from_str::<StaticConfig>(&config_source)?;
let path = resolve_home_path(static_config)?;
let raw = std::fs::read_to_string(path)?;
let raw = std::fs::read_to_string(static_config)?;
StaticConfig::aliases(&raw);
StaticConfig::deprecated(&raw);
StaticConfig::end_of_life(&raw);
@@ -1994,13 +2009,13 @@ fn main() -> Result<()> {
send_message(&SocketMessage::WorkspaceLayoutCustom(
arg.monitor,
arg.workspace,
resolve_home_path(arg.path)?,
arg.path,
))?;
}
SubCommand::NamedWorkspaceCustomLayout(arg) => {
send_message(&SocketMessage::NamedWorkspaceLayoutCustom(
arg.workspace,
resolve_home_path(arg.path)?,
arg.path,
))?;
}
SubCommand::WorkspaceLayoutRule(arg) => {
@@ -2023,14 +2038,14 @@ fn main() -> Result<()> {
arg.monitor,
arg.workspace,
arg.at_container_count,
resolve_home_path(arg.path)?,
arg.path,
))?;
}
SubCommand::NamedWorkspaceCustomLayoutRule(arg) => {
send_message(&SocketMessage::NamedWorkspaceLayoutCustomRule(
arg.workspace,
arg.at_container_count,
resolve_home_path(arg.path)?,
arg.path,
))?;
}
SubCommand::ClearWorkspaceLayoutRules(arg) => {
@@ -2103,13 +2118,12 @@ fn main() -> Result<()> {
let mut flags = vec![];
if let Some(config) = &arg.config {
let path = resolve_home_path(config)?;
if !path.is_file() {
bail!("could not find file: {}", path.display());
if !config.is_file() {
bail!("could not find file: {}", config.display());
}
// we don't need to replace UNC prefix here as `resolve_home_path` already did
flags.push(format!("'--config=\"{}\"'", path.display()));
let config = dunce::simplified(config);
flags.push(format!("'--config=\"{}\"'", config.display()));
}
if arg.ffm {
@@ -2128,17 +2142,12 @@ fn main() -> Result<()> {
flags.push("'--clean-state'".to_string());
}
let exec = exec.unwrap_or("komorebi.exe");
let script = if flags.is_empty() {
format!(
"Start-Process '{}' -WindowStyle hidden",
exec.unwrap_or("komorebi.exe")
)
format!("Start-Process '{exec}' -WindowStyle hidden",)
} else {
let argument_list = flags.join(",");
format!(
"Start-Process '{}' -ArgumentList {argument_list} -WindowStyle hidden",
exec.unwrap_or("komorebi.exe")
)
format!("Start-Process '{exec}' -ArgumentList {argument_list} -WindowStyle hidden",)
};
let mut system = sysinfo::System::new_all();
@@ -2181,9 +2190,8 @@ fn main() -> Result<()> {
if !running {
println!("\nRunning komorebi.exe directly for detailed error output\n");
if let Some(config) = arg.config {
let path = resolve_home_path(config)?;
if let Ok(output) = Command::new("komorebi.exe")
.arg(format!("'--config=\"{}\"'", path.display()))
.arg(format!("'--config=\"{}\"'", config.display()))
.output()
{
println!("{}", String::from_utf8(output.stderr)?);
@@ -2233,25 +2241,20 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
}
}
let static_config = arg.config.clone().map_or_else(
|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
if komorebi_json.is_file() {
Option::from(komorebi_json)
} else {
None
}
},
Option::from,
);
let static_config = arg.config.clone().or_else(|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
komorebi_json.is_file().then_some(komorebi_json)
});
if arg.bar {
if let Some(config) = &static_config {
let mut config = StaticConfig::read(config)?;
if let Some(display_bar_configurations) = &mut config.bar_configurations {
for config_file_path in &mut *display_bar_configurations {
let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"#
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
let script = format!(
r#"Start-Process "komorebi-bar" '"--config" "{}"' -WindowStyle hidden"#,
config_file_path.to_string_lossy()
);
match powershell_script::run(&script) {
Ok(_) => {
@@ -2313,21 +2316,13 @@ if (!(Get-Process masir -ErrorAction SilentlyContinue))
println!("\n# Documentation");
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
let bar_config = arg.config.map_or_else(
|| {
let bar_json = HOME_DIR.join("komorebi.bar.json");
if bar_json.is_file() {
Option::from(bar_json)
} else {
None
}
},
Option::from,
);
let bar_config = arg.config.or_else(|| {
let bar_json = HOME_DIR.join("komorebi.bar.json");
bar_json.is_file().then_some(bar_json)
});
if let Some(config) = &static_config {
let path = resolve_home_path(config)?;
let raw = std::fs::read_to_string(path)?;
let raw = std::fs::read_to_string(config)?;
StaticConfig::aliases(&raw);
StaticConfig::deprecated(&raw);
StaticConfig::end_of_life(&raw);
@@ -2629,9 +2624,7 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
send_message(&SocketMessage::CycleLayout(arg.cycle_direction))?;
}
SubCommand::LoadCustomLayout(arg) => {
send_message(&SocketMessage::ChangeLayoutCustom(resolve_home_path(
arg.path,
)?))?;
send_message(&SocketMessage::ChangeLayoutCustom(arg.path))?;
}
SubCommand::FlipLayout(arg) => {
send_message(&SocketMessage::FlipLayout(arg.axis))?;
@@ -2808,10 +2801,10 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
send_message(&SocketMessage::QuickLoad)?;
}
SubCommand::SaveResize(arg) => {
send_message(&SocketMessage::Save(resolve_home_path(arg.path)?))?;
send_message(&SocketMessage::Save(arg.path))?;
}
SubCommand::LoadResize(arg) => {
send_message(&SocketMessage::Load(resolve_home_path(arg.path)?))?;
send_message(&SocketMessage::Load(arg.path))?;
}
SubCommand::SubscribeSocket(arg) => {
send_message(&SocketMessage::AddSubscriberSocket(arg.socket))?;
@@ -2923,9 +2916,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
))?;
}
SubCommand::AhkAppSpecificConfiguration(arg) => {
let content = std::fs::read_to_string(resolve_home_path(arg.path)?)?;
let content = std::fs::read_to_string(arg.path)?;
let lines = if let Some(override_path) = arg.override_path {
let override_content = std::fs::read_to_string(resolve_home_path(override_path)?)?;
let override_content = std::fs::read_to_string(override_path)?;
ApplicationConfigurationGenerator::generate_ahk(
&content,
@@ -2950,9 +2943,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
);
}
SubCommand::PwshAppSpecificConfiguration(arg) => {
let content = std::fs::read_to_string(resolve_home_path(arg.path)?)?;
let content = std::fs::read_to_string(arg.path)?;
let lines = if let Some(override_path) = arg.override_path {
let override_content = std::fs::read_to_string(resolve_home_path(override_path)?)?;
let override_content = std::fs::read_to_string(override_path)?;
ApplicationConfigurationGenerator::generate_pwsh(
&content,
@@ -2977,23 +2970,21 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
);
}
SubCommand::ConvertAppSpecificConfiguration(arg) => {
let file_path = resolve_home_path(arg.path)?;
let content = std::fs::read_to_string(&file_path)?;
let content = std::fs::read_to_string(arg.path)?;
let mut asc = ApplicationConfigurationGenerator::load(&content)?;
asc.sort_by(|a, b| a.name.cmp(&b.name));
let v2 = ApplicationSpecificConfiguration::from(asc);
println!("{}", serde_json::to_string_pretty(&v2)?);
}
SubCommand::FormatAppSpecificConfiguration(arg) => {
let file_path = resolve_home_path(arg.path)?;
let content = std::fs::read_to_string(&file_path)?;
let content = std::fs::read_to_string(&arg.path)?;
let formatted_content = ApplicationConfigurationGenerator::format(&content)?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?;
.open(arg.path)?;
file.write_all(formatted_content.as_bytes())?;